Android技巧:学习使用GridLayout
GridLayout是一个非常强大的网格类布局,它不但能像TableLayout那样,实现网格类布局,但它更为强大的地方在于每个Cell的大小可以横向或者纵向拉伸,每个Cell的对齐方式也有很多种,而且不像TableLayout,需要一个TableRow,GridLayout可以通过指定Cell的坐标位置就能实现Cell的拉伸,从而实现,大小不一致的风格卡片式布局。
基本概念
GridLayout把页面分成m行和n列,使用m+1条线和n+1条线,把页面共分成n*m个Cell。指定位置时行坐标是从0到m,列坐标是从0到n。每一个子View占一个或多个Cell。比如(0, 0)到(0, 1)就是占第一个Cell的区域。(0, 0), (0, 2)就是占第一行的2个Cell的区域(横向拉伸).
使用方法
主要介绍一下如何添加Cell,以及设置Cell的位置和拉伸。其他的跟普通的ViewGr0up没什么区别的,也没啥好说的。
GridLayout的基本设置
首先需要给GridLayout设置行数和列数:
- android:columnCount 整数,最多的列数
- android:rowCount 整数,最多的行数
在添加Cell就需要注意,不能超过设置的最大行数和列数,否则在添加Cell时会有异常。
元素Cell的位置控制
添加Cell时需要指定其位置
- android:layout_column 整数n,在哪一列开始显示n=[0, 最大列-1]
- android:layout_columnSpan 整数k,指定元素横跨几列,需要注意保证n+k <= 最大列数
- android:layout_row 指定从哪一行开始显示,规则同列数
- android:layout_rowSpan 纵向跨几行,规则同列
行高和列宽的确定
每一行的高度是由这一行中Cell的最大高度决定的,以及每一列的宽度是由每一列中最大的宽度决定的,小于行高和列宽的元素可以设置其对齐方式和填充方式。
填充方式
通过Cell的android:layout_gravity参数来指定,Cell的填充方式,注意仅当Cell元素本身的尺寸小于它所占格子的大小时才有效,比如元素本身尺寸小于行高和列宽,或者当它占多行,或者占多列时:
- center -- 不改变元素的大小,仅居中
- center_horizontal -- 不改变大小,水平居中
- center_vertical -- 不改变大小,垂直居中
- top -- 不改变大小,置于顶部
- left -- 不改变大小,置于左边
- bottom -- 不改变大小,置于底部
- right -- 不改变大小,置于右边
- start -- 不改变大小,置于开头(这个是与RTL从右向左读的文字有关的,如果使用start/end,那么当LTR文字时start=left,end=right,当RTL时start=right,end=left,也就是说系统会自动处理了)
- end -- 不改变大小,置于结尾
- fill -- 拉伸元素控件,填满其应该所占的格子
- fill_vertical -- 仅垂直方向上拉伸填充
- fill_horizontal -- 仅水平方向上拉伸填充
- clip_vertical -- 垂直方向上裁剪元素,仅当元素大小超过格子的空间时
- clip_horizontal -- 水平方向上裁剪元素,仅当元素大小超过格子的空间时
需要注意的是这些值是可以组合的,比如:
android:layout_gravity="center_vertical|clip_horizontal"
Cell之间的间距如何控制
默认间距
可以使用默认的间距android:useDefaultMargins="true"或者GridLayout#setUseDefaultMargins()。这个属性默认值是"false"。
另外一种方式就是跟普通布局管理器一样,给每个Cell设置其margins
通常如果不满意系统的默认间距,就可以设置useDefaultMargins="false",然后通过给Cell设置margin来控制间距。
居中方法
- 仅有一个Cell或者仅有一行,或者仅有一列时
当仅有一个子View时或者仅有一行或者一列的时候,可以把每个Cell设置其android:layout_gravitiy="center"(相应代码为LayoutParams#Gravity为CENTER),就可以让其在GridLayout中居中。
让一行居中:
<GridLayout
android:layout_width="wrap_content"
android:layout_height="200dip"
android:useDefaultMargins="true"
android:background="@android:color/white"
android:rowCount="1"
android:columnCount="2">
<Button android:layout_column="0"
android:layout_row="0"
android:text="Left Button"
android:layout_gravity="fill_horizontal|center_vertical"/>
<Button android:layout_column="1"
android:layout_row="0"
android:text="Right Button"
android:layout_gravity="fill_horizontal|center_vertical"/>
</GridLayout>
让一个元素居中:
<GridLayout
android:layout_width="200dip"
android:layout_height="200dip"
android:useDefaultMargins="true"
android:background="@android:color/white"
android:rowCount="1"
android:columnCount="1">
<Button android:layout_column="0"
android:layout_row="0"
android:text="Left Button"
android:layout_gravity="center"/>
</GridLayout>
- 其他情况
其他情况,设置子View的Gravity就不再起作用了,这时最好的办法就是让GridLayout的高度是WRAP_CONTENT,然后让GridLayout在其父布局中居中。
<LinearLayout
android:layout_width="match_parent"
android:orientation="vertical"
android:gravity="center"
android:background="@android:color/darker_gray"
android:layout_height="200dip">
<GridLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:useDefaultMargins="true"
android:background="@android:color/white"
android:rowCount="2"
android:columnCount="2">
<Button android:layout_column="0"
android:layout_row="0"
android:text="Left Button"
android:layout_gravity="fill_horizontal|center_vertical"/>
<Button android:layout_column="1"
android:layout_row="0"
android:text="Right Button"
android:layout_gravity="fill_horizontal|center_vertical"/>
<Button android:layout_column="1"
android:layout_row="1"
android:text="Right Button 2"
android:layout_gravity="fill_horizontal|center_vertical"/>
</GridLayout>
</LinearLayout>
适用场景
GridLayout虽然强大,可以当作LinearLayout使用,也可以当作RelativeLayout使用,甚至也能当FrameLayout使用。但是,我们不可以滥用,对于任意布局都一样,不能是它能实现需求就使用它,而是要根据实际的需求,选择最简单,最方便的,同时也要考虑性能。
通常对于类似于网格的布局就可以考虑用GridLayout来实现,或者用LinearLayout横七竖八的套了好几层时也要考虑使用GridLayout。
GridLayout vs GridView or RecyclerView
当要实现网格布局,或者非均匀风格布局时,可能首先想到的就是GridView,但是这也要看实际的情况而定。GridView,ListView以及RecyclerView是用于无限长度列表或者网格的场景,它们最大的特点是无限长度,因此这几个组件的重点在于如何复用Cell以提升性能,以及处理手势事件(Fling)等。所以,每当遇到列表或者网格的时候,先想一下这个长度大概会是多少,如果是在百个以内,且不会随时增长,这时就可以考虑使用静态(非动态复用)的组件比如LinearLayout或者GridLayout来实现。
实例
说的太多都是废话,来一个实例感觉一下子是最直接的:
<GridLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="@android:color/white"
android:alignmentMode="alignMargins"
android:useDefaultMargins="true"
android:columnCount="4"
android:rowCount="5"
android:visibility="visible">
<Button android:layout_column="0"
android:layout_row="0"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dip"
android:text="1"/>
<Button android:layout_column="1"
android:layout_row="0"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dip"
android:text="2"/>
<Button android:layout_column="2"
android:layout_row="0"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dip"
android:text="3"/>
<Button android:layout_column="0"
android:layout_row="1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dip"
android:text="4"/>
<Button android:layout_column="1"
android:layout_row="1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dip"
android:text="5"/>
<Button android:layout_column="2"
android:layout_row="1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dip"
android:text="6"/>
<Button android:layout_column="0"
android:layout_row="2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dip"
android:text="7"/>
<Button android:layout_column="1"
android:layout_row="2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dip"
android:text="8"/>
<Button android:layout_column="2"
android:layout_row="2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dip"
android:text="9"/>
<Button android:layout_column="0"
android:layout_row="3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dip"
android:text="0"/>
<Button android:layout_column="1"
android:layout_row="3"
android:layout_gravity="fill_horizontal"
android:layout_columnSpan="2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dip"
android:text="Delete"/>
<Button android:layout_column="0"
android:layout_row="4"
android:layout_columnSpan="2"
android:layout_gravity="fill_horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dip"
android:text="Clear"/>
<Button android:layout_column="2"
android:layout_row="4"
android:layout_columnSpan="2"
android:layout_gravity="fill_horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dip"
android:text="="/>
<Button android:layout_column="3"
android:layout_row="0"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="fill"
android:padding="10dip"
android:text="+"/>
<Button android:layout_column="3"
android:layout_row="1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="fill"
android:padding="10dip"
android:text="-"/>
<Button android:layout_column="3"
android:layout_row="2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="fill"
android:padding="10dip"
android:text="*"/>
<Button android:layout_column="3"
android:layout_row="3"
android:layout_columnSpan="1"
android:layout_gravity="fill"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dip"
android:text="/"/>
</GridLayout>
参考资料
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
保护原创,请勿转载!
来源:juejin.cn/post/7449673188131717174
Android树形结构,项目通用
效果图
思路
- 树形展开时,将该节点的childList添加到该节点下,并更新树结构
mDataList.addAll(position, childList)
notifyItemRangeInserted(position, childList.size)
- 树形关闭时,树的数据结构移除该节点的childList的数量,并更新树结构
for (i in 0 until childList.size) {
mDataList[position].isExpand=false
mDataList.removeAt(position)
}
notifyItemRangeRemoved(position, childList.size)
- 对于含CheckBox的树形结构,每一个节点都需要监听他的状态,当状态和上一次的状态不一样时,则进行更新。更新不仅更新本节点,还需要递归的方式更新他的子节点;因为当前的选中状态还会牵连到他的父节点,他的父节点变更的话还会牵扯到再上一层,所以也需要递归的方式来更新。
private fun updateNodeState(bean: T) {
//更新子节点
updateChildState(bean)
//更新父节点状态
updateParentState(bean)
notifyDataSetChanged()
}
更新子节点
private fun updateChildState(bean: T) {
for (child in bean.getChildList()) {
//更新子节点状态
child.checkState = bean.checkState
//递归更新子节点
updateChildState(child)
}
}
更新父节点
private fun updateParentState(bean: T) {
//找到父节点并更新
mDataList.forEach { parent ->
if (bean.getMyId() in parent.getChildList().map { it.getMyId() }) {
//全部选中
val allChecked =
parent.getChildList().all { it.checkState == TriStateCheckBox.State.CHECKED }
val allUnChecked =
parent.getChildList().all { it.checkState == TriStateCheckBox.State.UNCHECKED }
if (allChecked) {
parent.checkState = TriStateCheckBox.State.CHECKED
} else if (allUnChecked) {
parent.checkState = TriStateCheckBox.State.UNCHECKED
} else {
parent.checkState = TriStateCheckBox.State.PARTIALLY_CHECKED
}
//递归更新父节点
updateParentState(parent)
}
}
}
- 设置选中项时,可以先获取到selectList中的所有叶子节点,然后再更新整个树形结构的mDataList选项
//获取所有的叶子节点
private fun getLeafNodeList(selectedList: List<T>):List<T>{
val result = mutableListOf<T>()
for (bean in selectedList){
if (bean.hasChild()){
result.addAll(getLeafNodeList(bean.getChildList()))
}else{
result.add(bean)
}
}
return result
}
fun setSelectedList(selectedList:List<T>){
//选中的叶子节点列表
val selectedChildNodeList = getLeafNodeList(selectedList).toMutableList()
//通过递归的方式检查子列表
updateSelectedTree(mDataList, selectedChildNodeList)
notifyDataSetChanged()
}
- 因为想通过泛型的方式,适用于任何项目,所以我们搞一个抽象类,包含该节点的层级、是否展开、选中状态等属性
abstract class TreeBaseBean<T>{
//层级
var level:Int=0
//是否展开
var isExpand = false
//当前节点状态
var checkState: TriStateCheckBox.State = TriStateCheckBox.State.UNCHECKED
//判断是否有子节点
fun hasChild():Boolean = !getChildList().isNullOrEmpty()
//获取子节点列表
abstract fun getChildList():List<T>
//获取当前节点id
abstract fun getMyId():Any
//获取父节点id
abstract fun getMyParentId():Any?
}
步骤
处理数据
将项目中的树形数据结构继承自TreeBaseBean,重写该抽象类中的方法
data class MenuBean(
var id: String = "",
var parentId: Any? = null,
var menuName: String = "",
var menuType: String = "",
var router: String = "",
var sort: Int = 0,
var icon: String = "",
var sonList: List<MenuBean> = listOf(),
var status: String = "",
var userid: String=""
): TreeBaseBean<MenuBean>() {
override fun getChildList(): List<MenuBean> {
return sonList
}
override fun getMyId(): Any {
return id
}
override fun getMyParentId(): Any? {
return parentId
}
}
2、建立自己的ItemView,确保里面包含有一个命名为ivArrow的箭头图片,一个命名为mCheckBox的CheckBox或自定义三种状态的TriStateCheckBox
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/root">
<ImageView
android:id="@+id/ivArrow"
android:layout_width="18dp"
android:layout_height="18dp"
android:src="@drawable/ic_keyboard_arrow_right_black_18dp"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@id/mCheckBox"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="@id/mCheckBox" />
<com.sxygsj.treefinalcase.TriStateCheckBox
android:id="@+id/mCheckBox"
android:layout_width="20dp"
android:layout_height="20dp"
app:layout_constraintLeft_toRightOf="@id/ivArrow"
app:layout_constraintTop_toTopOf="@id/tvCheckName"
app:layout_constraintBottom_toBottomOf="@id/tvCheckName"/>
<TextView
android:id="@+id/tvCheckName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="内容"
app:layout_constraintLeft_toRightOf="@id/mCheckBox"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginLeft="5dp"
android:paddingVertical="5dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
使用适配器
//数据项
private val dataList= mutableListOf<MenuBean>()
//设置的选中项
private val selectedList = mutableListOf<MenuBean>()
private lateinit var adapter: ChainCommonTreeCheckboxAdapter<MenuBean>
private fun initRcy() {
adapter = ChainCommonTreeCheckboxAdapter.Builder<MenuBean>()
.setData(dataList)
.setLayoutId(R.layout.item_checkbox_tree)
.addBindView { itemView, bean, level ->
itemView.findViewById<TextView>(R.id.tvCheckName).setText("层级${level}:"+bean.menuName)
}
.addItemClickListener {
Toast.makeText(this,"点击了:${it.menuName}", Toast.LENGTH_SHORT).show()
}
.setPadding(16)
.create()
binding.apply {
mRcy.layoutManager = LinearLayoutManager(this@ChainMultiActivity)
mRcy.adapter=adapter
}
}
//设置选中项
adapter.setSelectedList(selectedList)
//默认获取选中和部分选中节点
val list = adapter.getSelectedList()
//获取所有的叶子节点(具体想要获取怎么的选中项,可以通过lambda方式来自己设定规则)
val list = adapter.getSelectedList { bean ->
bean.checkState == TriStateCheckBox.State.CHECKED&&!bean.hasChild()
}
总结
其实整体的关键操作是数据的处理,怎么通过递归的方式,联动更新各个节点的状态是最重要的,关键代码在思路里已整理,剩余的三种状态的CheckBox、链式调用的通用适配器有时间再更新。
来源:juejin.cn/post/7434799466974134284
Android手机投屏方案实现方式对比
1.概述
手机投屏是目前市场上常见的一个功能,在车机娱乐场景,辅助驾驶场景比如苹果的carplay,VR 场景都很常见,目前市场上的投屏分为三类:
第一类: 镜像模式,直接把手机上整个界面原封不动进行投射。这类投屏通常是对手机进行录屏,然后编码成视频流数据的方式给到接受端,接收端再解码播放,以此完成投屏功能。比如AirPlay的镜像模式、MiraCast、乐播投屏等;
第二类: 推送模式,播视频的场景比较常见。即A把一个视频链接传给B,B自己进行播放,后学A可以传输一些简单控制指令。比如DLNA协议等;
第三类: 基于特殊协议投射部分应用或部分功能,车载领域居多。比如苹果的CarPlay、华为HiCar、百度CarLife等。
这里还有一种投屏方式比较新颖,将手机上的画面投到车机上,然后手机上可以操作自己的功能,车机上也可以操作手机的功能,而且两者互不干涉,具体可以参考蔚来手机和车机的投屏:蔚来手机的投屏视频 今天的主要内容是介绍实现投屏的各种技术方式,主要介绍Miracast、scrcpy、以及Google cast的实现方式以及优缺点局限性。
2.术语解释
2.1 miracast
Miracast是一种以WiFi直连为基础的无线显示标准,它允许用户通过无线方式分享视频画面。这种技术支持用户将智能手机、平板电脑、笔记本电脑等设备上的内容投射到大屏幕电视或其他显示设备上,而无需使用线缆连接。
2.2 scrcpy
Scrcpy是一种开源的命令行工具,允许用户通过USB数据线或Android ADB(Android调试桥)来控制他们的Android设备,包括手机和平板电脑。使用Scrcpy,用户可以在电脑上实时查看和控制他们的Android设备,就像使用一个远程屏幕一样。
2.3 DLNA投屏
DLNA投屏是一种通过网络将多媒体内容从一台设备传输到另一台设备的技术。它允许用户将智能手机、平板电脑或电脑上的视频、音频和图片等内容投射到支持DLNA的电视、音响系统或其他显示设备上。DLNA投屏基于设备之间的WiFi连接,无需额外的物理连接或设置,使用户能够轻松地将手机上的媒体内容投屏到大屏幕上并实现双向控制。
2.4 Wifi Direct
WiFi Direct是一种允许设备通过WiFi直接相互连接的技术,无需通过路由器或中继点。这种技术使得设备之间的连接更加直接和便捷,常用于文件共享、打印服务和Miracast投屏等场景。
2.5 app_process
是Android原生的一个可执行程序,位于/system/bin目录下,zygote进程便是由这个执行文件启动的。
3.技术实现对比
3.1 Miracast
3.1.1 Miracast介绍
Miracast是一种无线技术,用于将屏幕无线连接到我们的计算机。它是由WiFi联盟制定,以WiFi-Direct、IEEE802.11为无线传输标准,允许手机向电视或其他接收设备进行无线投送视频、图片。和Miracast类似的投屏协议,还有Airplay、DLNA、chromecast等,Miracast是点对点网络,用于类似蓝牙的方式(比蓝牙更高效)无线发送由Wi-Fi Direct连接组成的截屏视频。大多数最新一代的设备(例如笔记本电脑、智能电视和智能手机)都可以支持该技术,Miracast还支持高达1080p(全高清)的分辨率和5.1环绕声。它还支持4k分辨率。通过无线连接,视频数据以H.264格式发送,这是当今最常见的高清视频编码标准。Miracast在诞生之初就以跨平台标准而设计,这意味着它能在多种平台间使用。
3.1.2 Miracast原理
Miracast基于WiFi P2P,或TDLS,或Infrastructure进行设备发现,位于OSI模型的数据链路层。而媒体传输控制使用RTSP协议,还有远程I2C数据读写、UIBC用户输入反向信道、HDCP高带宽内容保护等,位于OSI模型的TCP/IP传输控制层与网络层。其中,由音视频数据封装成PES包,经过HDCP内容保护,再封装成TS包,接着封装成RTP包,使用RTSP协议发送。如下图所示
3.1.3 Miracast优缺点分析
优点:投屏画质清晰,兼容性好。Android手机集成了Mircast投屏,如果想要二次开发可以从AOSP源码中找到对应的实现,网上的开发文档多
缺点: Miracast正常工作时,Wi-Fi工作在P2P模式,源端与接收端建立一对一的联接。也即当一个设备与一个接收端建立连接后,其它设备不可见该接收端,也就不能投屏。只有当该设备退出连接后,其它设备才能投屏。所以无法实现抢占功能。Miracast底层封装了UDP传输协议,没有严谨的问答机制。所以在实际使用过程中,当遇到干扰时,容易造成丢帧花屏现象。而传输过程中,一旦出现花屏,给客人的感觉就非常糟糕,现在市面上,哪些无线投屏设备之所以经常出现花屏、马赛克就是这个原因。另外,Miracast是操作系统供应商提供,一般都是在安卓系统上使用,但是安卓协议导致手机投屏没有声音,所以大多数用户在安卓手机无线投屏的时候,需要开启蓝牙,以便于把声音投屏过去。如果我们需要使用Mircast,需要对ROM进行二次开发。下面是一个投屏技术公司的关于Miracast的技术文档,描述了目前Mircast存在的问题。Mircast目前存在的问题 若要实现双向控制,需要加一个控制的通道和事件转换和注入
3.2 Scrcpy
3.2.1 scrcpy 介绍
scrcpy通过adb调试的方式来将手机屏幕投到电脑上,并可以通过电脑控制Android设备。它可以通过USB连接,也可以通过Wifi连接(类似于隔空投屏),使用adb的无线连接后投屏,而且不需要任何root权限,不需要在手机里安装任何程序。scrcpy同时适用于GNU / Linux,Windows和macOS。Scrcpy 显示的每帧画面的大小达到1920x1080或者更高,帧率在30~60fps,延迟很低(大约35~70ms),启动快,第一帧画面显示出来的时间大约为1秒,并且不需要安装任何apk。并且代码完全开源,源码地址:github.com/Genymobile/…
3.2.2 scrcpy的实现原理
Scrcpy的基本原理是通过ADB(Android Debug Bridge)将电脑和手机连接到一起后,推送一个jar文件到手机/data/local/tmp的目录下,然后通过adb shell 执行app_process 程序将jar文件运行起来,这个jar文件相当于是手机上运行的一个服务器,它的作用是处理来自电脑端的的数据请求。它的免root原理主要基于两个关键点:
- 利用AIDL (Android Interface Definition Language):Scrcpy通过ADB(Android Debug Bridge)连接手机,AIDL允许非系统应用(如scrcpy)与系统服务交互。尽管root可以访问更多的底层功能,但是像显示屏幕这样的操作通常是安全的,并且无需获得root权限。
- 屏幕录制协议:Scrcpy设计了一个简单的UDP(User Datagram Protocol)服务器,在手机上运行,这个服务器只处理来自客户端(如电脑上的scrcpy软件)的数据请求,而不是系统级别的控制命令。这种方式避免了直接修改系统的文件系统或设置。
简单总结scrcpy的原理就是电脑端和手机端建立连接后通过3个socke通道分别传输音频,录频,控制信号去实现手机和电脑的数据共享,录屏和音频都可以通过aidl和系统的服务交互拿到对应的显示屏ID然后创建虚拟屏录制,然后再编码给到客户端(电脑端)解码显示。控制指令通过socket传输到手机端后,通过手机端的服务(shell 通过app_process启动的那个程序) 反射调用Android的事件注入接口实现的。下面是scrcpy的源码中关于事件注入的部分。
3.2.3 scrcpy的优缺点分析
优点:Scrcpy的优点是显示的画质好,延迟低(大约3570ms),帧率3060fps,非常流畅,而且代码完全开源并有很详细的文档,并且不需要安装任何apk和root权限。能自定义控制的行为,比如显示音频和视频,只播放音频,只显示视频,只投屏(不接受电脑端的控制,类似于投屏中的镜像)
缺点:需要用户打开开发者模式中的USB调试模式,否则很多的操作都无法进行了。这点会导致产品无法用于正式的生产环境中,因为用户一般都不会打开开发者选项中的USB调试模式。如果通过修改源码的方式,则无法实现事件注入的功能,因为事件注入需要依赖adb shell。
3.3 Google cast
3.3.1 Google cast 介绍
Google Cast类似于DLNA,AirPlayer,Miracast,就是一种投屏技术。Google Cast的作用在于把小屏幕(诸如手机、平板、笔记本)的内容通过无线(WIFI)方式发送到大屏设备(google TV、chromeCast)进行播放。Google Cast所做的便在于基于不同的平台提供提供为应用开支这种功能的SDK,这些平台即有发送端的也有接收端的,发送端的有IOS、android、chrome浏览器,接收端的有google TV, chromeCast等,可以说这一套解决方案是比较大而全的(就其涵盖的平台)。
3.3.2 Google cast 的实现原理
发送端 app(sender app)使用 SDK,将需要播放的媒体的信息发送到 Google 的服务器,服务器再通知接收端播放(所以发送端和接收端必须都可以访问 Google 的服务器才行)。接收端运行的是一个浏览器,它会根据发送端的app ID和媒体信息,去载入对应的一个网页,这个网页(receiver app)也是由发送端 app 的开发者提供的,的将会负责播放相应的媒体内容。即使接收端是 Chromecast Audio 之类只能播放音频的硬件,这个网页也是会载入并渲染的。Google Cast 和 DLNA 或者苹果的 AirPlay 不同之处,一是依赖 Google 的服务器,也就是说必须连接到 Internet 才可以用,如果只有一个局域网是不行的。二是前两个的接收端播放器接收端本身提供的,开发者只需要提供要播放的内容就可以,但是 Google Cast 则是需要提供自己的receiver app,这样的好处是开发者可以高度定制(比如可以定制UI,或者加入弹幕、歌词滚动、音乐可视化之类复杂功能),虽然接收端往往运行的并不是Android这样的开放操作系统,但是因为receiver app的本质是网页,所以开发难度并不高。
3.3.3 优缺点分析
优点:就是高度可定制,有官方成熟的SDK可接入,从宣传视频中看到手机可以投屏到大屏后,然后就可以随意操作其他应用而不会影响到大屏的显示内容了。
缺点:平台依赖性强,必须可以访问Google服务器,而由于国情的原因,必须可访问Google服务器这个缺点就可以宣告这个方案不合适了
总结
本文主要介绍了各种Android手机投屏的实现方式以及优缺点,手机投屏经常会涉及到投屏端和接收端端相互操作以及音频的播放。所以在建立了投屏需要建立好几个连接通道,分别传输音频、控制指令和录屏的视频流。scrcpy就是这样实现的,如果我们能获取到权限,目前决定scrcpy是最好的投屏实现方式。由于没有权限,现在的大多数控制都是通过Android手机的无障碍模式实现的。这就是我对手机投屏的一些调研总结,希望能帮到有需要的读者
来源:juejin.cn/post/7419297143787716618
Android热修
大家好,我是瑞英。
本篇会描述一个完整的安卓热修复工具的设计与实现。该热修工具基于instantRun原理,并且参照美团开源的robust框架,能够有效进行代码热修、so热修以及资源热修
热修复:无需通过发版,修复线上客户端问题的一种技术方案,特点是快速止损。
本文描述中:base包就是宿主包【需要被修复的包】,patch包是下发到base包的修复包
为何要热修?
客户端线上出现问题,传统的解决方案就是发一个新的客户端版本,让用户主动触发升级应用,覆盖速度十分有限。问题修复时间越长,损失就会越大。需要一种可以快速修复线上客户端问题的技术-称之为热修复。
热修复能够做到用户无感知,快速修复线上问题
热修方案概述
原理上看,目前安卓热修主要分三种:基于类加载、基于底层替换、侵入方法插桩的。
主流热修产品
厂商 | 产品 | 修复范围 | 修复时机 | 稳定性 | 接入成本 | 技术方案 |
---|---|---|---|---|---|---|
腾讯 | tinker | 类、资源、so | 冷启 | 一般 | 高 | 合成差量热修dex并冷启加载 |
阿里 | sophix | 类、资源、so | 冷启动、即时修复都支持(可选) | 高 | 高(商用) | 综合方案(底层替换方案&类加载方案) |
美团 | robust | 方法修复 | 及时修复 | 高 | 低 | 下文详细介绍 |
代码修复方案
底层替换方案
直接在native层,将被修复类对应的artMethod进行替换,即可完成方法修复。
每一个java方法在art中都对应着一个ArtMethod,记录了这个java方法的所有信息:所属类、访问权限、代码执行地址等
特性:
- 无法实现对原有类方法和字段的增减(只支持方法替换)
- 修复了的非静态方法,无法被正常发射调用(因为反射调用的时候会verifyObjectIsClass)
- 实效性好,可立即加载生效无需重启应用
- 需要针对dalvik虚拟机和art虚拟机做适配,需要考虑指令集的兼容问题
- 无法解决匿名内部类增减的情况
- 不支持 <clinit>方法热修
类加载方案
合成修复后全量dex,冷启重新加载类,完成修复
特性:
- 需要冷启生效
- 高兼容性,几乎可以修复任何代码修复的场景
so修复方案
通过反射将指定热修so路径插入到nativeLibraryDirectories
base构建时保留所有so的md5值,patch包构建时,会进行校验,识别出发生变动的热修so,并将其打入patch中
资源修复方案
资源热修包的构建:
base构建时会保留base包的资源id,以及所有资源md5值,patch构建时,利用base id实现资源id固定,同时将新增资源打入patch中,使用新增资源的方法被自动标注为修复方法
资源热修包的加载:
通过反射调用AssetManager.addAssetPath,添加热修资源路径,在activity loadResources时,触发load热修资源
代码修复方案详解
在base包构建时,对需要被热修的方法进行插桩,保留相关base包构建信息【方法、类、属性以及其混淆信息】,在热修包构建时,依赖注解识别出被热修的方法,并结合base包相关信息,最终构建出热修包。
实现修复的原理
在base包构建时,对于方法都插入一个条件分支,执行热修代理调用。如果热修代理方法返回结果为true,则当前方法直接返回热修result,即该方法被成功热修【如下图所示】。当然这种侵入base包构建的热修方案,会导致包体积有所增加。
详解base包插桩指令
根据方法的参数和返回值特性,进行不同proxy方法的插入
- 根据返回值分类:
无返回值,则proxy方法直接返回boolean即可,如此被插桩方法中不需要出现proxyResult.isSupport的判断
有返回值:需要返回ProxyResult
- 根据参数个数进行分类,使得在插桩时,插桩方法的参数尽可能的少且简单,即插入指令尽可能的少。(目前对5个及以下的参数个数进行分类)
只有5个以上的参数方法被插桩时,需要采用Object[]数组传递所有的参数。因为构建数组并且初始化数组元素,所需要的指令较多。
例如:若方法只有一个参数,那么直接传递object对象只需要1条指令,如果通过Object[]传递该对象需要6条指令
//有一个参数str:String,存放与局部变量表中 index = 1
//直接传递该object对象
mv.visitMethodInsn(ALOAD, 1)
//利用object数组进行传递
mv.visitInsn(1)//数组大小
mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object")
mv.visitInsn(Opcodes.DUP)// 创建数组object[]
mv.visitInsn(Opcodes.ICONST_0)// 下标索引
mv.visitVarInsn(Opcodes.ALOAD, 1) //获取局部变量表中该object对象
mv.visitInsn(Opcodes.AASTORE) //存入数组中
- 插入的热修代理方法示例
@JvmStatic
fun proxyVoid4Para(
param1: Any?,
param2: Any?,
param3: Any?,
param4: Any?,
obj: Any?,
cls: Class<*>,
methodNumber: Int
): Boolean {
return proxy(arrayOf(param1, param2, param3, param4), obj, cls, methodNumber).isSupported
}
@JvmStatic
fun proxy4Para(param1: Any?, param2: Any?, param3: Any?, param4: Any?, obj: Any?, cls: Class<*>, methodNumber: Int): PatchProxyResult {
return proxy(arrayOf(param1, param2, param3, param4), obj, cls, methodNumber)
}
- proxy方法传递的参数详解
- 当前方法的参数
- 当前类(用于查找当前类是否有热修对象)
- 当前类对象(如果是静态方法则传null,用于对当前类非静态属性的访问)
- 方法编号(用于匹配热修方法)
详解patch包插桩
每一个被修复的类(PatchTestAct)必然会插桩生成两个类:
- Patch类(PatchTestActPatch),这个类中有修复方法
- 一个控制类(实现ChangeQuickRedirect接口,PatchTestActPatchControl),分发执行Patch类中的修复方法
从上述PatchProxy.proxy方法中可以看出。所有被热修的类,会被存在一个重定向map中。执行proxy方法时,若表中有该被插桩类,则对应执行该插桩类的热修对象(ChangeQUickRedirect实现类对象),执行该对象的
accessDispatch方法。每个方法在base构建时都会有一个编号。热修对象通过传入的方法编号,确定最终执行的热修方法。
public interface ChangeQuickRedirect {
/**
* 将方法的执行分发到对应的修复方法
* @param methodName 被插桩的方法编号
* @param paramArrayOfObject 参数值列表
* @param obj 被插桩类对象
* @return
*/
Object accessDispatch(String methodNumber, Object[] paramArrayOfObject, Object obj);
/**
* 判断方法是否能被分发到对应的修复方法
*/
boolean isSupport(String methodNumber);
/** * 判断方法是否能被分发到对应的修复方法 */ boolean isSupport(String methodNumber);
}
如上述例子中,要热修该PatchTestAct2.test方法,对该方法加上@Modify注解后,进行热修patch构建后生成的PatchControl类和Patch类分别是:
public class PatchTestActPatchControl implements ChangeQuickRedirect {
public static final String MATCH_ALL_PARAMETER = "(\\w*\\.)*\\w*";
private static final Map<Object, Object> keyToValueRelation = new WeakHashMap();
public PatchTestActPatchControl() {
}
public Object accessDispatch(String methodNumber, Object[] paramArrayOfObject, Object var3) {
try {
PatchTestActPatch var4 = null;
if (var3 != null) {
if (keyToValueRelation.get(var3) == null) {
var4 = new PatchTestActPatch(var3);
keyToValueRelation.put(var3, (Object)null);
} else {
var4 = (PatchTestActPatch)keyToValueRelation.get(var3);
}
} else {
var4 = new PatchTestActPatch((Object)null);
}
if ("119".equals(methodNumber)){var4.invokeAddMethod((Context)paramArrayOfObject[0]);
}
if ("120".equals(methodNumber)) {
var4.test((String)paramArrayOfObject[0], (Function1)paramArrayOfObject[1]);
}
} catch (Throwable var7) {
var7.printStackTrace();
}
return null;
}
public boolean isSupport(String methodName) {
return ":119::120:".contains(":" + methodName + ":");
}
private static Object fixObj(Object booleanObj) {
if (booleanObj instanceof Byte) {
byte byteValue = (Byte)booleanObj;
boolean booleanValue = byteValue != 0;
return new Boolean(booleanValue);
} else {
return booleanObj;
}
}
// 看起来好像没有用到这个方法
public Object getRealParameter(Object var1) {
return var1 instanceof PatchTestAct ? new PatchTestActPatch(var1) : var1;
}
}
public class PatchTestActPatch {
PatchTestAct originClass;
/**
* 传入原始对象
*/
public PatchTestActPatch(Object var1) {
this.originClass = (PatchTestAct)var1;
}
/**
* 将所访问的变量做一个转换,如果访问的是当前类this,则需要转换为this.originClass对象
*/
public Object[] getRealParameter(Object[] var1) {
if (var1 != null && var1.length >= 1) {
Object[] var2 = (Object[])Array.newInstance(var1.getClass().getComponentType(), var1.length);
for(int var3 = 0; var3 < var1.length; ++var3) {
if (var1[var3] instanceof Object[]) {
var2[var3] = this.getRealParameter((Object[])var1[var3]);
} else if (var1[var3] == this) {
var2[var3] = this.originClass;
} else {
var2[var3] = var1[var3];
}
}
return var2;
} else {
return var1;
}
}
/**
* 被修复的方法
*/
public final void test(String str, Function1<? super String, Unit> a) {
String var3 = "str";
Object[] var5 = this.getRealParameter(new Object[]{str, var3});
Class[] var6 = new Class[]{Object.class, String.class};
EnhancedRobustUtils.invokeReflectStaticMethod("checkNotNullParameter", Intrinsics.class, var5, var6);
String var7 = "a";
Object[] var9 = this.getRealParameter(new Object[]{a, var7});
Class[] var10 = new Class[]{Object.class, String.class};
EnhancedRobustUtils.invokeReflectStaticMethod("checkNotNullParameter", Intrinsics.class, var9, var10);
Object[] var12 = this.getRealParameter(new Object[]{str});
Class[] var13 = new Class[]{Object.class};
Object var14;
if (a == this && 0 == 0) {
var14 = ((PatchTestActPatch)a).originClass;
} else {
var14 = a;
}
Object var10000 = (Object)EnhancedRobustUtils.invokeReflectMethod("invoke", var14, var12, var13, Function1.class);
}
}
每一个新增方法(在base包中不存在的方法):
对这个新增方法所在类打一个InlinePatch.class类,该类中定义这个新增方法
热修代码的处理过程
从字节码到patch.dex中
代码修复中解决的关键问题
本方案支持,方法修复、新增方法、新增类、新增属性、新增override方法。主要解决了以下问题:
- 修复方法中对其他类属性、方法的调用
- 修复代码中,存在调用base包中被删除的方法的指令
- 修复代码中存在匿名内部类的生成和使用、when表达式与enum联用
- 修复方法中存在调用父类方法的指令
- 修复代码中存在invokeDynamic指令(单接口lambda表达式/函数式接口、高阶函数等)
- 新增方法是override方法,并且使用其多态属性
- 修复构造方法、新增构造方法
- 修复方法有@JvmStatic注解,@JvmOverloads注解,这些注解方法被java 和kotlin调用不同而编译出不同的字节码
- r8内联、外联、类合并等系列优化操作,使得编译结果与原始字节码有很大的差异
总结
本文所描述的代码修复方案,相对于美团原始方案做了较大优化,base插桩对插入指令做了精简,且不再对每个类插入属性用于判断当前类是否被热修,而是将被修复类的信息存在一个静态map中。patch插桩完全重新处理,大大拓展了可修复的范围,提高了热修工具可用性。后续也扩展支持了,通过字节码对比自动识别需要修复的代码,无需开发者手动标注。
除上文所述之外,热修也有一些其他方面值得讨论,热修sop、热修包的构建速度提升,以及热修包的下发和加载等。
来源:juejin.cn/post/7426988056635015206
协程:解锁 Android 开发的超级英雄技能!
开发 Android 应用时,是否有过这样的时刻?
"我只是想请求个网络数据,为什么我的主线程就卡住了!"
"多线程真香,但这锁和回调让我头都大了!"
别担心!
今天我们来认识 Android 开发中的超级英雄——协程(Coroutines) ,它能让你以最优雅的方式,化解多线程的痛点,轻松应对复杂的异步操作!
🦸♂️ 协程是个啥?
想象一下,你正在玩一款冒险游戏。游戏里的主角不仅可以“战斗”,还能“暂停”战斗,去完成一些重要任务(比如喝口水、回血),然后再无缝回归到战斗中继续打怪。
协程就像这个灵活的冒险主角:
- 轻量级线程:协程不是普通线程,但它可以暂停和恢复。
- 灵活暂停和恢复:随时挂起(
suspend
),随时回来。 - 并发管理高手:让你在多任务之间游刃有余,再也不用担心线程锁或回调地狱。
🎯 协程的核心武器
协程的核心技能就三个字:
挂起(Suspend)!
挂起函数是协程的魔法技能,虽然它暂停了自己,但不会阻塞线程。
比如这个最简单的例子:
suspend fun fetchData(): String {
delay(1000) // 模拟网络请求,暂停1秒
return "服务器返回的数据"
}
看到没有?协程就像在说:
"我歇一会儿,待会儿继续,不耽误别人干活!"
🤹♀️ 协程的奇幻队伍
协程离不开一支强大的“队友团队”,它们是:
1️⃣ GlobalScope
协程的孤狼模式,一旦启动就运行到世界尽头,但稍有不慎可能造成内存泄漏,慎用!
GlobalScope.launch {
println("这是一个孤单的协程")
}
2️⃣ CoroutineScope
协程的队伍领袖,能保证所有子协程的生命周期跟它挂钩,避免资源泄漏。
class MyActivity : AppCompatActivity() {
private val scope = CoroutineScope(Dispatchers.Main)
override fun onDestroy() {
super.onDestroy()
scope.cancel() // 活动销毁时取消协程
}
}
3️⃣ Dispatchers
调度器,决定协程的运行地点:
- Main:UI线程,适合更新界面。
- IO:专注网络请求、文件读写。
- Default:CPU密集型任务。
- Unconfined:自由漂流,不常用。
🛠️ 协程实战:网络请求案例
假设你是一位披风加身的 Android 开发英雄,任务是从服务器获取一段数据并显示在界面上:
fun fetchDataAndShow() {
CoroutineScope(Dispatchers.Main).launch {
try {
val data = withContext(Dispatchers.IO) {
fetchData() // 网络请求在 IO 线程中执行
}
textView.text = data // 回到主线程更新 UI
} catch (e: Exception) {
textView.text = "出错啦!${e.message}"
}
}
}
suspend fun fetchData(): String {
delay(2000) // 模拟网络请求
return "这是来自服务器的数据"
}
你看,协程让异步操作简洁优雅,完全不需要复杂的回调!
以下是一篇轻松有趣、但同时技术性强的文章,主题是 "协程:解锁 Android 开发的超级英雄技能!" :
协程:解锁 Android 开发的超级英雄技能!
开发 Android 应用时,是否有过这样的时刻?
"我只是想请求个网络数据,为什么我的主线程就卡住了!"
"多线程真香,但这锁和回调让我头都大了!"
别担心!
今天我们来认识 Android 开发中的超级英雄——协程(Coroutines) ,它能让你以最优雅的方式,化解多线程的痛点,轻松应对复杂的异步操作!
🦸♂️ 协程是个啥?
想象一下,你正在玩一款冒险游戏。游戏里的主角不仅可以“战斗”,还能“暂停”战斗,去完成一些重要任务(比如喝口水、回血),然后再无缝回归到战斗中继续打怪。
协程就像这个灵活的冒险主角:
- 轻量级线程:协程不是普通线程,但它可以暂停和恢复。
- 灵活暂停和恢复:随时挂起(
suspend
),随时回来。 - 并发管理高手:让你在多任务之间游刃有余,再也不用担心线程锁或回调地狱。
🎯 协程的核心武器
协程的核心技能就三个字:
挂起(Suspend)!
挂起函数是协程的魔法技能,虽然它暂停了自己,但不会阻塞线程。
比如这个最简单的例子:
suspend fun fetchData(): String {
delay(1000) // 模拟网络请求,暂停1秒
return "服务器返回的数据"
}
看到没有?协程就像在说:
"我歇一会儿,待会儿继续,不耽误别人干活!"
🤹♀️ 协程的奇幻队伍
协程离不开一支强大的“队友团队”,它们是:
1️⃣ GlobalScope
协程的孤狼模式,一旦启动就运行到世界尽头,但稍有不慎可能造成内存泄漏,慎用!
GlobalScope.launch {
println("这是一个孤单的协程")
}
2️⃣ CoroutineScope
协程的队伍领袖,能保证所有子协程的生命周期跟它挂钩,避免资源泄漏。
class MyActivity : AppCompatActivity() {
private val scope = CoroutineScope(Dispatchers.Main)
override fun onDestroy() {
super.onDestroy()
scope.cancel() // 活动销毁时取消协程
}
}
3️⃣ Dispatchers
调度器,决定协程的运行地点:
- Main:UI线程,适合更新界面。
- IO:专注网络请求、文件读写。
- Default:CPU密集型任务。
- Unconfined:自由漂流,不常用。
🛠️ 协程实战:网络请求案例
假设你是一位披风加身的 Android 开发英雄,任务是从服务器获取一段数据并显示在界面上:
fun fetchDataAndShow() {
CoroutineScope(Dispatchers.Main).launch {
try {
val data = withContext(Dispatchers.IO) {
fetchData() // 网络请求在 IO 线程中执行
}
textView.text = data // 回到主线程更新 UI
} catch (e: Exception) {
textView.text = "出错啦!${e.message}"
}
}
}
suspend fun fetchData(): String {
delay(2000) // 模拟网络请求
return "这是来自服务器的数据"
}
你看,协程让异步操作简洁优雅,完全不需要复杂的回调!
🎮 协程的高阶玩法
1️⃣ 并发:一心多用
协程中的并发很简单,像玩双开游戏一样:
suspend fun loadData() = coroutineScope {
val data1 = async { fetchData() }
val data2 = async { fetchData() }
println("数据1: ${data1.await()}, 数据2: ${data2.await()}")
}
只要用了 async
,就能并发运行多个任务,效率提升 N 倍!
2️⃣ 结构化并发:协程的守护者
协程不像传统线程那么“放飞自我”。当它的宿主(比如 CoroutineScope
)取消了,所有子协程也会跟着取消。这种“组队行动”叫做结构化并发。
coroutineScope {
launch { delay(1000); println("任务1完成") }
launch { delay(2000); println("任务2完成") }
println("等待任务完成")
}
当 coroutineScope
结束时,所有子任务都会自动完成或取消。
💡 协程的隐藏技能:Flow
如果协程是单任务英雄,那 Flow 就是它的“数据流忍术”。用 Flow,你可以优雅地处理连续的数据流,比如加载分页数据、实时更新状态。
fun fetchDataFlow(): Flow<String> = flow {
for (i in 1..5) {
delay(500)
emit("第 $i 条数据")
}
}
CoroutineScope(Dispatchers.Main).launch {
fetchDataFlow().collect { data ->
println(data) // 每次收到数据时打印
}
}
🧩 总结
协程就像一位超级英雄,它能:
- 解决主线程阻塞的问题。
- 简化复杂的异步操作。
- 提供更高效、更安全的并发管理。
而它的乐趣在于:
- 让开发者从回调地狱中解脱出来。
- 代码更简洁、更易读,就像写同步代码一样。
如果你还没使用协程,试试吧!它会让你感受到开发 Android 应用的魔法力量!
“用协程开发,就像给代码装上了飞行装置,既轻松又高效!”
愿你的 Android 开发之路充满乐趣与协程的超能力! 😊
来源:juejin.cn/post/7444518315559714866
协程:解锁 Android 开发的超级英雄技能!
开发 Android 应用时,是否有过这样的时刻?
"我只是想请求个网络数据,为什么我的主线程就卡住了!"
"多线程真香,但这锁和回调让我头都大了!"
别担心!
今天我们来认识 Android 开发中的超级英雄——协程(Coroutines) ,它能让你以最优雅的方式,化解多线程的痛点,轻松应对复杂的异步操作!
🦸♂️ 协程是个啥?
想象一下,你正在玩一款冒险游戏。游戏里的主角不仅可以“战斗”,还能“暂停”战斗,去完成一些重要任务(比如喝口水、回血),然后再无缝回归到战斗中继续打怪。
协程就像这个灵活的冒险主角:
- 轻量级线程:协程不是普通线程,但它可以暂停和恢复。
- 灵活暂停和恢复:随时挂起(
suspend
),随时回来。 - 并发管理高手:让你在多任务之间游刃有余,再也不用担心线程锁或回调地狱。
🎯 协程的核心武器
协程的核心技能就三个字:
挂起(Suspend)!
挂起函数是协程的魔法技能,虽然它暂停了自己,但不会阻塞线程。
比如这个最简单的例子:
suspend fun fetchData(): String {
delay(1000) // 模拟网络请求,暂停1秒
return "服务器返回的数据"
}
看到没有?协程就像在说:
"我歇一会儿,待会儿继续,不耽误别人干活!"
🤹♀️ 协程的奇幻队伍
协程离不开一支强大的“队友团队”,它们是:
1️⃣ GlobalScope
协程的孤狼模式,一旦启动就运行到世界尽头,但稍有不慎可能造成内存泄漏,慎用!
GlobalScope.launch {
println("这是一个孤单的协程")
}
2️⃣ CoroutineScope
协程的队伍领袖,能保证所有子协程的生命周期跟它挂钩,避免资源泄漏。
class MyActivity : AppCompatActivity() {
private val scope = CoroutineScope(Dispatchers.Main)
override fun onDestroy() {
super.onDestroy()
scope.cancel() // 活动销毁时取消协程
}
}
3️⃣ Dispatchers
调度器,决定协程的运行地点:
- Main:UI线程,适合更新界面。
- IO:专注网络请求、文件读写。
- Default:CPU密集型任务。
- Unconfined:自由漂流,不常用。
🛠️ 协程实战:网络请求案例
假设你是一位披风加身的 Android 开发英雄,任务是从服务器获取一段数据并显示在界面上:
fun fetchDataAndShow() {
CoroutineScope(Dispatchers.Main).launch {
try {
val data = withContext(Dispatchers.IO) {
fetchData() // 网络请求在 IO 线程中执行
}
textView.text = data // 回到主线程更新 UI
} catch (e: Exception) {
textView.text = "出错啦!${e.message}"
}
}
}
suspend fun fetchData(): String {
delay(2000) // 模拟网络请求
return "这是来自服务器的数据"
}
你看,协程让异步操作简洁优雅,完全不需要复杂的回调!
以下是一篇轻松有趣、但同时技术性强的文章,主题是 "协程:解锁 Android 开发的超级英雄技能!" :
协程:解锁 Android 开发的超级英雄技能!
开发 Android 应用时,是否有过这样的时刻?
"我只是想请求个网络数据,为什么我的主线程就卡住了!"
"多线程真香,但这锁和回调让我头都大了!"
别担心!
今天我们来认识 Android 开发中的超级英雄——协程(Coroutines) ,它能让你以最优雅的方式,化解多线程的痛点,轻松应对复杂的异步操作!
🦸♂️ 协程是个啥?
想象一下,你正在玩一款冒险游戏。游戏里的主角不仅可以“战斗”,还能“暂停”战斗,去完成一些重要任务(比如喝口水、回血),然后再无缝回归到战斗中继续打怪。
协程就像这个灵活的冒险主角:
- 轻量级线程:协程不是普通线程,但它可以暂停和恢复。
- 灵活暂停和恢复:随时挂起(
suspend
),随时回来。 - 并发管理高手:让你在多任务之间游刃有余,再也不用担心线程锁或回调地狱。
🎯 协程的核心武器
协程的核心技能就三个字:
挂起(Suspend)!
挂起函数是协程的魔法技能,虽然它暂停了自己,但不会阻塞线程。
比如这个最简单的例子:
suspend fun fetchData(): String {
delay(1000) // 模拟网络请求,暂停1秒
return "服务器返回的数据"
}
看到没有?协程就像在说:
"我歇一会儿,待会儿继续,不耽误别人干活!"
🤹♀️ 协程的奇幻队伍
协程离不开一支强大的“队友团队”,它们是:
1️⃣ GlobalScope
协程的孤狼模式,一旦启动就运行到世界尽头,但稍有不慎可能造成内存泄漏,慎用!
GlobalScope.launch {
println("这是一个孤单的协程")
}
2️⃣ CoroutineScope
协程的队伍领袖,能保证所有子协程的生命周期跟它挂钩,避免资源泄漏。
class MyActivity : AppCompatActivity() {
private val scope = CoroutineScope(Dispatchers.Main)
override fun onDestroy() {
super.onDestroy()
scope.cancel() // 活动销毁时取消协程
}
}
3️⃣ Dispatchers
调度器,决定协程的运行地点:
- Main:UI线程,适合更新界面。
- IO:专注网络请求、文件读写。
- Default:CPU密集型任务。
- Unconfined:自由漂流,不常用。
🛠️ 协程实战:网络请求案例
假设你是一位披风加身的 Android 开发英雄,任务是从服务器获取一段数据并显示在界面上:
fun fetchDataAndShow() {
CoroutineScope(Dispatchers.Main).launch {
try {
val data = withContext(Dispatchers.IO) {
fetchData() // 网络请求在 IO 线程中执行
}
textView.text = data // 回到主线程更新 UI
} catch (e: Exception) {
textView.text = "出错啦!${e.message}"
}
}
}
suspend fun fetchData(): String {
delay(2000) // 模拟网络请求
return "这是来自服务器的数据"
}
你看,协程让异步操作简洁优雅,完全不需要复杂的回调!
🎮 协程的高阶玩法
1️⃣ 并发:一心多用
协程中的并发很简单,像玩双开游戏一样:
suspend fun loadData() = coroutineScope {
val data1 = async { fetchData() }
val data2 = async { fetchData() }
println("数据1: ${data1.await()}, 数据2: ${data2.await()}")
}
只要用了 async
,就能并发运行多个任务,效率提升 N 倍!
2️⃣ 结构化并发:协程的守护者
协程不像传统线程那么“放飞自我”。当它的宿主(比如 CoroutineScope
)取消了,所有子协程也会跟着取消。这种“组队行动”叫做结构化并发。
coroutineScope {
launch { delay(1000); println("任务1完成") }
launch { delay(2000); println("任务2完成") }
println("等待任务完成")
}
当 coroutineScope
结束时,所有子任务都会自动完成或取消。
💡 协程的隐藏技能:Flow
如果协程是单任务英雄,那 Flow 就是它的“数据流忍术”。用 Flow,你可以优雅地处理连续的数据流,比如加载分页数据、实时更新状态。
fun fetchDataFlow(): Flow<String> = flow {
for (i in 1..5) {
delay(500)
emit("第 $i 条数据")
}
}
CoroutineScope(Dispatchers.Main).launch {
fetchDataFlow().collect { data ->
println(data) // 每次收到数据时打印
}
}
🧩 总结
协程就像一位超级英雄,它能:
- 解决主线程阻塞的问题。
- 简化复杂的异步操作。
- 提供更高效、更安全的并发管理。
而它的乐趣在于:
- 让开发者从回调地狱中解脱出来。
- 代码更简洁、更易读,就像写同步代码一样。
如果你还没使用协程,试试吧!它会让你感受到开发 Android 应用的魔法力量!
“用协程开发,就像给代码装上了飞行装置,既轻松又高效!”
愿你的 Android 开发之路充满乐趣与协程的超能力! 😊
来源:juejin.cn/post/7444518315559714866
2024年的安卓现代开发
大家好 👋🏻, 新的一年即将开始, 我不想错过这个机会, 与大家分享一篇题为《2024年的安卓现代开发》的文章. 🚀
如果你错过了我的上一篇文章, 可以看看2023年的安卓现代开发并回顾一下今年的变化.
免责声明
📝 本文反映了我的个人观点和专业见解, 并参考了 Android 开发者社区中的不同观点. 此外, 我还定期查看 Google 为 Android 提供的指南.
🚨 需要强调的是, 虽然我可能没有明确提及某些引人注目的工具, 模式和体系结构, 但这并不影响它们作为开发 Android 应用的宝贵替代方案的潜力.
Kotlin 无处不在 ❤️
Kotlin是由JetBrains开发的一种编程语言. 由谷歌推荐, 谷歌于 2017 年 5 月正式发布(查看这里). 它是一种与 Java 兼容的现代编程语言, 可以在 JVM 上运行, 这使得它在 Android 应用开发中的采用非常迅速.
无论你是不是安卓新手, 都应该把 Kotlin 作为首选, 不要逆流而上 🏊🏻 😎, 谷歌在 2019 年谷歌 I/O 大会上宣布了这一做法. 有了 Kotlin, 你就能使用现代语言的所有功能, 包括协程的强大功能和使用为 Android 生态系统开发的现代库.
请查看Kotlin 官方文档
Kotlin 是一门多用途语言, 我们不仅可以用它来开发 Android 应用, 尽管它的流行很大程度上是由于 Android 应用, 我们可以从下图中看到这一点.
KotlinConf ‘23
Kotlin 2.0 要来了
另一个需要强调的重要事件是Kotlin 2.0
的发布, 它近在眼前. 截至本文发稿之日, 其版本为 2.0.0-beta4
新的 K2 编译器 也是 Kotlin 2.0 的另一个新增功能, 它将带来显著的性能提升, 加速新语言特性的开发, 统一 Kotlin 支持的所有平台, 并为多平台项目提供更好的架构.
请查看 KotlinConf '23 的回顾, 你可以找到更多信息.
Compose 🚀
Jetpack Compose 是 Android 推荐的用于构建本地 UI 的现代工具包. 它简化并加速了 Android 上的 UI 开发. 通过更少的代码, 强大的工具和直观的 Kotlin API, 快速实现你的应用.
Jetpack Compose 是 Android Jetpack 库的一部分, 使用 Kotlin 编程语言轻松创建本地UI. 此外, 它还与 LiveData 和 ViewModel 等其他 Android Jetpack 库集成, 使得构建具有强反应性和可维护性的 Android 应用变得更加容易.
Jetpack Compose 的一些主要功能包括
- 声明式UI
- 可定制的小部件
- 与现有代码(旧视图系统)轻松集成
- 实时预览
- 改进的性能.
资源:
- Jetpack Compose 文档
- Compose 与 Kotlin 的兼容性图谱
- Jetpack Compose 路线图
- 课程
- Jetpack Compose 中
@Composable
的API 指南
Android Jetpack ⚙️
Jetpack是一套帮助开发者遵循最佳实践, 减少模板代码, 编写在不同Android版本和设备上一致运行的代码的库, 这样开发者就可以专注于他们的业务代码.
其中最常用的工具有:
Material You / Material Design 🥰
Material You 是在 Android 12 中引入并在 Material Design 3 中实现的一项新的定制功能, 它使用户能够根据个人喜好定制操作系统的视觉外观. Material Design 是一个由指南, 组件和工具组成的可调整系统, 旨在坚持UI设计的最高标准. 在开源代码的支持下, Material Design 促进了设计师和开发人员之间的无缝协作, 使团队能够高效地创造出令人惊叹的产品.
目前, Material Design 的最新版本是 3, 你可以在这里查看更多信息. 此外, 你还可以利用Material Theme Builder来帮助你定义应用的主题.
代码仓库
SplashScreen API
Android 中的SplashScreen API对于确保应用在 Android 12 及更高版本上正确显示至关重要. 不更新会影响应用的启动体验. 为了在最新版本的操作系统上获得一致的用户体验, 快速采用该 API 至关重要.
Clean架构
Clean架构
的概念是由Robert C. Martin提出的. 它的基础是通过将软件划分为不同层次来实现责任分离.
特点
- 独立于框架.
- 可测试.
- 独立于UI
- 独立于数据库
- 独立于任何外部机构.
依赖规则
作者在他的博文Clean代码中很好地描述了依赖规则.
依赖规则是使这一架构得以运行的首要规则. 这条规则规定, 源代码的依赖关系只能指向内部. 内圈中的任何东西都不能知道外圈中的任何东西. 特别是, 外圈中声明的东西的名称不能被内圈中的代码提及. 这包括函数, 类, 变量或任何其他命名的软件实体.
- 博文Clean代码
安卓中的Clean架构
:
Presentation 层: Activitiy, Composable, Fragment, ViewModel和其他视图组件.
Domain层: 用例, 实体, 仓库接口和其他Domain组件.
Data层: 仓库的实现类, Mapper, DTO 等.
Presentation层的架构模式
架构模式是一种更高层次的策略, 旨在帮助设计软件架构, 其特点是在一个可重复使用的框架内为常见的架构问题提供解决方案. 架构模式与设计模式类似, 但它们的规模更大, 解决的问题也更全面, 如系统的整体结构, 组件之间的关系以及数据管理的方式等.
在Presentation层中, 我们有一些架构模式, 我想重点介绍以下几种:
- MVVM
- MVI
我不想逐一解释, 因为在互联网上你可以找到太多相关信息. 😅
此外, 你还可以查看应用架构指南.
依赖注入
依赖注入是一种软件设计模式, 它允许客户端从外部获取依赖关系, 而不是自己创建依赖关系. 它是一种在对象及其依赖关系之间实现控制反转(IoC)的技术.
模块化
模块化是一种软件设计技术, 可将应用划分为独立的模块, 每个模块都有自己的功能和职责.
模块化的优势
可重用性: 拥有独立的模块, 就可以在应用的不同部分甚至其他应用中重复使用.
严格的可见性控制: 模块可以让你轻松控制向代码库其他部分公开的内容.
自定义交付: Play特性交付 使用应用Bundle的高级功能, 允许你有条件或按需交付应用的某些功能.
可扩展性: 通过独立模块, 可以添加或删除功能, 而不会影响应用的其他部分.
易于维护: 将应用划分为独立的模块, 每个模块都有自己的功能和责任, 这样就更容易理解和维护代码.
易于测试: 有了独立的模块, 就可以对它们进行隔离测试, 从而便于发现和修复错误.
改进架构: 模块化有助于改进应用的架构, 从而更好地组织和结构代码.
改善协作: 通过独立的模块, 开发人员可以不受干扰地同时开发应用的不同部分.
构建时间: 一些 Gradle 功能, 如增量构建, 构建缓存或并行构建, 可以利用模块化提高构建性能.
更多信息请参阅官方文档.
网络
序列化
在本节中, 我想提及两个我认为非常重要的工具: 与 Retrofit 广泛结合使用的Moshi, 以及 JetBrains 的 Kotlin 团队押宝的Kotlin Serialization.
Moshi 和 Kotlin Serialization 是用于 Kotlin/Java 的两个序列化/反序列化库, 可将对象转换为 JSON 或其他序列化格式, 反之亦然. 这两个库都提供了友好的接口, 并针对移动和桌面应用进行了优化. Moshi 主要侧重于 JSON 序列化, 而 Kotlin Serialization 则支持包括 JSON 在内的多种序列化格式.
图像加载
要从互联网上加载图片, 有几个第三方库可以帮助你处理这一过程. 图片加载库为你做了很多繁重的工作; 它们既处理缓存(这样你就不用多次下载图片), 也处理下载图片并将其显示在屏幕上的网络逻辑.
_ 官方安卓文档
响应/线程管理
说到响应式编程和异步进程, Kotlin协程凭借其suspend函数和Flow脱颖而出. 然而, 在 Android 应用开发中, 承认 RxJava 的价值也是至关重要的. 尽管协程和 Flow 的采用率越来越高, 但 RxJava 仍然是多个项目中稳健而受欢迎的选择.
对于新项目, 请始终选择Kotlin协程
❤️. 可以在这里探索一些Kotlin协程相关的概念.
本地存储
在构建移动应用时, 重要的一点是能够在本地持久化数据, 例如一些会话数据或缓存数据等. 根据应用的需求选择合适的存储选项非常重要. 我们可以存储键值等非结构化数据, 也可以存储数据库等结构化数据. 请记住, 这一点并没有提到我们可用的所有本地存储类型(如文件存储), 只是提到了允许我们保存数据的工具.
建议:
- S̶h̶a̶r̶e̶d̶P̶r̶e̶f̶e̶r̶e̶n̶c̶e̶s̶
- DataStore
- EncryptedSharedPreferences
测试 🕵🏼
软件开发中的测试对于确保产品质量至关重要. 它能检测错误, 验证需求并确保客户满意. 下面是一些最常用的测试工具::
截屏测试 📸
Android 中的截屏测试涉及自动捕获应用中各种 UI 元素的屏幕截图, 并将其与基线图像进行比较, 以检测任何意外的视觉变化. 它有助于确保不同版本和配置的应用具有一致的UI外观, 并在开发过程的早期捕捉视觉回归.
R8 优化
R8 是默认的编译器, 可将项目的 Java 字节码转换为可在 Android 平台上运行的 DEX 格式. 通过缩短类名及其属性, 它可以帮助我们混淆和减少应用的代码, 从而消除项目中未使用的代码和资源. 要了解更多信息, 请查看有关 缩减, 混淆和优化应用 的 Android 文档. 此外, 你还可以通过ProGuard
规则文件禁用某些任务或自定义 R8 的行为.
- 代码缩减
- 缩减资源
- 混淆
- 优化
第三方工具
- DexGuard
Play 特性交付
Google Play 的应用服务模式称为动态交付, 它使用 Android 应用Bundle为每个用户的设备配置生成并提供优化的 APK, 因此用户只需下载运行应用所需的代码和资源.
自适应布局
随着具有不同外形尺寸的移动设备使用量的增长, 我们需要一些工具, 使我们的 Android 应用能够适应不同类型的屏幕. 因此, Android 为我们提供了Window Size Class, 简单地说, 就是三大类屏幕格式, 它们是我们开发设计的关键点. 这样, 我们就可以避免考虑许多屏幕设计的复杂性, 从而将可能性减少到三组, 它们是 Compat, Medium和Expanded.
Window Size Class
我们拥有的另一个重要资源是Canonical Layout, 它是预定义的屏幕设计, 可用于 Android 应用中的大多数场景, 还为我们提供了如何将其适用于大屏幕的指南.
其他相关资源
本地化 🌎
本地化包括调整产品以满足不同地区不同受众的需求. 这包括翻译文本, 调整格式和考虑文化因素. 其优势包括进入全球市场, 增强用户体验, 提高客户满意度, 增强在全球市场的竞争力以及遵守当地法规.
注: BCP 47 是安卓系统使用的国际化标准.
参考资料
性能 🔋⚙️
在开发 Android 应用时, 我们必须确保用户体验更好, 这不仅体现在应用的开始阶段, 还体现在整个执行过程中. 因此, 我们必须使用一些工具, 对可能影响应用性能的情况进行预防性分析和持续监控:
应用内更新
当你的用户在他们的设备上不断更新你的应用时, 他们可以尝试新功能, 并从性能改进和错误修复中获益. 虽然有些用户会在设备连接到未计量的连接时启用后台更新, 但其他用户可能需要提醒才能安装更新. 应用内更新是 Google Play 核心库的一项功能, 可提示活跃用户更新你的应用*.
运行 Android 5.0(API 等级 21)或更高版本的设备支持应用内更新功能. 此外, 应用内更新仅支持 Android 移动设备, Android 平板电脑和 Chrome OS 设备.
- 应用内更新文档
应用内评论
Google Play 应用内评论 API 可让你提示用户提交 Play Store 评级和评论, 而无需离开你的应用或游戏.
一般来说, 应用内评论流程可在用户使用应用的整个过程中随时触发. 在流程中, 用户可以使用 1-5 星系统对你的应用进行评分, 并添加可选评论. 一旦提交, 评论将被发送到 Play Store 并最终显示出来.
*为保护用户隐私并避免 API 被滥用, 你的应用应严格遵守有关何时请求应用内评论和评论提示的设计的规定.
- 应用内评论文档
可观察性 👀
在竞争日益激烈的应用生态系统中, 要获得良好的用户体验, 首先要确保应用没有错误. 确保应用无错误的最佳方法之一是在问题出现时立即发现并知道如何着手解决. 使用 Android Vitals 来确定应用中崩溃和响应速度问题最多的区域. 然后, 利用 Firebase Crashlytics 中的自定义崩溃报告获取更多有关根本原因的详细信息, 以便有效地排除故障.
工具
辅助功能
辅助功能是软件设计和构建中的一项重要功能, 除了改善用户体验外, 还能让有辅助功能需求的人使用应用. 这一概念旨在改善的不能包括: 有视力问题, 色盲, 听力问题, 灵敏性问题和认知障碍等.
考虑因素:
- 增加文字的可视性(颜色对比度, 可调整文字大小)
- 使用大而简单的控件
- 描述每个UI元素
更多详情请查看辅助功能 - Android 文档
安全性 🔐
在开发保护设备完整性, 数据安全性和用户信任的应用时, 安全性是我们必须考虑的一个方面, 甚至是最重要的方面.
- 使用凭证管理器登录用户: 凭据管理器 是一个 Jetpack API, 在一个单一的 API 中支持多种登录方法, 如用户名和密码, 密码匙和联合登录解决方案(如谷歌登录), 从而简化了开发人员的集成.
- 加密敏感数据和文件: 使用EncryptedSharedPreferences 和EncryptedFile.
- 应用基于签名的权限: 在你可控制的应用之间共享数据时, 使用基于签名的权限.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapp">
<permission android:name="my_custom_permission_name"
android:protectionLevel="signature" />
- 不要将密钥, 令牌或应用配置所需的敏感数据直接放在项目库中的文件或类中. 请使用
local.properties
. - 实施 SSL Pinning: 使用 SSL Pinning 进一步确保应用与远程服务器之间的通信安全. 这有助于防止中间人攻击, 并确保只与拥有特定 SSL 证书的受信任服务器进行通信.
res/xml/network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">example.com</domain>
<pin-set expiration="2018-01-01">
<pin digest="SHA-256">ReplaceWithYourPin</pin>
<!-- backup pin -->
<pin digest="SHA-256">ReplaceWithYourPin</pin>
</pin-set>
</domain-config>
</network-security-config>
- 实施运行时应用自我保护(RASP): 这是一种在运行时保护应用免受攻击和漏洞的安全技术. RASP 的工作原理是监控应用的行为, 并检测可能预示攻击的可疑活动:
- 代码混淆.
- 根检测.
- 篡改/应用钩子检测.
- 防止逆向工程攻击.
- 反调试技术.
- 虚拟环境检测
- 应用行为的运行时分析.
想了解更多信息, 请查看安卓应用中的运行时应用自我保护技术(RASP). 还有一些 Android 安全指南.
版本目录
Gradle 提供了一种集中管理项目依赖关系的标准方式, 叫做版本目录; 它在 7.0 版中被试验性地引入, 并在 7.4 版中正式发布.
优点:
- 对于每个目录, Gradle 都会生成类型安全的访问器, 这样你就可以在 IDE 中通过自动补全轻松添加依赖关系.
- 每个目录对构建过程中的所有项目都是可见的. 它是声明依赖项版本的中心位置, 并确保该版本的变更适用于每个子项目.
- 目录可以声明 dependency bundles, 即通常一起使用的 "依赖关系组".
- 目录可以将依赖项的组和名称与其实际版本分开, 而使用版本引用, 这样就可以在多个依赖项之间共享一个版本声明.
请查看更多信息
Secret Gradle 插件
Google 强烈建议不要将 API key 输入版本控制系统. 相反, 你应该将其存储在本地的 secrets.properties
文件中, 该文件位于项目的根目录下, 但不在版本控制范围内, 然后使用 Secrets Gradle Plugin for Android 来读取 API 密钥.
日志
日志是一种软件工具, 用于记录程序的执行信息, 重要事件, 错误调试信息以及其他有助于诊断问题或了解程序运行情况的信息. 日志可配置为将信息写入不同位置, 如日志文件, 控制台, 数据库, 或将信息发送到日志记录服务器.
Linter / 静态代码分析器
Linter 是一种编程工具, 用于分析程序源代码, 查找代码中的潜在问题或错误. 这些问题可能是语法问题, 代码风格不当, 缺乏文档, 安全问题等, 它们会对代码的质量和可维护性产生影响.
Google Play Instant
Google Play Instant 使本地应用和游戏无需安装即可在运行 Android 5.0(API 等级 21)或更高版本的设备上启动. 你可以使用 Android Studio 构建这类体验, 称为即时应用和即时游戏. 通过允许用户运行即时应用或即时游戏(即提供即时体验), 你可以提高应用或游戏的发现率, 从而有助于吸引更多活跃用户或安装.
新设计中心
安卓团队提供了一个新的设计中心, 帮助创建美观, 现代的安卓应用, 这是一个集中的地方, 可以了解安卓在多种形态因素方面的设计.
点击查看新的设计中心
人工智能
Gemini
和PalM 2
是谷歌开发的两个最先进的人工智能(AI)模型, 它们将改变安卓应用开发的格局. 这些模型具有一系列优势, 将推动应用的效率, 用户体验和创新.
人工智能编码助手工具
Studio Bot
Studio Bot 是你的 Android 开发编码助手. 它是 Android Studio 中的一种对话体验, 通过回答 Android 开发问题帮助你提高工作效率. 它采用人工智能技术, 能够理解自然语言, 因此你可以用简单的英语提出开发问题. Studio Bot 可以帮助 Android 开发人员生成代码, 查找相关资源, 学习最佳实践并节省时间.
Github Copilot
GitHub Copilot 是一个人工智能配对程序员. 你可以使用 GitHub Copilot 直接在编辑器中获取整行或整个函数的建议.
Amazon CodeWhisperer
这是亚马逊的一项服务, 可根据你当前代码的上下文生成代码建议. 它能帮助你编写更高效, 更安全的代码, 并发现新的 API 和工具.
Kotlin Multiplatform 🖥 📱⌚️ 🥰 🚀
最后, 同样重要的是, Kotlin Multiplatform 🎉 是本年度最引人注目的新产品. 它是跨平台应用开发领域的有力竞争者. 虽然我们的主要重点可能是 Android 应用开发, 但 Kotlin Multiplatform 为我们提供了利用 KMP 框架制作完全本地 Android 应用的灵活性. 这一战略举措不仅为我们的项目提供了未来保障, 还为我们提供了必要的基础架构, 以便在我们选择多平台环境时实现无缝过渡. 🚀
如果你想深入了解 Kotlin Multiplatform, 我想与你分享这几篇文章. 在这些文章中, 探讨了这项技术的现状及其对现代软件开发的影响:
来源:juejin.cn/post/7342861726000791603
如果你想做副业,不妨看看我这3个月的奋斗史
看过我文章的小伙伴应该也都知道,去年我是坚持了每周更新一篇技术文章,去年文章总数算下来也有个50篇左右,然后今年立下的flag是:
- 再学一门语言,比如鸿蒙开发、游戏开发等等。这个flag是做到了,目前为止可以运用kotlin开发Android原生应用。
- 继续在掘金平台输出文章,至少做到每周一更。年初原本想着是每周2-3更,但是目前看来是完全没做到,因为这2个月的个人时间全都扑在了“副业”上。
所以接下来,我会将这3个月的心路历程讲给大家听,如果大家想做副业,可以参考一下我的经历。
过年这段时间的思考
时间回退到去年过年的时候,我没记错的话应该是今年2月份是去年过年的时间点。我司是放了2周的假,那个时候我的工作年限是2年半(可以出道了,哈哈哈)。从大学到目前为止,心里一直有个声音告诉我:
我不可能通过打工来实现理想中的生活状态。原因如下:
1、房贷、车贷、孩子教育、大病、未知的风险,这些因素会导致生活质量非常脆弱。当然这也要因人而异,毕竟过的是否开心只有自己知道。
2、疫情后时代,裁员潮一浪更比一浪强,毕业人数越来越多,岗位越来越少,谋得一份自己满意的工作也趋近于“99%的运气 + 1%的努力”。
3、原来我努力学习技术是为了更好的打工,有人的地方就有江湖,你不能独善其身,因为并不是所有的人都喜欢双赢。
所以,年后在回北京的路上,我决定今年要尝试一下,不要把所有的时间全扑在技术上,可以考虑做一下“副业”。
如何做副业?
我个人比较喜欢看电影、电视剧、动漫、NBA。影视解说这个赛道可能就比较适合我,定了赛道以后,视频发到哪个平台呢?今年短视频平台特别多,抖音、快手、视频号、B站、西瓜、甚至是支付宝里都可以刷视频等等。在综合考虑之后,我选择了微信视频号。原因如下:
- 微信不缺流量,也不缺广告商赞助。
- 视频号绝对是某讯今年发力的重点,因为官方不止一次在公共场合里说明了视频号的重要性,而且春节晚会上也出现了视频号的赞助。
- 视频号上目前竞争不激烈(这句话只在2024年6月前生效),因为用的人还不是很多,所以这个阶段对视频质量的管控还不是很严,毕竟它要吸引用户进来,前期肯定不会管控太严,而且流量也会给的很足。
于是2024年2月18日,我的第一条影视作品在视频号上发布了。
这是我第一次剪辑影视作品,花了3天时间。当时的播放量就是200多,你现在看到710播放量是2月18日 - 6月16日的播放量。那个时候粉丝数量是1,对,没错,就是你们想的那样,那个1就是我自己,哈哈哈。当时剪辑第一条视频的时候,说实话完全是一边学习一边剪辑,而且那个时候我进入了一个误区,就是必须要剪辑的完美,所以前2天一直没有产出,这个时候我慌了,2天一点产出都没有,所以第3天的时候我告诉我自己,先把视频发出去,你又不是只发一条视频,后面的视频慢慢优化,不要想着一口吃个胖子。
前2条视频都是有关三国的视频,那个时候播放量都是200多,但是从第3条视频开始,播放量直接破万,有的能达到10w。为啥会有大的播放差距?后来我复盘了一下,是因为我当时正好踩中热点了,并且竞争不激烈。第三条视频以后,我开始剪辑《南来北往》这部影视剧,当时这部电视剧可以说是非常火,没看过的小伙伴强烈建议你去爱奇艺上观看一遍,真的超级好看。
因为周一到周五上班嘛,所以周六周天我会把下周要发的视频全都剪出来,一天至少按时发一个作品,就这样,差不多2周左右的时间吧,我的有效粉丝突破了100个。
视频号是分等级的,等级跟粉丝有关系,等级越高,视频的基础播放量就越高,能解锁的权益也会越来越多。
有效粉丝数量突破100个,这个阶段是比较难的,原因如下:
- 你要审视自己的作品质量。自己剪出来的东西是否有待提高等等。
- 你要确定细分赛道,影视解说都算是一个大概念,它可以再细分为 “影视解说”、“影视混剪”、“影视情感”。
- 一天分很多个时间段,每个时间段流量不一样,所以你要测出自己的作品在哪个时间段里,播放量比较高。
我原以为影视解说这条道路会这么顺的走下去,结果因为没有版权+播放量太高,被投诉侵权了。不是我吹,如果那个时候你在视频号里搜索“南来北往”,你看到的视频大部分都是我剪出来的。
说实话,我也是第一次见到这种阵仗,吓的我把所有关于“南来北往”的20多部作品连夜下架删除了。
在对比了其他平台后,我得出了如下结论:
- 平台之间是有合作的,这部影视剧在这个平台算侵权,但是在其他平台里就不算侵权。
- 二次创作的质量要高,要不然很容易就会被判违规,所以我已经完全转为纯影视解说。
- 还是要看平台规则,要尊重平台的规则。
影视号在视频号里如何赚钱?
相信这个话题是你们比较喜欢看的,根据我的历程,影视号在视频号里的变现途径有以下几个方向:
- 当你的作品播放量能够稳定突破10w的时候,或者每周的播放总量能够稳定突破50w的时候,会有很多人主动找你合作。
- 视频号里有视频变现任务,你可以主动去接一些变现任务,然后等待任务结束后结算。当然,这个只有当你的有效粉丝数量突破100的时候,你才有资格去接任务。
- 如果你的有效粉丝数量突破1000的时候,你可以挂商品链接,用户从你的链接点进去后,如果发生了交易,你会在中间赚一些分成,这个跟抖音的规则差不多。
- 如果你的有效粉丝突破了5000的时候,你的账号就可以解锁商单功能。就是将自己的报价放出去,如果广告主找你合作,那么他就要遵循你的视频报价。一条1分钟以下的视频报价是多少,一分钟以上的视频报价是多少等等。
当然我说的这几个方向,都必须要遵循平台的规则。所以前2年,在抖音上,80%的探店账号都赚到了钱,就是因为前几年,探店视频在抖音上是趋势,如果前几年你在抖音上发探店视频,能够做到基础的每日一更,你的流量一定不会差,而且你也一定能够接到商单。
最后
又到了该和大家说再见的时候啦,这3个月确实是学到了很多东西,这3个月的奋斗史也绝对不仅仅是视频号,在其他方面上我也有很多很深入的尝试。
以上内容绝对没有任何的引导,只是自己上半年的一个分享,如果你还想看更多的,有关我在其他方面的尝试,欢迎评论区里发言,嘿嘿,我们下期再见,拜拜~~
来源:juejin.cn/post/7380510171640446988
字节2面:为了性能,你会违反数据库三范式吗?
大家好,我是猿java。
数据库的三大范式,它是数据库设计中最基本的三个规范,那么,三大范式是什么?在实际开发中,我们一定要严格遵守三大范式吗?这篇文章,我们一起来聊一聊。
1. 三大范式
1. 第一范式(1NF,确保每列保持原子性)
第一范式要求数据库中的每个表格的每个字段(列)都具有原子性,即字段中的值不可再分割。换句话说,每个字段只能存储一个单一的值,不能包含集合、数组或重复的组。
如下示例: 假设有一个学生表 Student
,结构如下:
学生ID | 姓名 | 电话号码 |
---|---|---|
1 | 张三 | 123456789, 987654321 |
2 | 李四 | 555555555 |
在这个表中,电话号码
字段包含多个号码,违反了1NF的原子性要求。为了满足1NF,需要将电话号码拆分为单独的记录或创建一个新的表。
满足 1NF后的设计:
学生表 Student
学生ID | 姓名 |
---|---|
1 | 张三 |
2 | 李四 |
电话表 Phone
电话ID | 学生ID | 电话号码 |
---|---|---|
1 | 1 | 123456789 |
2 | 1 | 987654321 |
3 | 2 | 555555555 |
1.2 第二范式(2NF,确保表中的每列都和主键相关)
第二范式要求满足第一范式,并且消除表中的部分依赖,即非主键字段必须完全依赖于主键,而不是仅依赖于主键的一部分。这主要适用于复合主键的情况。
如下示例:假设有一个订单详情表 OrderDetail
,结构如下:
订单ID | 商品ID | 商品名称 | 数量 | 单价 |
---|---|---|---|---|
1001 | A01 | 苹果 | 10 | 2.5 |
1001 | A02 | 橙子 | 5 | 3.0 |
1002 | A01 | 苹果 | 7 | 2.5 |
在上述表中,主键是复合主键 (订单ID, 商品ID)
。商品名称
和单价
只依赖于复合主键中的商品ID
,而不是整个主键,存在部分依赖,违反了2NF。
满足 2NF后的设计:
订单详情表 OrderDetail
订单ID | 商品ID | 数量 |
---|---|---|
1001 | A01 | 10 |
1001 | A02 | 5 |
1002 | A01 | 7 |
商品表 Product
商品ID | 商品名称 | 单价 |
---|---|---|
A01 | 苹果 | 2.5 |
A02 | 橙子 | 3.0 |
1.3 第三范式(3NF,确保每列都和主键列直接相关,而不是间接相关)
第三范式要求满足第二范式,并且消除表中的传递依赖,即非主键字段不应依赖于其他非主键字段。换句话说,所有非主键字段必须直接依赖于主键,而不是通过其他非主键字段间接依赖。
如下示例:假设有一个员工表 Employee
,结构如下:
员工ID | 员工姓名 | 部门ID | 部门名称 |
---|---|---|---|
E01 | 王五 | D01 | 销售部 |
E02 | 赵六 | D02 | 技术部 |
E03 | 孙七 | D01 | 销售部 |
在这个表中,部门名称
依赖于部门ID
,而部门ID
依赖于主键员工ID
,形成了传递依赖,违反了3NF。
满足3NF后的设计:
员工表 Employee
员工ID | 员工姓名 | 部门ID |
---|---|---|
E01 | 王五 | D01 |
E02 | 赵六 | D02 |
E03 | 孙七 | D01 |
部门表 Department
部门ID | 部门名称 |
---|---|
D01 | 销售部 |
D02 | 技术部 |
通过将部门信息移到单独的表中,消除了传递依赖,使得数据库结构符合第三范式。
最后,我们总结一下数据库设计的三大范式:
- 第一范式(1NF): 确保每个字段的值都是原子性的,不可再分。
- 第二范式(2NF): 在满足 1NF的基础上,消除部分依赖,确保非主键字段完全依赖于主键。
- 第三范式(3NF): 在满足 2NF的基础上,消除传递依赖,确保非主键字段直接依赖于主键。
2. 破坏三范式
在实际工作中,尽管遵循数据库的三大范式(1NF、2NF、3NF)有助于提高数据的一致性和减少冗余,但在某些情况下,为了满足性能、简化设计或特定业务需求,我们可能需要违反这些范式。
下面列举了一些常见的破坏三范式的原因及对应的示例。
2.1 性能优化
在高并发、大数据量的应用场景中,严格遵循三范式可能导致频繁的联表查询,增加查询时间和系统负载。为了提高查询性能,设计者可能会通过冗余数据来减少联表操作。
假设有一个电商系统,包含订单表 Orders
和用户表 Users
。在严格 3NF设计中,订单表只存储 用户ID
,需要通过联表查询获取用户的详细信息。
但是,为了查询性能,我们通常会在订单表中冗余存储 用户姓名
和 用户地址
等信息,因此,查询订单信息时无需联表查询 Users
表,从而提升查询速度。
破坏 3NF后的设计:
订单ID | 用户ID | 用户姓名 | 用户地址 | 订单日期 | 总金额 |
---|---|---|---|---|---|
1001 | U01 | 张三 | 北京市 | 2023-10-01 | 500元 |
1002 | U02 | 李四 | 上海市 | 2023-10-02 | 300元 |
2.2 简化查询和开发
严格规范化可能导致数据库结构过于复杂,增加开发和维护的难度,为了简化查询逻辑和减少开发复杂度,我们也可能会选择适当的冗余。
比如,在内容管理系统(CMS)中,文章表 Articles
和分类表 Categories
通常是独立的,如果频繁需要显示文章所属的分类名称,联表查询可能增加复杂性。因此,通过在 Articles
表中直接存储 分类名称
,可以简化前端展示逻辑,减少开发工作量。
破坏 3NF后的设计:
文章ID | 标题 | 内容 | 分类ID | 分类名称 |
---|---|---|---|---|
A01 | 文章一 | … | C01 | 技术 |
A02 | 文章二 | … | C02 | 生活 |
2.3 报表和数据仓库
在数据仓库和报表系统中,通常需要快速读取和聚合大量数据。为了优化查询性能和数据分析,可能会采用冗余的数据结构,甚至使用星型或雪花型模式,这些模式并不完全符合三范式。
在销售数据仓库中,为了快速生成销售报表,可能会创建一个包含维度信息的事实表。
破坏 3NF后的设计:
销售ID | 产品ID | 产品名称 | 类别 | 销售数量 | 销售金额 | 销售日期 |
---|---|---|---|---|---|---|
S01 | P01 | 手机 | 电子 | 100 | 50000元 | 2023-10-01 |
S02 | P02 | 书籍 | 教育 | 200 | 20000元 | 2023-10-02 |
在事实表中直接存储 产品名称
和 类别
,避免了需要联表查询维度表,提高了报表生成的效率。
2.4 特殊业务需求
在某些业务场景下,可能需要快速响应特定的查询或操作,这时通过适当的冗余设计可以满足业务需求。
比如,在实时交易系统中,为了快速计算用户的账户余额,可能会在用户表中直接存储当前余额,而不是每次交易时都计算。
破坏 3NF后的设计:
用户ID | 用户名 | 当前余额 |
---|---|---|
U01 | 王五 | 10000元 |
U02 | 赵六 | 5000元 |
在交易记录表中存储每笔交易的增减,但直接在用户表中维护 当前余额
,避免了每次查询时的复杂计算。
2.5 兼顾读写性能
在某些应用中,读操作远多于写操作。为了优化读性能,可能会通过数据冗余来提升查询速度,而接受在数据写入时需要额外的维护工作。
社交媒体平台中,用户的好友数常被展示在用户主页上。如果每次请求都计算好友数量,效率低下。可以在用户表中维护一个 好友数
字段。
破坏3NF后的设计:
用户ID | 用户名 | 好友数 |
---|---|---|
U01 | Alice | 150 |
U02 | Bob | 200 |
通过在 Users
表中冗余存储 好友数
,可以快速展示,无需实时计算。
2.6 快速迭代和灵活性
在快速发展的产品或初创企业中,数据库设计可能需要频繁调整。过度规范化可能导致设计不够灵活,影响迭代速度。适当的冗余设计可以提高开发的灵活性和速度。
一个初创电商平台在初期快速上线,数据库设计时为了简化开发,可能会将用户的收货地址直接存储在订单表中,而不是单独创建地址表。
破坏3NF后的设计:
订单ID | 用户ID | 用户名 | 收货地址 | 订单日期 | 总金额 |
---|---|---|---|---|---|
O1001 | U01 | 李雷 | 北京市海淀区… | 2023-10-01 | 800元 |
O1002 | U02 | 韩梅梅 | 上海市浦东新区… | 2023-10-02 | 1200元 |
这样设计可以快速上线,后续根据需求再进行规范化和优化。
2.7 降低复杂性和提高可理解性
有时,过度规范化可能使数据库结构变得复杂,难以理解和维护。适度的冗余可以降低设计的复杂性,提高团队对数据库结构的理解和沟通效率。
在一个学校管理系统中,如果将学生的班级信息独立为多个表,可能增加理解难度。为了简化设计,可以在学生表中直接存储班级名称。
破坏3NF后的设计:
学生ID | 姓名 | 班级ID | 班级名称 | 班主任 |
---|---|---|---|---|
S01 | 张三 | C01 | 三年级一班 | 李老师 |
S02 | 李四 | C02 | 三年级二班 | 王老师 |
通过在学生表中直接存储 班级名称
和 班主任
,减少了表的数量,简化了设计。
3. 总结
本文,我们分析了数据库的三范式以及对应的示例,它是数据库设计的基本规范。但是,在实际工作中,为了满足性能、简化设计、快速迭代或特定业务需求,我们很多时候并不会严格地遵守三范式。
所以说,架构很多时候都是业务需求、数据一致性、系统性能、开发效率等各种因素权衡的结果,我们需要根据具体应用场景做出合理的设计选择。
4. 学习交流
如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。
来源:juejin.cn/post/7455635421529145359
谈谈在大环境低迷下,找工作和入职三个月后的感受
前言
今天是新公司入职后的三个多月了个人也是如愿的转正了,对于我个人而言此时的心情好像没有三个月前刚拿到offer那样的喜悦和兴奋了,更像一件很普通的事情在你身边发生了吧。从2023年底离职在到2024年初开始找工作中间休息了三个月,找工作到入职花了一个多月,在这个过程中也是第一次真真切切感受到了所谓大环境低迷下的“前端已死的言论”,也给大家分享一下自己入职三个月的个人感受吧。
从上一家公司离职时的个人感受
因为上一家公司的工作性质是人力外包驻场开发,年底客户公司(中国移动成都产业研究院)我所在的项目组不需要外包人员了,个人也是被迫拿了赔偿灰溜溜的走人了。
工作感受:对于这段工作经历我个人还是比较认可的,毕竟这里没有任何工作压力,也不加班,工作量少,有很多时间去学习新东西,做做自己的开源,认识了新的朋友等等。
学历的重要性:在这里面随便拎一个人出来可能就是研究生学历的国企单位,自己真实的意识到了学历的重要性(第一学历小专科的我瑟瑟发抖)。
和优秀的人共事:如果在一个长期压抑低沉消极的环境下工作无论你的性格在怎么积极乐观开朗,可能也很容易被影响到。相反如果是和在一群积极,乐观,开朗,充满自信的环境和人一起工作,相信你也会变得积极,乐观,自信这或许也是我这一段工作经历最大的收获吧。
2023年底找工作的市场就业环境
抱着试一试的心态在boss上更新了自己的简历状态,不出所料软件上面安静的奇怪ps:49入国军的感觉,已读未回可能是很失望的感觉吧,但年底找工作令人绝望的是大多数公司都是未读未回,这也就意味着年底基本上是没有正常公司招聘的了。
大概投了两周简历后终于在智联招聘上约到了一个短期三个月岗位的面试,现场两轮面试通过了,不过最终还是没有选择去。
原因有很多:
- 现场的工作环境个人感觉很压抑,从接待我前台和面试官都能感觉满脸写着疲惫
- 说公司最近在996,你也需要和我们一起
- 招聘岗位和工作内容是threejs开发,薪资却说只能给到普通前端开发的水平
- 人力外包公司hr的反复无常令我恶心,二面通过后hr给我打电话最主要就是聊薪资吧,电话内容也很简单hr:成都大部分前端的薪资是在XX-XX,可能给不到你想要的薪资,可能要往下压个1-2K。我:我提的薪资完全是在你们发布招聘岗位薪资的区间,既然你们给不到为什么要这样写了(有感到被侮辱了)。过了几天之后人力外包的hr又给我电话,说可以在原来提的薪资基础上加1.4K,希望能早点去客户公司入职。
总结:年底招聘的公司基本上没啥好鸟,如果你的经济能力还行的话让自己放松休息一段时间也是不错的选择
2024年初找工作:真实的感受到了大环境的低迷下的市场行情
印象最深刻的是在疫情时期的2021年,那会儿出来找工作boos上会有很多HR主动给你打招呼,一周大概能五六个面试,大专学历也有机会去自研公司
解封之后本以为市场行情会变得回缓,结果大概就是今年可是未来十年行情最好的一年
简单总结一下2024年的成都就业环境大概这样的:
- 只有外包公司会招专科学历
- boss上只给hr发一句打招呼的快捷语,99% 都是已读不回
- 大多数要完简历之后就没有后续了
- 待遇好的公司对于学历的要求更严格了(211,985)
- 给你主动打招呼的基本上都是人力外包公司
截至入职新公司前boss上面的投递状况:沟通了1384个岗位,投递了99份简历,一共约到了 8 家公司的面试
今年找工作的个人感受:不怕面试,就怕没有面试机会
首先说一下个人的一些情况吧,因为在创业小公司待过在技术栈方面个人认为算是比较全面的了
项目经验:做过管理系统(CRM,B2C,ERP,saas等管理系统)、商城和门户网站(响应式,自适应)、移动端(H5,小程序,app)、低代码和可视化(工作流,可视化大屏,3d编辑器)、第三方开发(腾讯IM,企业微信侧边栏)、微前端
项目经历:从0-1搭建过整个大项目的基建工作,封装过项目中很多功能性组件和UI组件二次封装(提高开发效率),接手过屎山代码并重构优化,约定项目的开发规范,处理很多比较棘手的疑难bug和提供相关的技术方案,没有需求概念下的敏捷开发,从0-1的技术调研等
代码方面:写过几个开源项目虽然star数量不多(目前最多一个项目是600+),但在代码规范和可读性方面个人认为还是比较OK的(至少不会写出屎山代码吧)
工作经验(4年):2020毕业至今一直从事前端开发工作
学历:自考本科学历(貌似没啥卵用)
学历确实是我很硬伤的一个点但是没办法,人嘛总归要为年轻时的无知买单吧
在这样的背景下开启了24年的找工作,从2月26号开始投递简历到4月1号拿到offer差不多一个多月左右时间,一共约到了8加公司的面试,平均一周两家公司
大概统计了一下这些公司的面试情况:
公司A:
- 数组哪些方法会触发Vue监听,哪些不会触发监听
- position 有哪些属性
- vue watch和computed的区别,computed和method的区别
- vue的watch是否可以取消? 怎么取消?
- position:absolute, position:fixed那些会脱离文档流
- 如何获取到 pomise 多个then 之后的值
- 常见的http状态码
- 谈谈你对display:flex 弹性盒子属性的了解
- 如何判断一个值是否是数组
- typeof 和instanceof的区别
- es6-es10新增了那些东西
- 离职原因,期望薪资,职业规划
公司B
到现场写了一套笔试题,内容记不清楚了
公司C
- vue router 和route 区别
- 说说重绘和重排
- css 权重
- 项目第一次加载太慢优化
- 谈谈你对vue这种框架理解
- sessionstorage cookie localstorage 区别
- 了解过.css 的优化吗?
- 闭包
- 内存泄漏的产生
- 做一个防重复点击你有哪些方案
- 解释一些防抖和节流以及如何实现
- 说一下你对 webScoket的了解,以及有哪些API
- 说一下你对pomise的理解
- vue2,vue3 中 v-for 和v-if的优先级
- 说说你对canvas的理解
公司D
笔试+面试
- vue 首屏加载过慢如何优化
- 说说你在项目中封装的组件,以及如何封装的
- 后台管理系统权限功能菜单和按钮权限如何实现的
- vue 中的一些项目优化
- 期望薪资,离职原因,
- 其他的记不清楚了
公司E
笔试+面试+和老板谈薪资
1.笔试:八股文
2.面试:主要聊的是项目内容比如项目的一些功能点的实现,和项目的技术点
3.老板谈薪资:首先就是非技术面的常规三件套(离职原因,期望薪资,职业规划),然后就是谈薪资(最终因为薪资给的太低了没有选择考虑这家)
公司F
也是最想去的一家公司,一个偏管理的前端岗位(和面试官聊的非常投缘,而且整个一面过程也非常愉快感受到了十分被尊重)
可惜的是复试的时候因为学历原因,以及一些职业规划和加班出差等方面上没有达到公司的预期也是很遗憾的错过了
一面:
- vue 响应式数据原理
- 说说es6 promise async await 以及 promise A+规范的了解
- 谈谈es6 Map 函数
- 如何实现 list 数据结构转 tree结构
- webScoke api 介绍
- webScoke 在vue项目中如何全局挂载
- vuex 和 pinia 区别
- 谈谈你对微任务和宏任务的了解
- call apply bind 区别
- 前端本地数据存储方式有哪些
- 数组方法 reduce 的使用场景
- 说说你对 css3 display:flex 弹性盒模型 的理解
- vue template 中 {{}} 为什么能够被执行
- threejs 加载大模型有没有什么优化方案
- 离职原因,住的地方离公司有多远,期望薪资
- 你有什么想需要了解的,这个岗位平时的工作内容
二面:
1.我看写过一个Express+Mongoose服务端接口的开源项目,说说你在写后端项目时遇到过的难点
2.介绍一下你写的threejs 3d模型可视化编辑器 这个项目
3.以你的观点说一下你对three.js的了解,以及three.js在前端开发中发挥的作用
4.现在的AI工具都很流行,你有没有使用过AI工具来提高你对开发效率
5.说说你认为AI工具对你工作最有帮助的地方是哪些
6.以你的观点谈谈你对AI的看法,以及AI未来发展的趋势
7.你能接受出差时间是多久
8.你是从去年离职的到今天这这几个月时间,你是去了其他公司只是没有写在简历上吗?
9.说说你的职业规划,离职原因,你的优点和缺点,平时的学习方式
公司G
一共两轮面试,也是最终拿到正式offer入职的公司
一面:
- 主要就是聊了一下简历上写的项目
- 项目的技术难点
- 项目从0-1搭建的过程
- 项目组件封装的过程
- vue2 和 vue3 区别
- vue响应式数据原理
- 对于typescript的熟练程度
- 会react吗? 有考虑学习react吗?
- 说一下你这个three.js3d模型可视化编辑器项目的一个实现思路,为什么会想写这样一个项目
二面:
- 说说了解的es6-es10的东西有哪些
- 说说你对微任务和宏任务的了解
- 什么是原型链
- 什么是闭包,闭包产生的方式有哪些
- vue3 生命周期变化
- vue3 响应式数据原理
- ref 和 reactive 你觉得在项目中使用那个更合适
- 前端跨越方式有哪些
- 经常用的搜索工具有哪些?
- 谷歌搜索在国内能使用吗?你一般用的翻墙工具是哪种?
- 用过ChatGPT工具吗? 有付费使用过吗?
- 你是如何看待面试造航母工作拧螺丝螺丝的?
- 谈谈你对加班的看法?
- 你不能接受的加班方式是什么?
- 为什么会选择自考本科?
- 你平时的学习方式是什么?
- 一般翻墙去外网都会干什么?,外网学习对你的帮助大吗?
- 上一家公司的离职原因是什么,期望薪资是多少, 说说你的职业规划
- 手里有几个offer?
hr电话:
- 大概说了一下面试结果通过了
- 然后就是介绍了一下公司的待遇和薪资情况?
- 问了一下上一家公司的离职原因以及上一家公司的规模情况?
- 手里有几个offer?
- 多久能入职?
因为后面没有别的面试了,再加上离职到在到找工作拿到offer已经有四个月时间没有上班了,最终选择了入职这家公司
入职第三天:我想跑路了!
入职后的第一天,先是装了一下本地电脑环境然后拉了一下项目代码熟悉一下,vue3,react,uniapp 项目都有
崩溃的开始:PC端是一个saas 系统由五个前端项目构成,用的是react umi 的微前端项目来搭建的,也是第一次去接触微前端这种技术栈,要命的是这些项目没有一个是写了readme文档的,项目如何启动以及node.js版本这些只能自己去package.json 文件去查看,在经过一番折腾后终于是把这几个项目给成功跑起来了,当天晚上回家也是专门了解了一下微前端
开始上强度: 入职的第二天被安排做了一个小需求,功能很简单就是改个小功能加一下字段,但是涉及的项目很多,pc端两个项目,小程序两个项目。在改完PC端之后,开始启动小程序项目不出所料又是一堆报错,最终在别的前端同事帮助下终于把小程序项目给启动成功了。
人和代码有一个能跑就行:入职的第三天也从别的同事那里了解到了,之前sass项目组被前端大规模裁员过,原因嘛懂得都懂? 能写出这样一堆屎山代码的人,能不被裁吗?
第一次知道 vue 还可以这样写
对于一个有代码强迫症的人来说,在以后的很长一段时间里要求优化和接触完全是一堆屎山一样代码,真的是很难接受的
入职一个月:赚钱嘛不寒掺
在有了想跑路的想法过后,也开始利用上班的空余时间又去投递简历了,不过现实就是在金三银四的招聘季,boss上面依旧是安静的可怕,在退一步想可能其他公司的情况也和这边差不多,于是最终还是选择接受了现实,毕竟赚钱嘛不寒掺
入职两个月:做完一个项目迭代过后,感觉好多了
在入职的前一个月里,基本上每天都要加班,原因也很简单:
1.全是屎山的项目想要做扩展新功能是非常困难的
2.整个项目的逻辑还是很多很复杂的只能边写项目边熟悉
3.因为裁了很多前端,新人还没招到,但是业务量没有减少只能加班消化
功能上线的晚上,加班到凌晨3点
在开发完一个项目迭代过后也对项目有了一些大概的了解,之后的一些开发工作也变得简单了许多
入职三个月:工作氛围还是很重要滴
在入职三个月后,前端组团队的成员也基本上是组建完成了,一共14人,saas项目组有四个前端,虽然业务量依然很多但是好在有更多的人一起分担了,每周的加班时间也渐渐变少了
在一次偶然间了解到我平时也喜欢打篮球后,我和公司后端组,产品组的同事之间也开始变得有话题了,因为大家也喜欢打球,后来还拉了一个篮球群周末有时间大家也会约出来一起打打球
当你有存在价值后一切的人情世故和人际关系都会变得简单起来
在这个世界上大多数时候除了你的父母等直系亲属和另一半,可能会对你无条件的付出
其余任何人对你尊重和示好,可能都会存在等价的利益交换吧
尤其是在技术研发的岗位,只有当你能够完全胜任这份工作时,并且能够体现出足够的价值时才能够有足够的话语权
入职三个月后的感受
- 公司待遇:虽然是一个集团下面的子公司 (200+人)但待遇只能说一般吧,除了工资是我期望的薪资范围,其他的福利待遇都只能是很一般(私企嘛,懂得都懂)
- 工作强度: 听到过很多从大厂来的新同事抱怨说这边的工作量会很大,对我来说其实都还ok,毕竟之前在极端的高压环境下工作过
- 工作氛围:从我的角度来讲的话,还是很不错的,相处起来也很轻松简单,大家也有很多共同话题,没有之前在小公司上班那么累
大环境低迷下,随时做好被裁掉的准备
从2020年毕业工作以来,最长的一段工作经历是1年4个月,有过三家公司的经历
裁员原因也很简单:创业小公司和人力外包,要么就是小公司经营问题公司直接垮掉,或者就是人力外包公司卸磨杀驴
除非你是在国企单位上班,否则需要随时做好被裁掉的准备
什么都不怕,就怕太安逸了
这句话出自《我的团长我的团》电视剧里面龙文章故意对几十个过江的日本人围而不歼时和虞啸卿的对话,龙文章想通过这几十个日本人将禅达搅得鸡犬不宁,来唤醒还在沉睡在自己温柔乡的我们,因为就在我们放松警惕时日本人就已经将枪口和大炮对准了我们。
或许大家会很不认同这句话吧,如果你的父母给你攒下了足够的资本完全可以把我刚才的话当做放屁,毕竟没有哪一个男生毕业之前的梦想是车子和房子,从事自己喜欢的工作不好吗? 但现实却是你喜欢工作的收入很难让你在这座城市里体面的生活
于我而言前端行业的热爱更多是因为能够给我带来不错的收入所以我选择了热爱它吧,所以保持终身学习的状态也是我需要去做的吧
前端已死?
前端彻底死掉肯定是不会的,在前后端分离模式下的软件开发前端肯定是必不可少的一个岗位,只不过就业环境恶劣下的情况里肯定会淘汰掉很多人,不过35岁之后我还是否能够从事前端行业真的是一个未知数
结语
选择卷或者躺平,只是两种不同的生活态度没有对与错,偶尔躺累了起来卷一下也是可以的,偶尔卷累了躺起了休息一下也是不错的。
在这个网络上到处是人均年收入百万以及各种高质量生活的时代,保持独立思考,如何让自己不被负面情绪所影响才是最重要的吧
来源:juejin.cn/post/7391065678546157577
老婆问我,看这么多书有什么用?我想用这四个字解释明白
前言
Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。
关于洞察事物的本质,之前用了两篇文章,分别从副业、程序员的天花板两个角度,解释了关于变量、因果链、增强回路、调节回路这几个概念。
最后的一个概念,是滞后效应,因为自己没有太好的案例来解释,所以一直没有完成这个概念的解释。
但今天,我想从读书的角度,来解释滞后效应,对你的影响。
看书有什么用呢
我最近开始疯狂看书,或者说,愿意去读书了,不需要人催。只要不陪孩子的时候,我便会放下手头上的事情去看书,有一种想把过去十几年欠缺的时间补回来的冲动。
前天和老婆聊天,我很骄傲的说:我说我最近一个月已经看完了三本书了,厉害吧。
我老婆天真的问了一句:读这么多书有什么用啊?
有什么用呢?我突然回忆起了上学时光,父母、老师不止一次的告诫我们,要多看书,而且也有很多名人名言告诉我们,要去多读书。
查理·芒格说:我这辈子遇到的来自各行各业的聪明人,没有一个不每天阅读的——没有,一个都没有。
钱锺书先生说过:“如果不读书,行万里路,也只是个邮差。”
过去十几年,我也经常在内心里发出这个疑问,有什么用呢?即使在这这样的耳濡目染下,从上学到工作,我也几乎很少去看书,打游戏、看电影是我的休闲方式,看书也只会选择像《白夜行》这种小说。
当自己被问到这个问题的时候,我试图找到看书的收益,嗯,上周写的文章,发布那天,给我带来了1毛钱的收益,似乎好久没入池了。
入池就是微信官方给推荐流量,能够让更多没关注我的朋友,也能看到我的文章。(虽然入推荐池也没多少钱)
肚子里有不少话想说,但有感觉几句闲聊又说不明白,于是我笑了笑,结束了这个话题。
虽然如此,但心里只有两个字:有用。
是什么转变了我的看法,从抖音、英雄联盟中抽离出来,愿意花时间去看书呢?就是文章开头所讲的,滞后效应。
滞后效应
什么是滞后效应
系统最后一个连接关系,是滞后效应。
先从大家最感同身受的一个例子开始,新冠疫情为什么影响这么大?因为新冠病毒有潜伏期,这个潜伏期,可能长达14天。
因为这14天,导致病毒的溯源变得极为困难,我们也被迫在特殊情况下,居家隔离14天。
你给自己定下了接下来一个月的业绩目标,目标设高了,没完成心情低落。目标设低了,太简单了,自己容易放松懈怠。
于是你只能高点、低点来回调整,尝试好几个月。
比如你在洗澡,打开开关的时候,水一会凉,一会烫,你需要左右调整一会才能调整到舒适的温度。
为什么会这样?因为定完目标到出结果可能要几周,打开开关到出热水,需要几秒钟,因和果之间,有一个时间差,这个时间差,就是“滞后效应”。
看书也是一样,从看书,到你真正的能够运用书中的知识,或者说因为书的内容,而做出了什么正确的决策,最后又拿到了什么结果。中间可能会有漫长的时间。
让我们一度会自我怀疑,读书到底有什么用?
所以我们去看很多成功的公众人物,虽然他们在成长过程中阅读过很多书,你很难解释,今天的他,是因为读过哪些书、经历过哪些事,才造就的他今天的成功。
甚至可能他自己,也不知道具体哪本书,是他成功路上最重要的那一本。
我们了解了“滞后效应”,如何让他更好的应用于读书这件事呢?有三点和大家分享。
原因不一定在结果附近
在字节的时候,我挺佩服我的导师和leader,在很多时候,都能带领大家拿到不错的业绩,也给予了我很多的帮助。
他技术能力强,你不会的问题,找到他都能够解决。
管理能力也不错,团队有着很强的战斗力,关键还特别勤奋,有一次我写完技术规划已经半夜2点了,我的leader在凌晨2点和我视频整体过了一遍,并给了改进意见。
我恍然大悟,领导能够如此优秀,是因为够拼。于是我也开始加班,别的学不会,先从勤奋开始学起。
因为存在滞后效应,原因不一定是在结果附近,领导加班只是因为他承担了更大的责任。
想学习他们,学习拼、加班是不够的,应该看看五年前,他们做对了什么,又或者看了什么书,才让他们的技术、管理看起来这么的游刃有余。
减少滞后,增加确定性
刚毕业开始工作时,我买了一本书,叫做《机器学习》,这本书很有名,因为封面的西瓜,很多人称它为“西瓜书”。
但是我做的是后端岗位,即使我把这本书通读了一遍,我也不会有任何的应用,直到现在,我也没实际应用过书里的内容。
这就是读书的滞后性,从我学习到真正的应用与收益,我不知道有多长的时间,或许十年后,不得不学习AI的知识时,我才能想起这本书的内容。
怎么办?学以致用,读那些你能用到的书。
你是一个初、中级后端程序员,就去阅读像《Spring Boot实战》这样的书,去一行行敲下代码,快速的应用到工作当中。
你是一个高级程序员,就去阅读像《深入理解Java虚拟机》,结合生产环境暴露出来的问题,具体分析背后的原因。
你已经变成一个团队管理者,就去阅读管理、沟通类的书籍,解决面临的团队问题。
立刻学习,立刻应用,循环往复。
缩短中间环节
在职场初期,我的技术学习渠道,都是靠百度。百度可以搜出大量的技术文章,来源于掘金、csdn、简书等。
但是,文章的内容参差不齐,你能够学到的只是博主能够掌握的,而内容本身,也可能有遗漏、甚至有错误。
所以我常常因为一篇文章,觉着学会了这个知识点,但后面再看到同一个知识点的其他文章,又会发现这里是不是不对,那里的概念我怎么从来没有听说过?
那怎么办?缩短学习的环节。
从碎片化的网络上学习,改为系统的学习,买业内最权威的专家写的书籍去看,去看官网给出的入门手册,甚至去看源代码。
最早学习JVM虚拟机,我阅读了大量的网上的文章,有讲垃圾回收算法的,讲CMS的,讲G1的,我想到什么就去看什么,面试问到了什么,就去看什么。
后来知道了一本书,就是《深入理解Java虚拟机》,我发现无论是网上的免费文章,还是极客时间的付费课程,这本书几乎涵盖了大部分的内容,而且内容丰富,逻辑性强,更适合新人的学习。
从学习别人理解的知识,到学习最权威的知识,减少中间发生的信息损耗,极大的提升自己的学习效率。
说在最后
好了,文章到这里就要结束了,感谢你能看到最后。
希望这篇文章,可以在读书这件事上,给你一点点帮助。
关于洞察力几个概念的历史文章在这里,欢迎查看。
不知道你最近有没有读到什么让你爱不释手的书呢?欢迎你在评论区和我分享,也希望你点赞、评论、收藏,这对我来说真的很重要。也欢迎你加我的wx:Ldhrlhy10,一起交流~
本篇文章是第47篇原创文章,2024目标进度47/100,欢迎围观。
来源:juejin.cn/post/7399606877543415847
俊劫的2024年终总结:当爹、卖主机、差点失业
笔者
- 96生农村,河南信阳
- 被裁2次,工作5年多,目前在杭州一家车企
- 摸鱼群 / 找工作群,加我wx:V798595965
一、布丁的出生
2024-9-19,我儿子布丁顺顺利利来到这个世界。
1.1 出生那一刻
一开始觉得没什么,但是当微信收到老婆发的信息,那一瞬间感动的热泪盈眶😭。生完还需要在产房观察2小时,我在外面等的这段时间,描述不清楚是什么感受。当时找了个角落,给自己录了几段视频,想说的话,巴巴拉拉啥都说说,不过现在也没看过😂
1.2 费用
全部费用没算过,就生娃住院那4天,自费部分差不多6500左右,包含1350的护工费(450*3),包含48天后产妇体检的费用。
因为是一胎,咱也没啥经验,害怕很多意外的出现,所以就选了杭州产科最强的医院:市一。 因为医院比较老,附近停车贼困难,而且还很贵,10块一小时。SUV停了几次机械车位,差点把后视镜干掉了。
实际生产还是挺顺利的,宝宝在妈妈肚子里多待了5天,出生之后做的各项目检查都正常。现在看,选着最近的医院才是比较好的选择。
1.3 为什么要生
和一部分人一样,家长催生占一部分,但不是决定性的。去年结婚,对我们而言就是生娃的信号。35岁以上会被定义为大龄产妇,大龄产妇又会面临着各种危险。同条件下,越年轻,生完恢复的也越好。
再加上我姐姐也还没生,我这个家庭相对来说缺一个娃来让整个大家庭更有目标感。所以现在布丁出生后,会有超级多的人来爱他,特别是妈妈和姑姑
偶然在xhs看到一句话:养娃能看到过去自己长大的过程,会把自己认为父母亏欠的部分加倍补偿给自己的孩子,或许是在治愈自己,也或许是在满足自己
1.4 养娃
太多攻略要做了,这里要非常感谢一位朋友的帮助,比我先生宝宝,然后经验都分享给我了,经常问他各种问题😭帮了巨多忙。
养娃不仅仅要研究育儿知识,每个月伺候宝宝的方法还是不同的,宝宝的变化非常非常快。针对宝宝的不同反应,要做出不同的应对。因为媳妇快要上班了,这些东西不能仅仅是我们自己会,还得教他奶奶。但是他奶奶也五十几了,很多东西学不会,记不住,就很困难,也没啥办法。
然后中间还要调解婆媳关系,我日常还要上班,中间有段时间,中午不吃饭,时间全部用来睡觉。。。
现在处于教学痛苦期,观察好宝宝的反应,其实很好哄。但是他奶奶学不会,导致现在给奶奶带,就往死里哭😭。但是我们又不得不依赖他奶奶,不然上班就没人管了。
但是,有时候他奶奶不觉得是自己的问题,就觉得是宝宝的问题,就是要闹人,也不知道找原因,所以我现在是非常痛苦的。有时候只能安慰媳妇,没办法,让宝宝自己适应。。。
1.5 拍拍拍
3个多月了,回头看过去的样子,感觉自己还拍少了😂
1.6 男宝女宝
就身边的现象来说
- 高中同学
- 目前已知3个女宝
- 微信网友
- 同一天出生的,1个女宝
- 去年兔年生的,1个女宝
- 村里
- 目前已知3个女宝
- 公司同事
- 1男宝1女宝
- 媳妇同事生1女宝
- 同产房
- 1男宝3女宝
15个宝宝,只有2个男宝,13个女宝。 生男生女,概率不是差不多吗?
我倒是无所谓,生男生女,各有各的好处。你们身边男宝多还是女宝多?
二、工作
去年武汉被裁后,就来杭州这家了,当时还有点小插曲。因为武汉一家公司在我入职这家后又给了offer,我很纠结要不要去,当时处于这也想要,那也想要的状态,精神差点崩溃。
2.1 极越(集度)
关注新能源的应该都知道这个事吧,12月直接宣布原地解散了。去年在武汉可是大规模招聘,开的也算武汉Top几了。当时面了4轮,战线拉一个月,后面HC收紧被待定,然后一个月以后又联系给offer,蜜汁操作。要是早给,我肯定就去了。。。
去了的话,现在又是找工作的时间。我媳妇就认为是她的功劳,不是她在杭州,不是她对房子没有那么大执念了,我肯定又回武汉了。当初想去集度,就想赌一把百度智驾。我想着百度都干那么久了,对其他车企不得是降维打击,结果啊,百度还是那个百度。
间接躲过一劫,差点失业
2.2 晋升
虽然结果没出,但是我觉得是个伤心事
2.3 面试
帮忙面试
最近帮着公司招外包,收到一些十年以上经验的简历,很尴尬。简历潦草的,让人感觉他们自己也没抱什么希望,上次招聘给了几个大龄的面试机会,结果一个不如一个。第一次9月份,第二次就是最近。9月份也面了挺多的,过了几个,但是当时卡的严,最后一面基本都被毙了,卡着卡着,HC就变成了0
还有个现象就是异地简历贼多,很多都不是在本地工作的,可见大家都在海投,市场情况就是这样
今年又出1个外包HC,我有时候面1面,有时候面2面,给过了几个,不知道能不能来入职。
前端分类
简单分为,1-3年,3-8年,8年+,外包,自研
3年内的多是自研,简历写的都挺不错,但是一问就不会,一问就是别人做的
5年左右,最近一份干外包的居多,技术也还不错
8年+的,很大一部分就简历拉跨,技术也拉跨,各方面都不太行,当然厉害的也不会来投递外包了哈
今年面试感觉到的情况,并不具有代表性,各位简单看看
简单分析
结合我自己现在的状况,我也明白为什么,就是技术停滞,就是学习能力在逐步下降,不得不服。或许因为懒惰,或许因为家庭事情越来越多。。今年我没怎么学习过,就写了1篇掘金文章,很是惭愧。有更多的时间,不是在打游戏,就是在刷视频,看直播。我尝试着在改变,但是有点难。。。
自己也越来越老,通过面试官的身份反省自己,得好好学习,不仅仅是技术方面。
2025年,我还是需要在这块寻找突破口,不能再停滞不前了,不然迟早要被淘汰。
三、旅游
因为有了车,计划了挺多地方的自驾游,但是因为媳妇怀孕,所以就只能轻度转转,尽量避开人多的地方
3.1 南京
视频带奶奶看了下玄武湖,还不如杭州湘湖,哈哈哈
3.2 千篇一律
之前想着把国内这些一二线城市都逛逛,感受感受。但是吧,现在感觉都是千篇一律的商业街,风景区,真没啥意思。每次做攻略都做的好好的,去了以后就感觉和理想的落差太大,然后从这次南京后,就不太想玩这种很常规的旅游了。
看xhs说,这是要加入下一个level的迹象了,明年等小布丁1岁后,他奶奶能带的时候。计划计划去港澳台逛逛,然后日本韩国这些,怎么都得去看看吧。。。
四、主机
4.1 入手
6月初,终于入手了人生第一台主机,是的,没错。毕业5年了,第一次拥有自己的主机,之前都用MacBook 虚拟机打游戏,LOL fps,30~60😂
4.2 配置
2024-5-29价格:
- 板U: 微星B760 爆破弹 Wifi D5 + 12600kf 1694
- 显卡:微星RTX4060 VENTUS2 X WHITE8GOC白色 2180
- 电源:微星MAG A600DN额定600W 234
- 机箱:微星PAG PANO M100L 白色 188
- 散热:微星MAG 寒冰E240白色水冷 369
- 内存:威刚D300 16G 6400MHZ 387
- 硬盘:威刚S50 PRO NVME 1TB 465
合计:5517
pdd微星官方旗舰店整机4999,用卷到手4863
4.3 为什么卖
主要3个原因
- window和mac两种系统切换着用,还是不太舒服,更喜欢mac
- 空闲时间就爱玩LOL,玩几把就要红温
- 有两次下班没带娃玩LOL,媳妇生气了
想了想以后,主机对我也没太大吸引力了,就挂xhs了,就挂了一天,第二天晚上卖了。4863买的,用了半年,卖了4050。
黑神话开挂通关的、使命召唤系列玩了3部,总体也算是过瘾了。
卖完只有一个感慨:老了,花有重开日,人无再少年
4.4 JJ卖主机的奇幻经历
AB两个买家,A爽快最终成交,B一直砍价最后破防
A需送上门 B上门自提,时间线如下:
- B凌晨3点就给我发了个消息,要购买记录,我早上回复了下,人家看我买半年了,砍价说3800,我说不出
- A看到后直接问3900送上门行不行,我犹豫了,来回70km+可能现场验收有问题,就拒绝了。拿着3900,我问早上的B要不要,要的话就给B了。结果B还在还价,问3850行不行,我拒绝了。
- A看我犹豫,直接说不还价了3999送上门,他急着用。我就准备和他交易了,这个时候B又来了,问我怎么样,我说A直接3999了,B这个时候急了,说他也可以3999,现在就可以上门
- 同价格我肯定选择B上门自提的,但是这个时候A已经拍下了咸鱼链接,我和A说了这个事,他又给我加了50,意思给路费。 我和B说,他那边已经拍了,B就生气了,长篇大论说我人不行。。。
所以最后的结果:我怕B是个事逼,而且A已经拍了,所以还是选择送货上门和A交易,A比较痛快,貌似是个主播,上门简单验机后直接打钱,省了咸鱼0.6%的手续费
这俩人都是玩无畏契约的,玩过几把,这游戏现在这么火?🔥
4.5 老了
回来路上,一个人在高架上飙了一把,只能感慨:花有重开日,人无再少年
五、11月软考
5.1 系统规划与管理师
过去没有了解过杭州政策,最近朋友说了考这个东西的好处,可以认证E类人才。买房只需要30%,不买房每个月也有2500补贴,政策很香。所以准备来试试,但是因为很久没看过书了,+懒+生娃各方面的因素,几乎没看,考试前还一直在想要不要去考。后面一想,钱都交了,不得去试试,看看裸考能考多少。
结果就是:
还有俩朋友一起考的也没过,很多认真学的,一部分卡在了论文上。毕竟这个东西和利益相关,所以会卡通过率。
5.2 信息系统项目管理师
2025-5月来战斗,有一起考的没!!!
六、其他
零零碎碎的其他事,不想花费太多精力去写这个,年级大了,很多东西都要和利益挂钩。没得利益,就不太愿意付出了。
6.1 兼职
- 赚了几个w,非理财
- 辛苦钱且不稳定
- 得寻找比较稳定的睡后收入
今年国庆节那波股市,太猛了。本来准备拿10个入场的,媳妇都同意了,还是胆小没敢上。。。差点套进去
6.2 领证
感觉要给孩子出生做准备了,之前了解的准生证、建档什么的都得结婚证,反正去年也结婚了,赶紧找时间领了,方便后面办户口。
实际上现在很多都放开了,并不需要结婚证,领了证反而变成已婚了,租房个税都只能填一个人的了。领了证,现在这行情,浙江刚落地的13天婚假也不敢休,有些地方领结婚证还给钱。
所以,领证没得啥好处,建议大家能不领还是不要领
6.3 减肥
- 减了30斤,不过现在还是很胖
- 目前体重稳定了一个月,继续开始减
6.4 房子
- 和媳妇两个人都不再有买房的执念
- 租了个两室一厅,4200,住的挺舒服的
- 没有房贷、没有车贷、没有任何贷款
- 养着小布丁,满足了
6.5 计划
2025年,全面拥抱AI,用一句话说:所有行业都值得被AI重构
想要更多技术分享和摸鱼乐趣?
- 加入微信摸鱼群:一起交流学习,共享资源,偶尔摸鱼,趣味多多!
- 关注公众号 [前端技术圈儿] :获取最新的前端技术干货、学习笔记和实用工具推荐!
- 加我wx:V798595965
快来和我一起探索前端世界吧! 🚀
最后再放一波儿子
来源:juejin.cn/post/7456898384331522099
进电子厂了,感触颇多...
作者:三哥
个人网站:j3code.cn
本文已收录到语雀:http://www.yuque.com/j3code/me-p…
是的,真进电子厂了,但主人公不是我。
虽然我不是主人公,但是我经历的过程是和主人公一样的,真实而又无奈。真实是真真切切的经历了这一段过程,而无奈则是我进厂这段旅程所体会出来的。
如果你对我的这段经历感兴趣,可以耐心的往下看。
故事
这里我先定义一个主人公 A,然后他来我这边找工作,那么我作为在广州待了有些年头的靓仔(一坤年多点),肯定要出时间给他张罗张罗,所以就有了这一段难忘的经历(现在回想起来,我是不想再经历第二次了)。
在广州没有学历,能选择的工作其实并不多,无非就是进厂或者打零工当学徒。但奈何我也没有什么找工作的门道,所以只能帮着从某直聘软件上刷工作岗位了。只要是不要求经验,学历的统统都是我备选岗位中的一个。在这里就不得不说在某直聘软件上,刷这种普工、配/拣/打包等岗位一刷一大堆,而且还自动找上门来,不像我现在干的程序员行业,要么进人才库要么人才库都不配进,只能石沉大海。
在结合 A 自身的情况和交通出行方便的情况下,我们决定接受进厂工作。因为他们发的待遇信息确实很吸引人,如:小时工 30/小时,多劳多的;正式工底薪加提成综合薪资7-8k;还包吃包住。你看,这怎么能不吸引人,怎么能不吸引一个以前只能拿 2-3k 工资不包吃不包住的主人公 A 呢!当然,我当时也觉得非常可以,以至于不假思索的和主人公 A 达成了统一战线。
那么,这次的难忘之旅就由此展开了。
先是早上按照他们微信上聊的,带好行李,身-份-证去一个交通非常方便的地铁口面试。那么我在想去面个试带行李干啥,我放在住的地方也挺好的,等面试通过了,安排住宿的地方了,我在将行李搬过去,这不简单轻松嘛。所以我和主人公 A ,就一致决定没有带行李过去,直接人 + 身-份-证过去面试,然而奇怪的点就来了。当我们到达面试的地方时,见到好多外来务工的人,这一幕让我有点触动。都是大包小包,蛇皮袋+黑行李箱,然而我两却空空如也。这时候我还没意识到问题的严重性,就直接过去问怎么面试,而那个穿蓝色工作服的人只是抬头撇了我一眼,看我们没有行李,直接抬手挥了挥,说没行李的不收。我说,为什么,他也不搭理我,直接说别妨碍后面的人面试(其实后面也没人),不收就是不收。
在这里我已经忍不了了,先说一下此刻我的内心:diss 他上下 18 代无数遍了,后面细讲,咱们先看故事。
其实我当时是想直接怼他,但为了照顾主人公 A 的情况,我忍了,重新回家去拿了一次行李,然后再次去面试的地方找工作了,但这次不是上一个地点,而是我另一个备选面试(也是需要行李,还是行李)。
两个人拖着个行李,酿酿跄跄的来到了下一个面试点,还是和上一个面试点一样的流程,但这次不同的是我带了行李,ok,他接收了,说等会安排面试。其实我以为这就完了,但是并没有,这才是开始的第一步。
等了一会之后,这个面试点就陆陆续续的来了二三十个人,也还是和我上次看到的人群一样,大包小包,蛇皮袋+黑色行李箱。此时我内心就不仅感慨,为了个工作也是不容易。等了差不多十几分钟就有个嗓门大的人喊,要面试的人跟我来,带上行李,没行李的不要来。就这样,我和主人公 A 被带到了一个不是很大的大厅,里面差不多容纳了几百人,此时我又被触动了(是的,以我目前的认知,我这段经历会被触动很多次),感觉进了传销一样。
从进入那个大厅以后,我就觉得非常无力,这种无力感你只有亲自体会了才知道。全是人,但就是没有工作人员,只知道等着叫号,其余时间找个地方坐着。当时就想,走也不是来都来了,留也不是和印象中的面试太不一样了。所以就在这种内心纠结中,徘徊了几个小时,是的你没看错,就是几个小时(起码两小时)。
这期间其实还有套路,我怕文章会写的很长,就忽略了,如果你们想听,以后多关注我 B 站直播,我会时不时的讲出来。
中午的时候,终于有个人扯着嗓门喊话,让我们安静下来,听他介绍后续的招聘流程。他的话不是很精炼,但是足够洗脑,因为他会在合适的时机给这些找工作的人透露,在该公司上班工资比的过广州大多数CBD上班的白领。只要这话时不时的出现,人群就会时不时的骚动几下,其实我能明白这些务工人拼搏一年不就是为了多赚点吗?听到高薪,眼睛发光是在正常不过了。但是,我就是看不惯这些中介公司层层剥利,压榨忽悠这些信息闭塞的外来务工人员(我这是事后才知道,不是工厂直招,而是中介)。
反正一番讲话之后,我们被分成了两批人,一批工资高点,但是上班地点异常的偏僻,另一批就是工资地点,但好在是市里,交通方便。
但我个人觉得,工作高低也是骗人的,说的高工资,去的人就多,他们抽的利也就多。
而我选择工资低的这类工作也就是看中他交通方便,能时常照顾一下,帮点忙啥的。
但,你以为这就完了嘛,并没有。之后的这些时间,我被他们用大巴车从集合点、拉到电子厂、再由电子厂拉到体检中心、最后拉到安排宿舍大楼。每一个地点的具体事宜如下:
- 集合点:忽悠办电话卡,打印身-份-证,蓝底照片制作,几百号人分成高薪资与低薪资两批人(但都声称比 CBD 白领薪资高)。
- 电子厂:面试,很奇葩的面试,就是看你的面相 + 基本信息,全称一句话都不用说。
- 体检中心:这个就是单纯体检,不过体检地点非常远,非常远,一看就是和这种中介公司有绑定关系,还是抽利。
- 宿舍大楼:安排宿舍,如蜂窝煤一样的格子间,超密集。
这期间,我们还没工作,杂七杂八的费用就被他们收去了 200 块(我们没有办卡,如果办卡了估计更多)。而且以上的时间线,每一个点都是非常熬人的,我一个正常男子都觉得非常累非常累,真不知道那些年纪小的、年纪大的、女生是如何坚持下来的。
熬人是因为,我们是一批人一批人的过,要等到这一批人的流程走完了才能走下一个流程,而且有些流程只有特定的时间点才开始,来早了,你就给我等着,到点才开始。
最后,当你分配到一张上下铺的床位后,你的这一趟流程就算是彻底走完了,时间也应该是来到了晚上六七点,所以就为了一个进厂工作的机会,从早上九点折腾到晚上六七点,真 TM 累。
以上就是我今天一天所经历的事情,现在想想,如果是你,你会为了进厂而这样折腾吗?
有感
不经历一遍,永远是不能感同身受。
上面的故事你们看了,也就是看了,可能内心都不会有一点波澜,但这个没关系,如果条件允许我希望看到这篇文章的人,永远都不要有这种经历。
首先,我来对这种中介第三方公司招人的方面,来说说我的看法:
你们要先分别什么是工厂直招还是第三方中介招聘,如果是从某直聘上看到的岗位应该是会写“派遣”或者“代招”。当然,也有不写的,那么这就需要后续的判断了“没有行李不接待”。是的,就是这个,我现在才反应过来,如果你去面试,不带行李,他们接待你,然后给你安排这一系列的流程,你中途觉得不靠谱,你是可以随时走人的,一点都不麻烦。但是如果你是大包小包 + 黑色行李箱的话,那么你就不可能那么方便的随时想走就走了,这个行李就是第三方中介拷住你的加锁。
你们也不要相信从某直聘上加的所谓人事的话,他们只有一句话是真的,那就是引你到面试的地点是真的,其余的都是假话,其余的都是假话。如果你们以后不幸碰到我上面的故事情节,千万不要对这种人抱有任何的感激,觉得他们真好,为了给我一个工作机会,解答我的各种问题。醒醒吧!你的真诚/感激之情真有可能是喂了狗(狗子对不起,不应该拿你做对比),你带着真诚的心态去问你所不了解的各种问题,他们只会用高薪,轻松等话术迷惑你。因为他们招到一个人,拿一个人的提成,你们只是他们的赚钱工具,才不会管你这个工作是否真高薪,真轻松,如果是为啥他们自己不上。
这种第三方公司做事效率是非常快,一天办理几百人的入职流程,为了就是让你们入职,他们有提成。如果等到第二天你们反应过来,后悔了,那么他们就没钱可赚了。效率快是好,但是整套流程下来,你们有没有发现,连午饭时间都不给人了(就怕你们溜走)。是的,我当时就是没吃午饭,硬撑着走完了一天。而且在后续我与同行的打工人交谈过程中了解到,有些人早饭都没吃,也就是一点都行没吃,来来回回折腾一整天,直到晚上才有时间去吃饭,而这个时候就不怕你走了,因为你的入职流程已经办完了,该交的钱也交了。
以上套路只是我经历过得一种,但肯定不止这一种,希望大家能提前辨别!
接着,我来说说这一天,我与同行人交谈过程中了解到的各种人生经历:
虽然我是程序员,但绝不是那种沉闷不爱社交的性格,相反我还是挺爱和人交谈的,爱听别人的故事也爱和别人分享我的故事。
我碰到的第一种人就是未满 18 岁的小孩,故事开始的时候我提过,前期我是被人带到一个大厅集合的,之后,未满 18 岁的小孩就被所谓的工作人员给筛选出来了。其实说是筛选出来,肯定也不是让他们走,只是安排他们去那种更偏僻、工资更低一点的、对年龄要求不严格的小厂工作。反正,只要有人头进入工厂,他们这种中介就有钱赚,只是从你身上赚多赚少而已。
看着这几个未满 18 岁就出来打工的小孩,内心多少都是有点不是滋味。毕竟这个时候他们应该是呆在校园,且正处于高中,正是读书改变命运的关键时刻。但,我想这种再次读书的机会,应该是不会在这几个小孩身上出现了。因为我没有机会和这几个小孩说话,所以不知道他们是什么原因走出校园,走出家庭,而选择步入社会。但是,希望他们几个往后能一帆风顺,改天换命。
第二种人,我也没有机会和他说话,我关注到他是因为一段对话:
招人的负责人说:看你年龄比较偏大,都 45 了?
务工大哥:是,确实大了点。
招人的负责人说:像你这种情况,年龄比较大,我可以招你进去,但是工厂要不要,就不能保证了,这个你要明白。
务工大哥:行,就过去试试吧!
招人的负责人说:好,等会跟着一起过去。
其实从这段对话中我们也能很快想到,工厂肯定是不想要那种年龄偏大的员工了,毕竟工作强度还是有的,万一出现什么身体情况,咋搞。而务工大哥,肯定也是不想失去这种工作机会嘛,毕竟对于他而言,能有高工资就行,其他的应该都是次要的。
看着这务工大哥珍惜工作机会的态度,我瞬间就醍醐灌顶。内心不禁在想,是什么样的生活压力或者其他原因,才能让这务工大哥情愿跟着一群小年轻这样奔波找工作。
因为没有机会和这位务工大哥搭上话,所以我也就内心想想罢了。
第三种人,我终于是和他们搭上话了,他们都很年轻。在和他们简单的交流过后,我了解到的情况大致如下:
年龄基本都是18、19岁左右,并且学历都不高,有初中、高中、中专等学历的。还有些是中途辍学,并没有完整的读完学历就出来工作的,而且我问他们不读书的原因,他们的回答基本都是自己不想读了,想出来找工作,有钱花。所以现在读书,倒不是家里穷读不起,而是很大一部分人,受不了社会上的一下诱惑,想要快点赚钱,好有能力买各种喜欢的东西,包括打游戏买装备(因为和他们聊天中,都是游戏不离手的状态)。
像他们这种,吃不了学习上的苦,就只能吃生活上的苦。以后或许他们还有选择的机会,毕竟还年轻,但是,我想他们应该在未来很长一段时间,都是游离在各种电子厂。因为感觉他们还是玩心重,而电子厂这种工作,多白了就是工作轻松,多劳多得,还包吃包住。等到某一时刻他们过腻了这种日子,应该能重新审视一下自己,未来的路,希望他们能早点醒悟。
第四种人,你可以理解他们是第三种人的升级版,就是他们一直没有醒悟,一直都是从事着电子厂这种工作。只是从厂 A,来到了他们认为工资,工作各方面还不错的厂 B 而已。你可以在他们身上看到全国各地奔波的痕迹:蛇皮袋、黑行李箱、竹席被褥、胡子拉碴、头发凌乱、烟味很重。我提到的这些,只是单纯的用文字写出来,而没有任何的其他意思。在和他们交谈的过程中,能很明显的看出一种无奈的心情出来,他们无非就是想要一个稳定,有点收入的工作。但是工厂就是这样,忙的时候招人,不忙的时候拼命的赶人。他们也不想走的,但工厂不忙,肯定是不发工资的,你这样呆在工厂没有任何的意义,所以只能又拿起行囊四处奔波。
所以,生活就是充满着未知的变化,我们能做的就是不断的提升自己,来面对这种变化。希望能看到这里的人,都不被生活所压迫,加油吧!
还有最后一种人,也是我最想不到的一种人。他们在没有进厂之前,手底下有十多号人,一个月光租金就可以花出去五六万。是的,我们暂且可以把他们定为老板被迫进厂的一类人。
说他们是老板,一点都没错。因为他们在没有进厂之前,确实是自己开店做老板的,只是因为 21 年疫情的时候,大家都闭门不出,实体经济很难再维持这种高房租的压力,所以才被迫关店,倒闭。我那时问他们,为什么不多坚持坚持,你看现在不就春暖花开了吗?他们只是面带苦涩的说,多坚持一个月,就是五六万的流水出去,而且那会不知道什么时候是个头。并且他们不是没坚持,而是已经没有钱再可以坚持下去了,所以只能现倒闭,让自己缓一缓,释放一下压力。
我也确实从他们身上感受到了他们所说的压力,只出不进,五六万的流水,这谁能受得了。并且疫情过去的时候,也想过重新再来,但是已经没有钱可以再来了,那会欠的钱还没还清,已经没有资金重头再来了。
这一天的交谈中,我见过两个这种人,以前都是老板,也都是因为疫情原因,赔钱了,只能进厂缓一缓,让自己好受一点。但是他们毕竟是见过一些世面的人,你可以很明显的感受到他们和前面这几种人不一样,如装扮、说话和回答问题方式,并且他们也都很乐观,还说,等我在工厂缓过来之后,还是会继续再拼搏一番。你看看,这不就非常好嘛!在人生的这条道路上,如果你跌倒,说明你选择的是一条坎坷的路,只要你能站起来,继续走下去,我相信你的收获绝对能对得起你这一路的坎坷。
好了,洋洋洒洒五千多字,算是我对那天经历的一个交代。如果你看到这里,内心也有一些感触,说明你是一个感性的人,欢迎你在评论区留下你的足迹。
题外话:主人公 A 最后还是没去成!🤣🤣🤣
来源:juejin.cn/post/7343132138655858724
如果我贷款买一套 400W 的房子,我要给银行多送几辆迈巴赫?
买房攻略
2023 年至今,上海房价一跌再跌。俺已经蠢蠢欲动了,磨刀霍霍向"买房"。但是奈何手里钞票不够,只能向天再借 500 年打工赚钱。但是作为倔强的互联网打工人,想知道自己会被银行割多少韭菜。于是就写了个程序,用于计算我贷款买房需要多给银行还多少钱。这样我就能知道银行割我的韭菜,能省下几辆迈巴赫的钱了。
贷款利率
- 公积金的贷款利率。
- 首房:贷款时间 <=5 年,利率为 2.6% ;贷款时间 >= 5 年,利率为 3.1% 。
- 非首房:贷款时间 <=5 年,利率为 3.025% ;贷款时间 >= 5 年,利率为 3.575% 。
- 商业险贷款利率
- 贷款时间 <=5 年,利率为 3.45% ;贷款时间 >= 5 年,利率为 3.95% 。
代码实现
- 以下代码,实现了:我贷款买房需要多给银行还多少钱。
public class LoanAmountCalculation {
//首套住房5年以内公积金贷款利率
private static final double FIRST_HOUSE_ACCUMULATION_FUND_LOAN_RATE_WITHIN_FIVE_YEARS = 2.6;
//首套住房5年以上公积金款利率
private static final double FIRST_HOUSE_ACCUMULATION_FUND_LOAN_RATE_MORE_FIVE_YEARS = 3.1;
//二房5年以内公积金贷款利率
private static final double NOT_FIRST_HOUSE_ACCUMULATION_FUND_LOAN_RATE_WITHIN_FIVE_YEARS = 3.025;
//二房5年以上公积金款利率
private static final double NOT_FIRST_HOUSE_ACCUMULATION_FUND_LOAN_RATE_MORE_FIVE_YEARS = 3.575;
//5年以内商业贷款利率
private static final double COMMERCIAL_LOAN_RATE_WITHIN_FIVE_YEARS = 3.45;
//5年以上商业贷款利率
private static final double COMMERCIAL_LOAN_RATE_MORE_FIVE_YEARS = 3.95;
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
double houseAmount = getInputValue(scanner, "请输入预计买房金额(单位:W):", "请输出正确的买房金额(>0)!");
double principal = getInputValue(scanner, "请输入您的本金(单位:W):", "请输出正确的买房金额(>0)!");
if (principal >= houseAmount) {
System.out.println("全款买房,崇拜大佬!");
return;
}
double accumulationFundLoanAmount = getInputValue(scanner, "请输入公积金贷款金额(单位:W):", "请输出正确的公积金贷款金额(>0)!");
double commercialLoanAmount = houseAmount - principal - accumulationFundLoanAmount;
if(commercialLoanAmount <= 0){
System.out.println("您的本金+公积金贷款已经够买房啦,恭喜大佬!");
return;
}else{
System.out.println("您的本金+公积金贷款还不够买房哦,需要商业贷款金额为(单位:W):" + commercialLoanAmount + "\n");
}
int accumulationFundLoanYears = getInputIntValue(scanner, "请输入公积金贷款年份(单位:年):");
int commercialLoanAmountYears = getInputIntValue(scanner, "请输入商业贷款年份(单位:年):");
int isFirstHouse = getInputIntValue(scanner, "请输入是否首房(0:否,1:是):");
LoanAmount loanAmount = calculateLoanAmount(
accumulationFundLoanAmount, accumulationFundLoanYears,
commercialLoanAmount, commercialLoanAmountYears, isFirstHouse);
System.out.println("详细贷款信息如下:" + "\n" + loanAmount);
}
/**
* 获取double类型的输入
* @param scanner:Java输入类
* @param prompt:提示信息
* @param errorMessage:输入错误的提示信息
* @return 一个double类型的输入
*/
private static double getInputValue(Scanner scanner, String prompt, String errorMessage) {
double value;
while (true) {
System.out.println(prompt);
if (scanner.hasNextDouble()) {
value = scanner.nextDouble();
if (value > 0) {
break;
} else {
System.out.println(errorMessage);
}
} else {
scanner.next();
System.out.println(errorMessage);
}
}
return value;
}
/**
* 获取int类型的输入
* @param scanner:Java输入类
* @param prompt:提示信息
* @return 一个int类型的输入
*/
private static int getInputIntValue(Scanner scanner, String prompt) {
int value;
while (true) {
System.out.println(prompt);
if (scanner.hasNextInt()) {
value = scanner.nextInt();
if (value > 0) {
break;
} else {
System.out.println("请输入正确的年份(>0)!");
}
} else {
scanner.next();
System.out.println("请输入正确的年份(>0)!");
}
}
return value;
}
/**
* 功能:贷款金额计算
* 入参:
* 1.accumulationFundLoanAmount:公积金贷款金额 2.accumulationFundLoanYears:公积金贷款年份;
* 3.commercialLoanAmount:商业贷款金额; 4.commercialLoanAmountYears:商业贷款年份
* 5.isFirstHouse:是否首房
*/
private static LoanAmount calculateLoanAmount(double accumulationFundLoanAmount, int accumulationFundLoanYears,
double commercialLoanAmount, int commercialLoanAmountYears, int isFirstHouse){
LoanAmount loanAmount = new LoanAmount();
//公积金贷款还款金额
double accumulationFundRepaymentAmount;
if(isFirstHouse == 1){
accumulationFundRepaymentAmount = accumulationFundLoanYears <= 5 ?
accumulationFundLoanAmount * Math.pow((100 + FIRST_HOUSE_ACCUMULATION_FUND_LOAN_RATE_WITHIN_FIVE_YEARS) / 100, accumulationFundLoanYears)
: accumulationFundLoanAmount * Math.pow((100 + FIRST_HOUSE_ACCUMULATION_FUND_LOAN_RATE_MORE_FIVE_YEARS) / 100, accumulationFundLoanYears);
}else{
accumulationFundRepaymentAmount = accumulationFundLoanYears <= 5 ?
accumulationFundLoanAmount * Math.pow((100 + NOT_FIRST_HOUSE_ACCUMULATION_FUND_LOAN_RATE_WITHIN_FIVE_YEARS) / 100, accumulationFundLoanYears)
: accumulationFundLoanAmount * Math.pow((100 + NOT_FIRST_HOUSE_ACCUMULATION_FUND_LOAN_RATE_MORE_FIVE_YEARS) / 100, accumulationFundLoanYears);
}
loanAmount.setAccumulationFundRepaymentAmount(String.format("%.2f", accumulationFundRepaymentAmount));
//公积金贷款每年还款金额
loanAmount.setAccumulationFundAnnualRepaymentAmount(String.format("%.2f", accumulationFundRepaymentAmount / accumulationFundLoanYears));
//商业贷款还款金额
double commercialRepaymentAmount = commercialLoanAmountYears <= 5 ?
commercialLoanAmount * Math.pow((100 + COMMERCIAL_LOAN_RATE_WITHIN_FIVE_YEARS) / 100, commercialLoanAmountYears)
: commercialLoanAmount * Math.pow((100 + COMMERCIAL_LOAN_RATE_MORE_FIVE_YEARS) / 100, commercialLoanAmountYears);
loanAmount.setCommercialRepaymentAmount(String.format("%.2f", commercialRepaymentAmount));
//商业贷款每年还款金额
loanAmount.setCommercialAnnualRepaymentAmount(String.format("%.2f", commercialRepaymentAmount / commercialLoanAmountYears));
//公积金贷款超出金额
loanAmount.setAccumulationFundLoanExceedAmount(String.format("%.2f", accumulationFundRepaymentAmount - accumulationFundLoanAmount));
//商业贷款超出金额
loanAmount.setCommercialLoanExceedAmount(String.format("%.2f", commercialRepaymentAmount - commercialLoanAmount));
loanAmount.setTotalExceedLoanAmount(String.format("%.2f", accumulationFundRepaymentAmount - accumulationFundLoanAmount + commercialRepaymentAmount - commercialLoanAmount));
return loanAmount;
}
@Data
static class LoanAmount{
/**
* 公积金贷款还款金额
*/
private String accumulationFundRepaymentAmount;
/**
* 公积金贷款每年还款金额
*/
private String accumulationFundAnnualRepaymentAmount;
/**
* 商业贷款还款金额
*/
private String commercialRepaymentAmount;
/**
* 商业贷款每年还款金额
*/
private String commercialAnnualRepaymentAmount;
/**
* 公积金贷款超出金额 = 公积金贷款还款金额 - 公积金贷款金额
*/
private String accumulationFundLoanExceedAmount;
/**
* 商业贷款超出金额 = 商业贷款还款金额 - 商业贷款金额
*/
private String commercialLoanExceedAmount;
/**
* 总共贷款超出金额
*/
private String totalExceedLoanAmount;
@Override
public String toString() {
return "1.公积金贷款还款金额=" + accumulationFundRepaymentAmount + "万元\n" +
"2.商业贷款还款金额=" + commercialRepaymentAmount + "万元\n" +
"3.公积金贷款每年还款金额=" + accumulationFundAnnualRepaymentAmount + "万元\n" +
"4.商业贷款每年还款金额=" + commercialAnnualRepaymentAmount + "万元\n" +
"5.公积金贷款超出金额=" + accumulationFundLoanExceedAmount + "万元\n" +
"6.商业贷款超出金额=" + commercialLoanExceedAmount + "万元\n" +
"7.总共贷款超出金额=" + totalExceedLoanAmount + "万元\n";
}
}
}
代码输入,输出示例
由上图可知,我要贷款买一套 400w 的房子,本金只有 120w,使用组合贷:公积金贷款 120w(10年),商业贷款 160w(20年)。最终我需要多还银行 230.07w,相当于买两辆迈巴赫的钱了,巨亏!
以上就是全部内容了,如果涉及到真实场景,还是需要根据具体的情况计算的!
来源:juejin.cn/post/7346385551366684722
从支付宝P0事故处理方案,合理推测损失金额
支付宝
支付宝昨天(2024-01-16)的无差别"送钱"的事儿,大家都知道了吧。
具体的,就是在昨天 14:40~14:45 期间,所有支付宝的支付订单都被减免了 20%,减免原因在界面上显示为"政府补贴"。
这里指的订单,是指所有通过支付宝产生的交易,包括「购物、信用卡、生活缴费、个人转账」等等,而且和此前(消费类的)有减免上限的政府补贴不同,本次减免无上限,统统 20%。
好家伙,个人转账 5W 减免 1W,那些刚好在那段时间有大额支付的幸运儿,你能想象他们多开心吗 🤣🤣🤣🤣
如此重大的 P0 事故(互联网公司对线上事故的评级,代表事故最高等级),虽然只有短短的 5 分钟,但如果被反应快又别有用心的不法分子利用上(同一笔钱,两个账号来回转),那就可不是一点羊毛的事儿,可能几十上万上百万,整条羊村都被薅走了。
正当所有人都觉得支付宝一定会或多或少有"追回"行动时,凌晨一则来自「支付宝官方微博」的公告说明来了:
简单总结的话:错误在支付宝一方,给出的优惠不会再追回。
好家伙,这属于是给这小部分的幸运儿发"过年费"了 🍋🍋🍋
虽然犯的是如此低级的错误,不像大公司所为,但处理方案又是实打实的"有格局"。搞得我都不好意思笑支付宝"草台班子、降本增笑"了 🤣🤣🤣
从本次的处理方案来看,我们可以做一些合理的猜测:
从这个发布声明的时间点来看,不难猜测,对于这次事故,支付宝经过了「修正模板 -> 统计事故损失金额 -> 事件逐级上报到高层 -> 高层决议最后处理方案 -> 将处理方案下发对应部门 -> 公关拟对外声明 -> 走声明审批流程 -> 正式发出」等多步环节,导致了声明发出的时间已经接近凌晨一点。
由于声明中涉及「提醒大家不要点击诈骗短信/链接」,因此必然不存在支付宝故意推迟声明时间的可能性,他们从事故到发声明,确实就是花了 10+ 小时。
另外,关于支付宝损失金额的猜测。
如果简单结合数据来看,这个数字会很大。
根据易观分析报告的公开数据,支付宝 2024 年第一季度的交易量为 118.19 万亿元,即每个月 39.4 万亿,折合每天约 1.31 万亿,每小时约 0.0546 万亿,每分钟约 9.1 亿。
事故维持 5 分钟,减免力度为 20%,就当全部订单都是不符合"政府补贴"要求的支付订单,那么损失金额约 亿。
但实际情况并不会如此直接了当,支付订单的流量也不可能是均摊每天,甚至是每分钟。
从日期来说,1-16 是一个没有节日加成的普通周四;从时间点来说,14:40~14:45 虽然属于"白天"范畴,但也不是什么支付高峰期。
我问了在支付宝工作的朋友,他跟我分享道:一整周里的交易,会有接近一半的交易流水,会在周末假期产生;而如果是周内的工作日的话,会有 8 成的流水会在上班时间(09:00~18:00)以外的时间段产生。
基于此,虽然没有具体数字,同时事故维持时间段(5 分钟),不考虑传播效应导致的交易激增。可以合理推算 2025-01-16 14:40~14:45 产生的交易最多不会超过 20 亿,即亏损最多不会超过 4 亿。相比于简单线性均摊的 9.1 亿,还是要小不少的。
支付宝(蚂蚁金服)是一家全年净利润 238.2 亿的公司,4 个亿的事故,还是支付得起的,只不过导致事故发生的员工和部门,估计要面临大处罚了。
对此,你怎么看?昨天有薅到支付宝的羊毛吗?欢迎评论区交流。
...
回归主题。
来一道和「阿里(校招)」相关的算法题。
题目描述
平台:LeetCode
题号:406
假设有打乱顺序的一群人站成一个队列,数组 people
表示队列中一些人的属性(不一定按顺序)。每个 表示第 个人的身高为 ,前面 正好 有 个身高大于或等于 的人。
请你重新构造并返回输入数组 people
所表示的队列。返回的队列应该格式化为数组 queue
,其中 是队列中第 个人的属性( 是排在队列前面的人)。
示例 1:
输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
解释:
编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。
编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。
编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。
编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。
编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。
示例 2:
输入:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]]
输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]
提示:
- 题目数据确保队列可以被重建
构造 + 二分 + 树状数组
这是一道非常综合的题目。
首先根据双关键字排序:当「高度(第一维)」不同,根据高度排升序,对于高度相同的情况,则根据「编号(第二维)」排降序。
采取这样的排序规则的好处在于:在从前往后处理某个 时,我们可以直接将其放置在「当前空位序列(从左往后统计的,不算已被放置的位置)」中的 位(预留了前面的 个位置给后面的数)。
关于「空位序列」如图所示(黄色代表已被占用,白色代表尚未占用):
具体的,我们按照构造的合理性来解释双关键字排序的合理性,假设当前处理的是 :
根据「高度」排升序,根据「编号」排降序:由于首先是根据「高度」排升序,因此当 被放置在「当前空位序列」的第 之后,无论后面的 如何放置,都不会影响 的合法性:后面的数的高度都不低于 ,无论放在 前面还是后面都不会影响 的合法性。
同时对于高度(第一维)相同,编号(第二维)不同的情况,我们进行了「降序」处理,因此「每次将 放置在空白序列的 位置的」的逻辑能够沿用:
对于「高度」相同「编号」不同的情况,会被按照「从右到左」依次放置,导致了每个 被放置时,都不会受到「高度」相同的其他 所影响。换句话说,当 放置时,其左边必然不存在其他高度为 的成员。
剩下的在于,如何快速找到「空白序列中的第 个位置」,这可以通过「二分 + 树状数组」来做:
对于已被使用的位置标记为 ,未使用的位置为 ,那么第一个满足「 的个数大于等于 」的位置即是目标位置,在长度明确的情况下,求 的个数和求 的个数等同,对于位置 而言(下标从 开始,总个数为 ),如果在 范围内有 个 ,等价于有 个 。
求解 范围内 的个数等价于求前缀和,即「区间查询」,同时我们每次使用一个新的位置后 ,需要对其进行标记,涉及「单点修改」,因此使用「树状数组」进行求解。
Java 代码:
class Solution {
int n;
int[] tr;
int lowbit(int x) {
return x & -x;
}
void add(int x, int v) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] += v;
}
int query(int x) {
int ans = 0;
for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i];
return ans;
}
public int[][] reconstructQueue(int[][] ps) {
Arrays.sort(ps, (a, b)->{
return a[0] != b[0] ? a[0] - b[0] : b[1] - a[1];
});
n = ps.length;
tr = new int[n + 1];
int[][] ans = new int[n][2];
for (int[] p : ps) {
int h = p[0], k = p[1];
int l = 1, r = n;
while (l < r) {
int mid = l + r >> 1;
if (mid - query(mid) >= k + 1) r = mid;
else l = mid + 1;
}
ans[r - 1] = p;
add(r, 1);
}
return ans;
}
}
C++ 代码:
class Solution {
public:
int n;
vector<int> tr;
int lowbit(int x) {
return x & -x;
}
void add(int x, int v) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] += v;
}
int query(int x) {
int ans = 0;
for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i];
return ans;
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& ps) {
sort(ps.begin(), ps.end(), [](const vector<int>& a, const vector<int>& b) {
return a[0] != b[0] ? a[0] < b[0] : b[1] < a[1];
});
n = ps.size();
tr.resize(n + 1, 0);
vector<vector<int>> ans(n, vector<int>(2));
for (auto& p : ps) {
int h = p[0], k = p[1];
int l = 1, r = n;
while (l < r) {
int mid = l + r >> 1;
if (mid - query(mid) >= k + 1) r = mid;
else l = mid + 1;
}
ans[r - 1] = p;
add(r, 1);
}
return ans;
}
};
Python 代码:
class Solution:
def __init__(self):
self.n = 0
self.tr = []
def lowbit(self, x):
return x & -x
def add(self, x, v):
while x <= self.n:
self.tr[x] += v
x += self.lowbit(x)
def query(self, x):
ans = 0
while x > 0:
ans += self.tr[x]
x -= self.lowbit(x)
return ans
def reconstructQueue(self, ps: List[List[int]]) -> List[List[int]]:
ps.sort(key=lambda x: (x[0], -x[1]))
self.n = len(ps)
self.tr = [0] * (self.n + 1)
ans = [[0, 0] for _ in range(self.n)]
for p in ps:
h, k = p
l, r = 1, self.n
while l < r:
mid = l + r >> 1
if mid - self.query(mid) >= k + 1:
r = mid
else:
l = mid + 1
ans[r - 1] = p
self.add(r, 1)
return ans
TypeScript 代码:
let n: number;
let tr: number[];
function lowbit(x: number): number {
return x & -x;
}
function add(x: number, v: number): void {
for (let i = x; i <= n; i += lowbit(i)) tr[i] += v;
}
function query(x: number): number {
let ans = 0;
for (let i = x; i > 0; i -= lowbit(i)) ans += tr[i];
return ans;
}
function reconstructQueue(ps: number[][]): number[][] {
ps.sort(((a, b) => {
return a[0] != b[0] ? a[0] - b[0] : b[1] - a[1];
}));
n = ps.length;
tr = new Array(n + 1).fill(0);
const ans = new Array(n).fill([0, 0]);
for (const p of ps) {
const [h, k] = p;
let l = 1, r = n;
while (l < r) {
const mid = l + r >> 1;
if (mid - query(mid) >= k + 1) r = mid;
else l = mid + 1;
}
ans[r - 1] = p;
add(r, 1);
}
return ans;
};
- 时间复杂度:排序的复杂度为 ;共要处理 个 ,每次处理需要二分,复杂度为 ;每次二分和找到答案后需要操作树状数组,复杂度为 。整体复杂度为
- 空间复杂度:
来源:juejin.cn/post/7460696845434961920
程序员加班很晚应该怎么锻炼身体?
文章首发到公众号:月伴飞鱼,每天分享程序员职场经验!
大家好呀,我是飞鱼。
作为程序员,肯定都深受加班的痛苦。
❝
那每天加班很晚的情况下,该通过怎样的锻炼来保持身体健康呢?
我觉得还是得先把觉睡够,然后才是锻炼。
❝
睡眠不足情况下高强度锻炼,容易猝死。
如果睡觉的时间都不够,建议辞,换个不太卷的地方。
把特别卷的岗位,留给那些更年轻的,特别需要钱买房结婚的,拼几年,把生存问题解决掉之后,就不要再用命赚钱了。
人生几十年,钱是赚不完的,基本生活需求之外,多赚到的钱,对生活质量提升作用有限。
睡眠的优先级,不但高于锻炼,甚至高于洗脸洗澡。
❝
而且睡前三小时不要吃太多东西。
对于经常晚上加班很晚的人来说,戒掉睡觉前玩手机的不良习惯,尽量减少晚上的一切活动,争分夺秒地保证睡眠。
健身,足够的营养和休息,都比身体锻炼本身更重要。
❝
所以如果长期生活不规律,饮食习惯不好,休息睡眠不能保证。
如果已经很累了,就不要考虑上高强度的训练了,夸张一点有可能做个俯卧撑都有可能把人送进医院。
有位网友总结得好:
❝
去健身,你会得到强壮的身体,过度劳累,你会得到猝死的尸体,过度劳累还去健身,你会得到强壮的尸体。
所以:下班晚,好好休息就是你最好的健身!
程序员在工作空闲之余也可以通过以下方式来锻炼身体:
❝
通过走路或骑自行车的方式出门活动,可以锻炼身体的同时享受户外的新鲜空气。
在家里可以做一些简单的,如俯卧撑、仰卧起坐等,这些操作都可以锻炼身体的同时不需要太多的器材。
有啥其他看法,欢迎在评论区留言讨论。
❝
想看技术文章的,可以去我的个人网站:hardyfish.top/。
- 目前网站的内容足够应付基础面试(
P6
)了!
每日一题
题目描述
❝
给你一个二叉树的根节点
root
, 检查它是否轴对称。
解题思路
❝
递归实现
递归结束条件:
- 都为空指针则返回
true
- 只有一个为空则返回
false
递归过程:
- 判断两个指针当前节点值是否相等
- 判断 A 的右子树与 B 的左子树是否对称
- 判断 A 的左子树与 B 的右子树是否对称
代码实现
Java
代码:
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root == null) {
return true;
}
return dfs(root.left,root.right);
}
public boolean dfs(TreeNode p,TreeNode q){
if (p == null && q == null) {
return true;
} else if (p == null||q == null) {
return false; //只有一个为空
}
if(p.val != q.val) {
return false;
}
//第一棵子树的左子树和第二棵子树的右子树对称,且第一棵子树的右子树和第二棵子树的左子树对称
return dfs(p.left,q.right) && dfs(p.right,q.left);
}
}
Python
代码:
class Solution(object):
def isSymmetric(self, root):
"""
:type root: TreeNode
:rtype: bool
"""
if not root:
return True
def dfs(left,right):
# 递归的终止条件是两个节点都为空
# 或者两个节点中有一个为空
# 或者两个节点的值不相等
if not (left or right):
return True
if not (left and right):
return False
if left.val!=right.val:
return False
return dfs(left.left,right.right) and dfs(left.right,right.left)
# 用递归函数,比较左节点,右节点
return dfs(root.left,root.right)
Go
代码:
func isSymmetric(root *TreeNode) bool {
// 递归-对称二叉树
var dfs func(left, right *TreeNode) bool
dfs = func(left, right *TreeNode) bool {
if left == nil && right == nil {
return true
}
if left ==nil || right == nil || left.Val != right.Val {
return false
}
// 左右子节点都存在且val等,递归其子树
return dfs(left.Left, right.Right) && dfs(left.Right, right.Left)
}
return dfs(root.Left, root.Right)
}
复杂度分析
❝
假设树上一共
n
个节点。
时间复杂度:
- 这里遍历了这棵树,时间复杂度为
O(n)
。
空间复杂度:
- 这里的空间复杂度和递归使用的栈空间有关,这里递归层数不超过
n
,故空间复杂度为O(n)
。
来源:juejin.cn/post/7453489707109531702
30岁普通程序员的薪资应该是多少?
文章首发到公众号:月伴飞鱼,每天分享程序员职场经验+科普AI知识!
大家好呀,我是飞鱼。
古书有云:三十而立,30岁的话,到底达到年薪多少才算人均水平?
经常在脉脉上看到程序员动不动就是月入3万,年入百万,给人以为大多数程序员就是这么高的薪资水平。
❝
其实这些看看就好了,千万不要焦虑,不要觉得怎么别人都这么高工资而我这么低。
虽然网上的信息五花八门,但具体到个人情况还得结合实际情况来看。
- 不同公司的薪酬体系、个人表现以及谈判能力都会影响最终的薪资水平。
程序员的薪资差距巨大,低的几千块也有,高的四五万甚至更高的也有。
学历并非衡量薪资的标准,但高学历的薪资普遍会比低学历要高很多。
不同方向的薪资均有高有低,但普遍会比其他行业要高一些。
大厂员工薪资是真的高没错,周围还有很多其他大厂同事,肯定都不低于 30K 的,更不要说是年龄已经 30+,工作经验 10 年左右了。
❝
但是,大厂员工是少数,大部分人其实就是在小公司摸爬滚打。
他们的薪资基本在 25K 左右,我想这才是大多数普通程序员的薪资。
不过我觉得目前的大环境,失业总是来的猝不及防,还能有班上就已经很不错了,工资高低看运气了。
❝
而且国内99%以上的公司做的都是应用层的开发,根本不涉及底层框架。
- 而应用层的东西本身就不复杂工作3年足以胜任。
换句话说就是工作3年,技术就已经到了天花板。
这也是为什么近年来,很多软件公司裁员先会瞄准大龄程序员的原因。
- 因为他们的工资成本和技术能力完全不成比例,性价比太低。
既然技术天花板不高,工作3年就可以轻易达到。
都说大城市机会多,薪资高, 但互联网行业也很卷啊。
❝
大公司要求高,小公司薪资又不给力。
加班成常态,身体和精神都在极限拉扯。
而且,学历、项目经验、人脉资源,哪个都不能少。
但也有人一路披荆斩棘,成功拿到高薪。
有啥其他看法,欢迎在评论区留言讨论。
❝
想看技术文章的,可以去我的个人网站:hardyfish.top/
- 目前网站的内容足够应付基础面试(
P7
)了!
每日一题
题目描述
❝
给定两个数组
nums1
和nums2
,返回它们的交集。
输出结果中的每个元素一定是 唯一 的。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的
解题思路
❝
定义一个集合,将
nums1
中的元素依次添加入集合当中。
定义
result
数组,大小为两数组中长度的最小值。
定义
index
,遍历nums2
,如果set
中存在nums2
中的元素,将该元素添加到result
数组中。
代码实现
Java
代码:
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
Set<Integer> set = new HashSet<>();
for (int e : nums1) {
set.add(e);
}
int[] result = new int[Math.min(nums1.length, nums2.length)];
int index = 0;
for (int e : nums2) {
if (set.contains(e)) {
result[index++] = e;
set.remove(e);
}
}
return Arrays.copyOfRange(result, 0, index);
}}
资料分享
HBase不睡觉书 带目录(高清)
- 资料链接:url81.ctfile.com/f/57345181-…
- 访问密码:3899
HBase企业应用开发实战:
- 资料链接:url81.ctfile.com/f/57345181-…
- 访问密码:3899
HBase权威指南:
- 资料链接:url81.ctfile.com/f/57345181-…
- 访问密码:3899
来源:juejin.cn/post/7463016078375010323
作为一名程序员,你是如何看待外包的
大家好,我是凌览 。
同样是程序员靠手艺吃饭,为啥外包却是过街老鼠人人喊打,这里我精选了几位网友的回答让我们一起来看看。
第一位网友
其实我觉得,国家应当立法禁止外包驻场。应当规定只有在外包公司所在办公场所工作才能算外包,驻场外包一律必须与目标公司签订劳务合同。否则,驻场外包本质上相当于公司钻劳动法漏洞雇人。
你请外包公司开发软件,给需求给预算给时间给报酬,对方开发了给你验收,这叫外包。没毛病。
你给外包公司钱,人家直接把人派到你办公室,这叫什么玩意的外包?这不就是逃避责任,规避劳动法么?——你是个公司,又不是个人。
当然了,其实确实有些情况,需要不同公司去同一个办公地点合作做项目的,大家觉得说不清楚这与外包的区别。但其实区别还是很明显,区别在于,这些员工遵守谁家的工作制度。
我是A公司人,去客户B那里出差,帮客户B解决问题,这段时间虽然在客户B公司上班,但我不用打他们的卡,不用给他们汇报工作,不用交他们周报,我只对自己公司负责,这是出差,因为我还是A公司员工。
如果我在A公司签订合同然后去B公司工作,由他们(B公司)给分配任务,由他们考核计划完成情况,由他们收我周报,由他们定我KPI,我向他们汇报工作,这性质就完全变了。
这除了劳务关系以外,难道不是实质上B公司员工么?如果允许这样的形态存在,那不就等于是B公司的金蝉脱壳方式规避劳动法么?
所以我的看法是这样:外包可以,B公司写好需求人力时间,签合同,包项目,A公司直接交付最终成果,A公司的员工不受B公司管理,这是外包,这样的外包我觉得很合理。——A公司直接把人派出去给B公司,让B公司管理A公司的人,这不叫外包,这叫买卖人口,这叫A公司帮B公司规避劳动法,这是对外包的侮辱。
第二位网友
我其实一直本着给钱做事的风格,所以外包我并不歧视,直到有一天。
今年我面试了一个外包,行情不好,所以不怎么敢开薪资,比离职前低了一丢丢的样子,喊了12K
甲方面试,问的那一个细,从日常工作到项目数据流,到接口全问了一个遍,还好大差不多,聊了半个多小时。
面试结果是过的。
但是但是面试官和我介绍项目时候就说了,上半个月加班会少一点,可能到8点9点,下半个月可能会到1点2点,偶尔周六还要加班,是一个新项目。
我懂了。
我就问外包公司有加班费吗?他们说没有,只能调休,我算了一下按照面试官介绍,这加班一个月得加班120个小时打底,这没加班费,还只能调休,我直接裂开。
五险一金有,最低的
试用期全薪,这个除了小公司基本上是全的。
剩下啥福利没有,没餐补,没车补,啥也没有。
我直接就拒了。。
第三位网友
外包的活尽量别干,比如培训班入行,或换城市发展着急找工作,或者刚毕业想积累经验,这些情况下可能不得不找外包积累经验,但外包的活尽量别超过2年,干3年都嫌多,原因如下:
- 外包员工的工资会被“折上折”,甲方公司会根据自己同等条件员工的薪资打个折给外包公司,而外包公司会在此基础上再打个折,所以外包的薪资一般是甲方同类员工的6折甚至更低。
- 技术上得不到提升。甲方公司明着可能不说,但在分配重要活的时候,一定是一个正式员工带若干个外包员工,外包员工顶多就调用下api,打打下手,这样干2,3年,接触不到核心技术,而且在组里干久了业务都熟,可能还自我感觉良好。但此时如果出去找工作,真就很难找了。
- 出了问题,会让外包公司顶包。比如在一个项目中,只要外包员工参与的活出了大问题,一般甲方员工顶多就内部批评,外包员工一般就会被“退回原外包公司”。
- 工作环境不好,传说中的“不能吃甲方公司提供的零食”,这真不是空悬来风。
- 丧失信心。外包干久了,逆来顺受惯了,真会认为自己无法去挑战更高级的职位。
- 有一定的风险。比如甲方公司项目组砍预算,优先考虑的是,裁剪外包员工。
总之,甲方人员对外包员工可能真是客客气气的,但在各种工作中,总不免会想,我是甲方,他是外包,也就是说,甲方和外包之间的鸿沟是天然存在的。
来源:juejin.cn/post/7453817457912938505
阿里人的2024年终总结:迷茫而又清晰的一年
阿里人的2024年终总结:迷茫而又清晰的一年
一、引言
好久没有写过外部文章了,突然提笔写 2024
年终总结,还有点不太适应
如果让我给 2024
年做一个总结,那想必是:迷茫而又清晰的一年
还是一样的流程,让时间的车轮带我们走近 2024
的开头,回首我的 2024
年
二、技术
这一年一直在想一个问题,那就是技术到底是什么?
如果技术仅仅是阅读的源码,那么应届生、毕业一年、毕业三年的区别在哪呢?
毕竟,对于源码来说,和工作经验完全没有关系,硬读就可以
见过大一、大二、大三学生均开始阅读源码且进入大公司进行实习的,那我们这种已经有工作经验的相比他们来说,优势到底在哪里呢?
于是,这一年也没有对源码技术投入时间,现在年终回想下这几个问题,有了浅浅的答案
第一点:虽说阅读源码是一件费力不讨好且没有年限限制的事情,但真正能坚持下来持续阅读的人很少。大多数都是眼高手低,觉得人人可阅读,没有门槛,于是选择了鄙视源码,也包括博主自己。
第二点:阅读源码我认为是一个习惯问题,当你养成习惯以后,你做其他的事情也会有所反馈。 当前习惯的养成不止局限于阅读源码,阅读书籍、健身、运动等都可以,一定要养成自律习惯。
第三点:技术不止源码,更多的是整个人的对外体现,或者说你个人标签。 包括但不限:乐观、积极、好学、开朗、集体、协作、沟通、承担等,每一个标签都对应你在公司后续承担的一些事情,比如:你的协作/沟通能力较好,那你大概率会被安排项目经理的职责,但反过来说,协作/沟通能力做到什么地步才能说好,这是一个可研究性较强的问题。
虽然这一年并没有阅读源码,但这一年的技术比之前两年提升的要多
- 架构方面: 在线(Dubbo)、近线(Flink)、离线(ODPS)
- 技术方面: 模型推理、向量检索、词匹配、大模型
- 个人方面: 视野格局、事物认知、团队协作
但这些很多东西自己并没有深入研究或者说没有成为体系,从而导致目前知识都是零碎的
正好也立下2025年的第一个Flag:将所有的技术深入研究沉淀,达成一个体系,做团队或自媒体分享
三、工作
如果说阿里技术影响对于我的冲击力是10分的话,那工作的冲击力就是100分
当我还以上家公司决策引擎为自豪的时候,随之而来的就是同事:我10年前在网易就用这套方案了......
之前整个同盾公司核心工程做的事情,到了阿里这边,就交给一个组来进行实现
业务的复杂度是一方面,阿里更多的强调工作价值性,当你做任何工作的时候,都要想清楚该工作的价值,如果你做的是一个大规模的项目,要随时跟进项目进度。当老板需要了解该项目进度时,要有文档对项目进行支撑。
所以,工作更多的时候不是完成即可,完成只是一个基础,更多的是如何把一件事情做的更好。有可能60分就及格的情况下,大家都要做到90分以上。这种模式对个人能力提升较快,每个人不单是CRUD就完事的,需要有独立思考空间以及项目规划;而反观友商PDD,更多的时候员工只需要执行上层命令,做事情即可,如果这件事情失败了,那就抓紧换一件事情做,两种做法各有优缺点,适合不同的打工人
也通过这篇文章,沉淀下这一年的一些工作总结:
- 做事风格:
- 想到去做、马上去做、做到成功
- 预期做完给谁用,价值衡量的标准是什么,是否量化
- 做事情要深入,研究清楚,不能停留在系统能跑的层面
- 很多小事情,不要看热闹,平时要比别人多想一点、多做一点,自然就脱颖而出了
- 在做一件事情时,不要局限自己的一亩三分地,要让自己建立老板思维,不断要自己向老板思维靠拢
- 能力体现:
- 认知能力:基础技术、技术架构、风控业务、经济学、哲学
- 指标能力:大盘关键指标、维度下指标对比、细节指标表格展现
- 专业能力:专业、专注、务实、分清ROI/优先级、不当螺丝钉、广阔视野、全局视角
- 项目能力:不需要征求多人意见,要有掌控力,权威性,你想要的、你要求的,要明确提出
- 学习能力:人不是从经验中学习,而是从对经验的反思中学习,反思-日常action-刻意提升-换位思考
- 沟通能力:外部合作沟通,务必保持团队内部一致性,凡是换位思考,多从合作方的角度出发,期望什么样的合作体验,所谓自信的来源,是提前做了大量不为人知的准备
- 工作:
- 需求沟通要带着思路去跟别人聊,不能只被动的接受需求输入
- “问题”的解决需要加上事后跟进、追踪的需求记录,这样延展性会更多;有的问题自己解决,有的问题推动他人解决,这也是一种能力
- 其余思考:
- 所谓“水到渠成”,所有你能承担更大责任的,都是提前准备好,不是被安排了某些事情才做到的
- "老中医"理论不可靠,知识获取的视野、主动高效学习的方法、知行合一的实践、上下前后(历史经验)的思考、行动,反复拷打自己,加速曲线,超过"老中医",不成为平庸的普通人
- 我们一定要想明白想成为什么样的人,是做一件事,还是站在更高的角度去看待完整的问题,看待问题的视野和格局,决定了我们的日常行动、结果、面向未来的可能性的差异
同样,集体/团队认同也是最最最重要的一点,有幸能够加入这个团队,贼开心。 团队团建照片:
顺便立下2025年的第二个Flag:在阿里妈妈继续钻研风控业务,将流量与内容线全面掌握并能够成功晋升。
四、运动
之前一直没有专注过运动,来了阿里之后,整个团队氛围喜欢运动,我也参加了很多运动,包括:篮球、羽毛球等,也因为运动认识了一堆朋友
同样,自己因为运动整个人的精神也比之前好了很多
嘿嘿嘿,是不是变化很大,果然大厂是最好的医美工具
这里立下第三个Flag:2025年继续保持篮球、羽毛球运动,同时加入跑步这项运动
五、家庭
今年对象考上了县医院,正式成为了一名在编人士
同时也看了看老家的房子,准备上车还房贷了,不出意外的话,2025年应该就背上房贷了
这里有个重点:我和我亲姐,我们一起选定的一个小区、一个单元,以后爸妈串门就方便了!!!
这里立第四个Flag:希望2025能够买一套属于自己的房子,和对象进行订婚流程~
五、总结
2024年过的真快呀,嗖嗖嗖的就过完了,自己也已经三年半经验了
看着现在刚毕业的年轻人,左手源码、右手算法,压力贼大
同样,感觉自己也挺幸运的,当时在众多 offer 之间选择了阿里妈妈,怀着忐忑的心情来了北京,没想到团队氛围贼好,好的老板、好的师兄、好的同事,还有好的喝酒小团队
自己有时也在想,可能这就是前两年每天认真学习上天的奖励吧
希望2025年能够完成自己所有的Flag并保持每天学习的好习惯~
花有重开日,人无再少年
2025,让我们顶峰相见!
如果你也对 后端架构 和 中间件源码 有兴趣,欢迎添加博主微信:hls1793929520,一起学习,一起成长
我是爱敲代码的小黄,阿里巴巴Java开发工程师,CSDN博客专家,喜欢后端架构和中间件源码。
我们下期再见。
我从清晨走过,也拥抱夜晚的星辰,人生没有捷径,你我皆平凡,你好,陌生人,一起共勉。
来源:juejin.cn/post/7456298133044822042
Redis - 全局ID生成器 RedisIdWorker
概述
定义
:一种分布式系统下用来生成全局唯一 ID 的工具特点
- 唯一性,满足优惠券需要唯一的 ID 标识用于核销
- 高可用,随时能够生成正确的 ID
- 高性能,生成 ID 的速度很快
- 递增性,生成的 ID 是逐渐变大的,有利于数据库形成索引
- 安全性,生成的 ID 无明显规律,可以避免间接泄露信息
- 生成量大,可满足优惠券订单数据量大的需求
ID 组成部分
- 符号位:1bit,永远为0
- 时间戳:31bit,以秒为单位,可以使用69年
- 序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID
代码实现
目标
:手动实现一个简单的全局 ID 生成器实现流程
- 创建生成器:在 utils 包下创建 RedisIdWorker 类,作为 ID 生成器
- 创建时间戳:创建一个时间戳,即 RedisId 的高32位
- 获取当前日期:创建当前日期对象 date,用于自增 id 的生成
- count:设置 Id 格式,保证 Id 严格自增长
- 拼接 Id 并将其返回
代码实现
@Component
public class RedisIdWorker {
// 开始时间戳
private static final long BEGIN_TIMESTAMP = 1640995200L;
// 序列号的位数
private static final int COUNT_BITS = 32;
private StringRedisTemplate stringRedisTemplate;
public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
// 获取下一个自动生成的 id
public long nextId(String keyPrefix){
// 1.生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
// 3.获取当前日期
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
// 4.获取自增长值:生成一个递增计数值。每次调用 increment 方法时,它会在这个key之前的自增值的基础上+1(第一次为0)
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
// 5.拼接并返回
return timestamp << COUNT_BITS | count;
}
}
测试
一、CountDownLatch 工具类
定义
:一个同步工具类,用于协调多个线程的等待与唤醒功能
:
- 控制多个线程的执行顺序和同步
- 确保主线程在所有子线程完成后才继续执行
- 防止主线程过早结束导致子线程执行状态丢失
常用方法
:
- await:用于主线程的阻塞方法,使其阻塞等待直到计数器归零
- countDown:用于子线程的计数方法,使计数器递减
二、ExecutorService & Executors
定义
:Java 提供的线程池管理接口功能
:
- 简化异步任务的执行管理
- 提供有关 “线程池” 和 “任务执行” 的标准 API
常用方法
方法 说明 Executors.newFixedThreadPool(xxxThreads) Executors 提供的工厂方法,用于创建 ExecutorService 实例 execute(functionName) 调用线程执行 functionName 任务,无返回值 ⭐ submit(functionName) 调用线程执行 functionName 任务,返回一个 Future 类 invokeAny(functionName) 调用线程执行一组 functionName 任务,返回首成功执行的任务的结果 invokeAll(functionName) 调用线程执行一组 functionName 任务,返回所有任务执行的结果 ⭐ shutdown() 停止接受新任务,并在所有正在运行的线程完成当前工作后关闭 ⭐ awaitTermination() 停止接受新任务,在指定时间内等待所有任务完成
- 参考资料:一文秒懂 Java ExecutorService
- 代码实现
- 目标:测试 redisIdWorker 在高并发场景下的表现(共生成 30000 个 id)
private ExecutorService es = Executors.newFixedThreadPool(500); // 创建一个含有 500 个线程的线程池
@Test
void testIdWorker() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(300); // 定义一个工具类,统计线程执行300次task的进度
// 创建函数,供线程执行
Runnable task = () -> {
for(int i = 0; i < 100; i ++) {
long id = redisIdWorker.nextId("order");
System.out.println("id = " + id);
}
latch.countDown();
}
long begin = System.currentTimeMillis();
for( int i = 0; i < 300 ; i ++) {
es.submit(task);
}
latch.await(); // 主线程等待,直到 CountDownLatch 的计数归
long end = System.currentTimeMillis();
System.out.println("time = " + (end - begin)); // 打印任务执行的总耗时
}
超卖问题
- 目标:通过数据库的 SQL 语句直接实现库存扣减(存在超卖问题)
一、乐观锁
定义
:一种并发控制机制,不使用数据库锁,而是在更新时通过版本号或条件判断来确保数据一致性优点
:并发性能高,不会产生死锁,适合读多写少的场景实现方式
:CAS (Compare and Swap) - 比较并交换操作实现示例
(基于版本号的乐观锁)
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1, version = version + 1")
.eq("voucher_id", voucherId)
.eq("version", version)
.gt("stock", 0)
.update();
分布式环境的局限性
- **原子性问题:**多个线程同时检查库存并更新时,可能导致超卖。这是因为检查和更新操作不是原子的
- **事务隔离:**在默认的"读已提交"隔离级别下,分布式环境中的多个节点可能读取到不一致的数据状态
- **分布式一致性:**在分布式环境中,不同的应用服务器可能同时操作数据库,而数据库层本身并不能感知跨服务器的事务一致性
二、悲观锁
定义
:一种并发控制机制,通过添加同步锁强制线程串行执行优点
:实现简单,可以确保数据一致性缺点
:由于串行执行导致性能较低,不适合高并发场景事务隔离级别
:读已提交及以上实现方法
:使用 SQL 的 forUpdate() 子句,可以在查询时锁定选中的数据行。被锁定的行在当前事务提交或回滚前,其他事务无法对其进行修改或读取
三、事务隔离级别
- 定义:数据库事务并发执行时的隔离程度,用于解决并发事务可能带来的问题
- 优点:可以防止脏读、不可重复读和幻读等并发问题
- 缺点:隔离级别越高,并发性能越低
- 实现方法:
- 读未提交(Read Uncommitted):允许读取未提交的数据
- 读已提交(Read Committed):只允许读取已提交的数据
- 可重复读(Repeatable Read):在同一事务中多次读取同样数据的结果是一致的
- 串行化(Serializable):最高隔离级别,完全串行化执行
一人一单问题
一、单服务器系统解决方案
需求
:每个人只能抢购一张大额优惠券,避免同一用户购买多张优惠券重点
- 事务:库存扣减操作必须在事务中执行
- 粒度:事务粒度必须够小,避免影响性能
- 锁:事务开启时必须确保拿到当前下单用户的订单,并依据用户 Id 加锁
- 找到事务的代理对象,避免 Spring 事务注解失效 (需要给启动类加
@EnableAspectJAutoProxy(exposeProxy = true)
注解)
实现逻辑
- 获取优惠券 id、当前登录用户 id
- 查询数据库的优惠券表(voucher_order)
- 如果存在优惠券 id 和当前登录用户 id 都匹配的 order 则拒绝创建订单,返回 fail()
- 如果不存在则创建订单 voucherOrder 并保存至 voucher_order 表中,返回 ok()
二、分布式系统解决方案 (通过 Lua 脚本保证原子性)
一、优惠券下单逻辑
二、代码实现 (Lua脚本)
--1. 参数列表
--1.1. 优惠券id
local voucherId = ARGV[1]
--1.2. 用户id
local userId = ARGV[2]
--1.3. 订单id
local orderId = ARGV[3]
--2. 数据key
--2.1. 库存key
local stockKey = 'seckill:stock:' .. voucherId
--2.2. 订单key
local orderKey = 'seckill:order' .. voucherId
--3. 脚本业务
--3.1. 判断库存是否充足 get stockKey
if( tonumber( redis.call('get', stockKey) ) <= 0 ) then
return 1
end
--3.2. 判断用户是否下单 SISMEMBER orderKey userId
if( redis.call( 'sismember', orderKey, userId ) == 1 ) then
return 2
end
--3.4 扣库存: stockKey 的库存 -1
redis.call( 'incrby', stockKey, -1 )
--3.5 下单(保存用户): orderKey 集合中添加 userId
redis.call( 'sadd', orderKey, userId )
-- 3.6. 发送消息到队列中
redis.call( 'xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId )
三、加载 Lua 脚本
RedisScript 接口
:用于绑定一个具体的 Lua 脚本DefaultRedisScript 实现类
- 定义:RedisScript 接口的实现类
- 功能:提前加载 Lua 脚本
- 示例
// 创建Lua脚本对象
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
// Lua脚本初始化 (通过静态代码块)
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("/path/to/lua_script.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
四、执行 Lua 脚本
调用Lua脚本 API
:StringRedisTemplate.execute( RedisScript script, List keys, Object… args )示例
- 执行 ”下单脚本” (此时不需要 key,因为下单时只需要用 userId 和 voucherId 查询是否有锁)
Long result = stringRedisTemplate.execute(
SECKILL_SCRIPT, // 要执行的脚本
Collections.emptyList(), // KEY
voucherId.toString(), userId.toString(), String.valueOf(orderId) // VALUES
);
- 执行 “unlock脚本”
- 执行 ”下单脚本” (此时不需要 key,因为下单时只需要用 userId 和 voucherId 查询是否有锁)
实战:添加优惠券 & 单服务器创建订单
添加优惠券
目标
:商家在主页上添加一个优惠券的抢购链接,可以点击抢购按钮抢购优惠券
一、普通优惠券
定义
:日常可获取的资源代码实现
@PostMapping
public Result addVoucher(@RequestBody Voucher voucher) {
voucherService.save(voucher);
return Result.ok(voucher.getId());
}
二、限量优惠券
定义
:限制数量,需要设置时间限制、面对高并发请求的资源下单流程
- 查询优惠券:通过 voucherId 查询优惠券
- 时间判断:判断是否在抢购优惠券的固定时间范围内
- 库存判断:判断优惠券库存是否 ≥ 1
- 扣减库存
- 创建订单:创建订单 VoucherOrder 对象,指定订单号,指定全局唯一 voucherId,指定用户 id
- 保存订单:保存订单到数据库
- 返回结果:Result.ok(orderId)
代码实现
- VoucherController
@PostMapping("seckill")
public Result addSeckillVoucher( @RequestBody Voucher voucher ){
voucherService.addSeckillVoucher(voucher);
return Result.o(voucher.getId());
}
- VoucherServiceImpl
@Override
@Transactional
public void addSeckillVoucher(Voucher voucher) {
// 保存优惠券到数据库
save(voucher);
// 保存优惠券信息
SeckillVoucher seckillVoucher = new SeckillVoucher();
seckillVoucher.setVoucherId(voucher.getId());
seckillVoucher.setStock(voucher.getStock());
seckillVoucher.setBeginTime(voucher.getBeginTime());
seckillVoucher.setEndTime(voucher.getEndTime());
seckillVoucherService.save(seckillVoucher);
// 保存优惠券到Redis中
stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
}
- VoucherController
(缺陷) 优惠券下单功能
一、功能说明
目标
:用户抢购代金券,保证用户成功获得优惠券,保证效率并且避免超卖工作流程
- 提交优惠券 ID
- 查询优惠券信息 (下单时间是否合法,下单时库存是否充足)
- 扣减库存,创建订单
- 返回订单 ID
四、代码实现
- VoucherOrderServiceImpl (下述代码在分布式环境下仍然存在超卖问题)
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService{
@Resource
private ISeckillVoucherService seckillVoucherService;
@Override
public Result seckillVoucher(Long voucherId) {
// 查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 优惠券抢购时间判断
if(voucher.getBeginTime().isAfter(LocalDateTime.now) || voucher.getEndTime().isBefore(LocalDateTime.now()){
return Result.fail("当前不在抢购时间!");
}
// 库存判断
if(voucher.getStock() < 1){
return Result.fail("库存不足!");
}
// !!! 实现一人一单功能 !!!
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
}
@Transactional
public Result createVoucherOrder(Long userId) {
Long userId = UserHolder.getUser().getId();
// 查询当前用户是否已经购买过优惠券
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if( count > 0 ) {
return Result.fail("当前用户不可重复购买!");
// !!! 实现乐观锁 !!!
// 扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") // set stock = stock - 1;
.eq("voucher_id", voucherId).gt("stock", 0) // where voucher_id = voucherId and stock > 0;
.update();
if(!success) {
return Result.fail("库存不足!");
}
// 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setId(redisIdWorker.nextId("order"));
voucherOrder.setUserId(UserHolder.getUser().getId());
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
// 返回订单id
return Result.ok(orderId);
}
来源:juejin.cn/post/7448119568567189530
这五年,我学这么多东西再没有高学历背景下,有没有意义
今年的10月初我被辞退了,公司为盈利,部门整体裁掉,感慨挺多的。也是我工作以来待的最久的一家单位了,从原来的菜鸟到现在能算是合格的前端工程师,不管怎么说,我很感谢这个平台。
被辞退以现在的环境制定要面临降薪,或者换个没有福利待遇那么好的单位。
这五年我得到的太多了,可能有人说如果你在别的单位应该也会得到你应得的,这个说法确实没毛病。我只不过是一个打工仔,平台不过是带我见识了一些上限,如果我不愿意去学,可能也收获不了这么多。那么就从生活、技术等等方面展开来说下我得到了什么。
生活
刚入职这家公司没多久(2019年),我就跟我老婆处对象了。是的我们在2023年10月4号结婚了,也很顺利我们在12月份接着接到喜讯我们有宝宝了。宝宝出生在2024年10月3日(女宝),很漂亮,大眼萌娃。照片我放下面给各位叔叔姨姨们看看:
买房:没逃得了当房奴(被逼的),在2023年8月份我们在南京市浦口桥北买了个小三室,总价150W
。首付了70W
,贷款80W
,20年,每个月要还5700
左右。不好的是今年房子跌了好多钱。我们小区同户型的已经有挂120+
的了,成交价更低。
之前有个同事大哥说过,如果你买了房子,能娶妻生子,那么房子的价值就够了,它跌就跌吧。落了个媳妇+孩子赚大了。
这里给还没有买房的朋友一些建议:贷款要贷30
年(前期压力小,后面可以提前还款), 买毛坯的话一定要预留好装修资金,其实我不建议买毛坯,那个时候你就会要负担房贷+房租+装修(我就是个例子:后悔si了😭),还要晾着一年半载的才能入住,最好能买个二手房直接能入住的。
我这几年在这家公司待遇也涨幅了几次:
第一次是2020年10月份
,觉得自己还凑合,想去外面看看,当时确实环境大好,也拿到了不错的涨幅。后来就是我当时部门的前端领导全力留我,觉得我很负责,提出给我涨幅3.5k
。
第二次是2022年3月份
,也是我这个前端领导要换平台,不在这个部门带领我们了。我也想出去看看😁,结果是部门领导留我了涨幅4k
。
第三次是2023年12月份
,我把我们的一个用户后台管理(老旧难用)在一次迭代中,用时7天把14个模块整体重构(公司框架+公司UI组件),就是一天写两个模块,然后交给测试同学同步测试,第二天把bug和两个新模块同步修改好继续提测。7天后上线,新的web页很好用(都是老的web衬托的),得到了部门领导的主动涨薪1.5k
。
可以说我运气好,也可以说我遇到了好领导,我可能是有点价值的,但是我确实是幸运的。
技术
- (2019)、校招生好厉害
高校的校招生竟然这么厉害,这是我在这家单位接触校招生或者说应届生的真实感受。 也是他们本身基础就很扎实,有编程思想,有领导带领规划成长路线,又很积极爱学确实成长很快。
我开始了重新学了下ES6
,买了一本阮一峰老师的《ES6标准入门》,来回看了3遍,深入理解了解构
,数组的some、every
, 字符串的:startsWith、endsWith、padStart、padEnd
, Set、Map
,Promise
等等。我觉得它帮助我特别大,如果没有这本书我或许会看到一些同事写的语法,我不认识的语法。为我的js奠定了一定的基础。
- (2020)、同事竟然手写正则
- 再一次跟同事协作中需要检验一个ip段,他竟然给我直接手写手写,大佬真真厉害。然后我就去重新学习了正则表达式基本能达到写一些简单的表示式。
- (2020)、前端领导让我看看
eggjs
,了解怎么的用法,后面需要跟我做一个东西
- 在这个里面学到了很多关于
node
的相关知识。
- (2020)、前端大屏大佬,我们有一个专门写大屏的前端大佬
- 跟着他学到了怎么做适配,
rem、em、vw、postCss
,使用echarts
,并认识了d3js
(后面刚好会用到,会更深入了解)
- (2020)、前端领导让我学习
nginx
,起个服务3000
端口的时候能访问到百度的页面
- 这里用到了转发服务以及代理资源,让我在后面对前端资源处理以及处理代理问题时有了很大的帮助,我搭建博客等好多地方都用到了。
- (2020)、前端领导让我用谷歌插件开发一个chrome插件,主要是 读取数据(标签页中的某个窗口)到周报生成渲染。
其实到这里我都没有学习编码规范,对组件设计的思想也了解不多,基本上就是野蛮开荒。本来一直计划给我做代码
codereview
,看看怎么能帮我做些规范的提升,一直没太多机会。后来就是他自身发展走了去了别的单位,然后换了个空降领导,他开始组织我们创建各种规范拦截,eslint
,git
提交拦截。
- (2021)、定义相关规范拦截
eslint、git
eslint
: 用的airbnb的规范。还是蛮严格的帮助我纠正了不少代码缺点。git
:commit
规范,分支规范等等。
- (2021)、阅读库源码提升自己的代码规范。我觉得对我帮助最大的是
element-ui
的源码
2021 年我觉得我最大的进步就是代码质量有了大幅度提升,懂的怎么设计组件了,怎么能写让代码更壮健,懂的了规范带来的价值。
- (2022)、我们开始放弃了
jenkins
做打包构建,换成了gitLab CI CD
, 我深入了解并学习了下。 - (2022)、前【前端领导】让我协助他开发个微信小程序,有了解到了小程序的相关知识。
- (2022)、我们开始做一个图谱产品,拓扑图可视化分析,使用了
d3js
以及canvas
- 为了支持大数据的统计以及绘制,在这两年中一直在做数据结构的优化以及接触
web Worker、Wasm
等,只在可视区域渲染等等优化策略。 算是学到了性能的优化手段,也了解到了图形算法的魅力。
- (2022)、学习
TS
,写了写react Hooks
。也了解到了跟vue之前的区别 - (2022)、我开始研究怎么生成脚手架,发布
NPM
包。 - (2023)、这年我开始对我们公司平台组的产物做源码学习,学到了
babel AST
、webpack
插件编写等等,还写了个vscode
的插件主要是处理AST
的达到函数插桩。 - (2023)、这年我开始对之前用到的知识做了总结发表一系列文章(得到了掘金Lv5优秀创作者)。
- (2023)、我持续查阅那些好的插件包的源码具体怎么实现的,对编码思想又了更深层次的理解。懂得了怎么做能更好的建立一个可持续发展的方向,技术选型,风险评估等等。
- (2024)、这年精力主要是在工作上(大环境太不好了,不想被裁,也算是坚持到了最后),开发大屏,重构了好多老的代码,对产品的用户体验做到了细致的优化。
我学这么多东西有在没有高学历背景下,有没有意义?
实话说我在这次找工作中,对自己的一直努力的方向做了怀疑,还不如好好享受,学习这么多干啥呢? 约面试会被卡在学历上,我很烦恼,有些怀疑自己。我之前在环境大好的背景下,我始终认为多学习,一定能涨工资。
我也会向我老婆吐槽说学这么多又有啥用呢,第一步就被卡死了。是不是统招本科!!!抱歉我不是。
她说你比你同学朋友们可能要好多了,他们一年、两年可能就会换个工作,在者说口罩那几年你一直也算很稳定,别人都换了好几家单位了,你知道为啥一直再给你加薪吗?其实都是你努力得来的! 工作嘛,咱们换个就行了,现在大环境不好,不好找是常态,她说没关系的,她还有存款(彩礼钱+自己之前攒下的),你就是一两年不上班都没问题。
是啊!我一直在稳步加薪,待遇在朋友中也还算不错的
没上班怎么可能不焦虑,我是刚好我家孩子出生那个月被裁的,我还等到了小孩20天后才开始找工作,那个时候父母还都在这边,我对双方父母说我是在休产假+年假,在出去面试那几天也是说出去给我闺女办理正件
后来因为孩子要喝奶粉 + 房贷 + 房租 + 装修,其实我很大的压力,老婆虽然没催我给我压力,但是每天还是很焦虑的,有的时候会算我家娃一个月花费多少... 唉!!
一切都是有意义
我有掘金社区优秀创作者,加我的个人博客网站,再加上我会的技术,在加上我还有发表的各类插件,写在简历上都是我的优势,我会刻意去找技术面试官去投简历,我会抓住每次面试机会,努力的去展示自己。
好消息我用时14天收到offer了,一家小单位,双休,待遇降了20%。我知道现在的行情,就是我在多花一个月精力去面试,去找,我可能会更奔溃(最近压力很大),最后的待遇应该也会跟现在这个的大差不差的。 现在都是一个萝卜一个坑,我果断选择入职。
回头看其实我由于我的那些优势,我算约到了不少面试,我总共面试6家,二面了2家,我跟我同时被裁的前端同事聊,他说用时两个月才约到2家面试。我运气真的好的太多了
还有个好消息就是前【领导】11月初找我做一个项目,持续7个月,让我兼职每天干2-4个小时,每个月给我1W。由于我现在通勤+加班,可能没太多精力去做,然后就介绍给我的同事阿祖全职,给了他1.8W。
其实前领导能找到我也是认可我的工作能力,我其实也后悔推荐给朋友了,我应该找他一起兼职这份工作,我俩平分这个1w,毕竟我刚降薪加实习期80%,落差不少。
11月中的时候又有前单位的UI设计师(他很认可我,每次都说我写页面还原度是最高的)找上我说要不要考虑私活报价1.2W
,做一个网站+h5,其实1.2w我俩平分不算特别高,但是最后没能接下来。
12月下我前前【领导】来找我说他们单位可能要裁员,然后呢他搞了这么多年不想在打工了,想着做做产品。他好像看中小程序的市场,他也有些人脉。我其实在第一份兼职1w的没接的时候我也考虑做什么产品化的东西能卖钱。小程序的市场看到某宝好多个卖模版的,还老便宜了,加上我也没有什么人脉就放弃了。这次跟着它们用业余时间搞搞试试。用雷布斯的一句话:我们悄悄搞,没搞成,就当我们没有搞过!!。雄起!!!!
接下来的计划
- 产品化的东西让他们计划,只是定了个初步目标,还暂未执行!
- 攒钱买个小车(10w),有孩子了没车的话出行不是很方便
- 我老婆在做xiao红书,做母婴方面的,最近粉丝也达到了600个🔥,继续加油
- 我在斗音上拍我闺女视频,但是没太多流量,刚刚投了150块钱,得到了40多个粉丝,😭,继续加油吧!!!
- 我在学习
Java
,主要是要是做点什么写后端能方便点 - 搞钱!!!!!!!!
来源:juejin.cn/post/7453120781771341859
2024年,30岁前最后一次年度思考
没错!95年,还剩几个月就奔三了。2024年,注定是人生中意义非凡的一年,忐忑、裁员、出书、求职、转正这几个词贯穿了一整年。
忐忑
在上一家公司时,我从面试开始和到入职半年转正后,其实内心对于公司的状况一直保持一种忐忑不安的心情,这种感觉跟我老婆说过几次,我们一致认为应当有心理准备。原因在于薪资与公司的组织架构、基础建设、日常工作量安排和人员扩充速度都让人感到迷惑。
公司是在一个包括高层话事人不断更换,高层(副总裁)突然接受停止调查;技术部门仅仅作为辅助,技术氛围低沉,基建缺失,直属leader作用甚微;工作量与人员匹配失常,人多活少,尽管如此年初还在不断扩招中,泡沫感极强,伴随着薪酬发放日漂浮不定,每到月底像是在开盲盒,你永远不知道银彳亍卡何时会有一笔款到账。
裁员
一系列薪酬制度改革和薪酬拖欠不得不怀疑高层战略的正确性,直到四月某一天CTO私聊我,泡沫破裂,裁员尘埃落定。
我被归属于第一批裁员名单中,与CTO交谈中,似乎也流露一丝对高层决策的不满,但没有明说,给我的理由是当前工作任务都很简单,匹配不了我的能力,所以给了我一个名额。
这放在当时听上去有些许意外,但我接受了这种措辞,并不是因为CTO说了几句好听的话,更多是我作为一个技术人的直觉认为这个CTO靠谱。离职过程中对人事提出的补偿计算方式以及分期发放,我都拒绝了,最后经过与人事反复讨论之后拿到了补偿,少不了他的协助,所以内心表示感谢。从现在的视角看来,似乎是他已经意料到公司的发展趋势,以致于后来被裁员的人有很大一部分都没有赔偿。
出书
离职后我在家休息了一个月,期间也为了帮一个粉丝忙,接手了他工作的一部分任务,主要是做游戏业务的动画。期间有被一个后端恶心到,业务不熟悉,接口一直不通就算了,关键还理直气壮说是前端问题;我佩服那个粉丝能够忍气吞声这么久,换做其他人也很难不高血压,为此特意发圈宣泄。
由于后端提供的接口迟迟不通,需求没有预期上线,为此他们老板还大发雷霆,最后把锅推给了这个前端粉丝,声称把他给炒了。没过一个月,粉丝的这个公司被帽子叔叔查封,业务涉及到了灰产,老板和负责人进去了。员工的工资都没发,但我的报酬是因为签了合约,在deadline之前要求他们打款,对我没有影响,这是苦了这个粉丝。
在此之后我便全职写书,《NestJS全栈开发解析:快速上手与实践》 这本书临近结尾,我一鼓作气完成了并在5.1号劳动节那天交稿;写书的想法也有一部分是来源于CTO的启发,后面图书审阅也是找了CTO帮忙,熬夜帮我看完并给了这个评语,为此我很感谢他。
经过几个月的审批和改稿,图书在9月份正式发布了各大平台,这是一件值得高兴的事情。
而对于前司的后续,据说后面还搬到一个CBD进行办公,但当时员工已经欠薪几个月,以至于到年底,公司被迫全员原地解散,很遗憾这不是一个好结果。
求职
交稿完成后,花了一个月左右时间求职,拿到了3个offer,最后选择了去深圳的美图,这是凭借NestJS的图书写作获得的一个岗位。之后由于组织架构变化,我在转正前夕面临选择继续从事Node全栈还是Go语言开发,考虑一番后我选择了后者,顺利转到了后端架构组,负责go语言开发,这对我来说又是一个新的尝试和挑战,我选择了这种变化,与框架和语言无关,只不过是践行我的人生哲学:【不断变化】,让自己处于一种长期乐观、短期痛苦、当下快乐的舒适区边缘中。
觉醒
关于成长,过去我一直不喜欢看历史,或许归根于上学时代对于历史学科的厌倦,没看过基本历史文献。2024年底,我看了教员的《毛选》、《实践论》、《矛盾论》、《寻乌调查》,第一种感受是成功绝不是偶然,环环相扣的逻辑能力令人惊叹。我想这些书籍回答了我一直以来的问题:
如何成为一个独立、深度思考的人?
我们人生中做了一个坏的决定,在股市中选择了不争气的股票,最坏的结果无非是让自己从头再来。但革命不同,选择错了就有可能让整个民族处于被毁灭的境地中,每一步都步履蹒跚,这该有怎样的智慧与思维?
第二种感受是遗憾没有早点开悟,在临近30岁时才开始阅读这些书籍,当然也很庆幸没有太晚,一切都来得及!
特别的是,《寻乌调查》报告里面的细节,应该是我人生中读过的一本最详细的一本书籍,里面还记载了寻乌与我老家(兴宁)相关的历史宜了,没有一句多余的,都是干货。第一次感受原来伟人离我这么近。
教员做了这个调查报告之后,便留下一句千古格言:没有调查,就没有发言权!反观自身,何尝不是应该这样呢?
关于家庭,今年整个过程中家里的大大小小的事基本上都是我老婆操办,为我们的小家默默付出了很多,加上我去了深圳之后,我的衣食住大部分也是她来打理,一个人照顾小孩,现在甜筒一岁半了,如我们所愿健康成长,这隶属她的功劳。
一个家庭要想变好,靠一个人努力不行,需要“拉拢”有能力的人一起,话事人脑子要清醒,能够明辨是非,唯唯诺诺绝对是会出问题的。
一个家族要想变好,靠一两个人不行,得靠一两个家庭真正向好,大家庭才会有希望。
最后,没有Flag,年度总结中对未来进行遐想没有意义,沉浸于自己完成所有Todo List的那种兴奋是虚构的,而实践中那种痛苦、无助才是我们最真实的感受,人不能总活在无限遐想的递归当中
。
我看过那些在新年Flag列举诸多愿望,买了一堆书籍想要读完的,来年能真正落地完成的少之又少,毕竟我亦如此。
2025年,爱自己,爱家人,步步为营,不负将来!祝所有支持我的粉丝朋友们,一切如意,事业感情双丰收~
来源:juejin.cn/post/7455282891535302708
虾皮开的很高,还有签字费。
大家好,我是二哥呀。
虾皮在去年之前,还是很多大厂人外逃的首选项,因为总部在新加坡,比较有外企范,但去年就突然急转直下,队伍收紧了不少。
作为东南亚电商市场的领头羊,市场覆盖了新加坡、马来西亚、泰国、菲律宾、印尼、越南等地,目前也开始进军巴西和墨西哥等新兴市场。
我从 offershow 上也统计了一波 25 届虾皮目前开出来的薪资状况,方便大家做个参考。
- 本科 985,后端岗,给了 32k,还有 5 万签字费,自己硬 A 出来的,15 天年假,base 上海,早 9.30 晚 7 点
- 硕士双一流,后端给了 40 万年包,但已经签了其他的三方,拒了,11 月 31 日下午开的
- 硕士 985,后端开发,给到了 23k,白菜价,主要面试的时候表现太差了
- 硕士海归,后端开发给了 26.5k,还有三万签字费,咩别的高,就释放了
- 硕士211,测试岗,只给了 21k,还有 3 万年终奖,但拒了
从目前统计到的情况来看,虾皮其实还蛮舍得给钱的,似乎有点超出了外界对他的期待。但很多同学因为去年的情况,虾皮只能拿来做备胎,不太敢去。
从虾皮母公司 Sea 发布的2024 年第三季度财报来看,电子商务(主要是 Shopee)收入增长了 42.6%,达到了 31.8 亿美元,均超预期。
总之,希望能尽快扭转颓势吧,这样学 Java 的小伙伴也可以有更多的选择。
那接下来,我们就以 Java 面试指南中收录的虾皮面经同学 13 一面为例,来看看下面的面试难度,自己是否有一战之力。
虾皮面经同学 13 一面
tcp为什么是可靠的
TCP 首先通过三次握手和四次挥手来保证连接的可靠性,然后通过校验和、序列号、确认应答、超时重传、滑动窗口等机制来保证数据的可靠传输。
①、校验和:TCP 报文段包括一个校验和字段,用于检测报文段在传输过程中的变化。如果接收方检测到校验和错误,就会丢弃这个报文段。
推荐阅读:TCP 校验和计算方法
②、序列号/确认机制:TCP 将数据分成多个小段,每段数据都有唯一的序列号,以确保数据包的顺序传输和完整性。同时,发送方如果没有收到接收方的确认应答,会重传数据。
③、流量控制:接收方会发送窗口大小告诉发送方它的接收能力。发送方会根据窗口大小调整发送速度,避免网络拥塞。
④、超时重传:如果发送方发送的数据包超过了最大生存时间,接收方还没有收到,发送方会重传数据包以保证丢失数据重新传输。
⑤、拥塞控制:TCP 会采用慢启动的策略,一开始发的少,然后逐步增加,当检测到网络拥塞时,会降低发送速率。在网络拥塞缓解后,传输速率也会自动恢复。
http的get和post区别
GET 请求主要用于获取数据,参数附加在 URL 中,存在长度限制,且容易被浏览器缓存,有安全风险;而 POST 请求用于提交数据,参数放在请求体中,适合提交大量或敏感的数据。
另外,GET 请求是幂等的,多次请求不会改变服务器状态;而 POST 请求不是幂等的,可能对服务器数据有影响。
https使用过吗 怎么保证安全
HTTP 是明文传输的,存在数据窃听、数据篡改和身份伪造等问题。而 HTTPS 通过引入 SSL/TLS,解决了这些问题。
SSL/TLS 在加密过程中涉及到了两种类型的加密方法:
- 非对称加密:服务器向客户端发送公钥,然后客户端用公钥加密自己的随机密钥,也就是会话密钥,发送给服务器,服务器用私钥解密,得到会话密钥。
- 对称加密:双方用会话密钥加密通信内容。
客户端会通过数字证书来验证服务器的身份,数字证书由 CA 签发,包含了服务器的公钥、证书的颁发机构、证书的有效期等。
https能不能抓包
可以,HTTPS 可以抓包,但因为通信内容是加密的,需要解密后才能查看。
其原理是通过一个中间人,伪造服务器证书,并取得客户端的信任,然后将客户端的请求转发给服务器,将服务器的响应转发给客户端,完成中间人攻击。
常用的抓包工具有 Wireshark、Fiddler、Charles 等。
threadlocal 原理 怎么避免垃圾回收?
ThreadLocal 的实现原理就是,每个线程维护一个 Map,key 为 ThreadLocal 对象,value 为想要实现线程隔离的对象。
1、当需要存线程隔离的对象时,通过 ThreadLocal 的 set 方法将对象存入 Map 中。
2、当需要取线程隔离的对象时,通过 ThreadLocal 的 get 方法从 Map 中取出对象。
3、Map 的大小由 ThreadLocal 对象的多少决定。
通常情况下,随着线程 Thread 的结束,其内部的 ThreadLocalMap 也会被回收,从而避免了内存泄漏。
但如果一个线程一直在运行,并且其 ThreadLocalMap
中的 Entry.value 一直指向某个强引用对象,那么这个对象就不会被回收,从而导致内存泄漏。当 Entry 非常多时,可能就会引发更严重的内存溢出问题。
使用完 ThreadLocal 后,及时调用 remove()
方法释放内存空间。remove()
方法会将当前线程的 ThreadLocalMap 中的所有 key 为 null 的 Entry 全部清除,这样就能避免内存泄漏问题。
mysql慢查询
慢 SQL 也就是执行时间较长的 SQL 语句,MySQL 中 long_query_time 默认值是 10 秒,也就是执行时间超过 10 秒的 SQL 语句会被记录到慢查询日志中。
可通过 show variables like 'long_query_time';
查看当前的 long_query_time 值。
不过,生产环境中,10 秒太久了,超过 1 秒的都可以认为是慢 SQL 了。
mysql事务隔离级别
事务的隔离级别定了一个事务可能受其他事务影响的程度,MySQL 支持的四种隔离级别分别是:读未提交、读已提交、可重复读和串行化。
遇到过mysql死锁或者数据不安全吗
有,一次典型的场景是在技术派项目中,两个事务分别更新两张表,但是更新顺序不一致,导致了死锁。
-- 创建表/插入数据
CREATE TABLE account (
id INT AUTO_INCREMENT PRIMARY KEY,
balance INT NOT NULL
);
INSERT INTO account (balance) VALUES (100), (200);
-- 事务 1
START TRANSACTION;
-- 锁住 id=1 的行
UPDATE account SET balance = balance - 10 WHERE id = 1;
-- 等待锁住 id=2 的行(事务 2 已锁住)
UPDATE account SET balance = balance + 10 WHERE id = 2;
-- 事务 2
START TRANSACTION;
-- 锁住 id=2 的行
UPDATE account SET balance = balance - 10 WHERE id = 2;
-- 等待锁住 id=1 的行(事务 1 已锁住)
UPDATE account SET balance = balance + 10 WHERE id = 1;
两个事务访问相同的资源,但是访问顺序不同,导致了死锁。
解决方法:
第一步,使用 SHOW ENGINE INNODB STATUS\G;
查看死锁信息。
第二步,调整事务的资源访问顺序,保持一致。
怎么解决依赖冲突的
比如在一个项目中,Spring Boot 和其他库对 Jackson 的版本有不同要求,导致序列化和反序列化功能出错。
这时候,可以先使用 mvn dependency:tree分析依赖树,找到冲突;然后在 dependencyManagement 中强制统一 Jackson 版本,或者在传递依赖中使用 exclusion 排除不需要的版本。
spring事务
在 Spring 中,事务管理可以分为两大类:声明式事务管理和编程式事务管理。
编程式事务可以使用 TransactionTemplate 和 PlatformTransactionManager 来实现,需要显式执行事务。允许我们在代码中直接控制事务的边界,通过编程方式明确指定事务的开始、提交和回滚。
声明式事务是建立在 AOP 之上的。其本质是通过 AOP 功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前启动一个事务,在目标方法执行完之后根据执行情况提交或者回滚事务。
相比较编程式事务,优点是不需要在业务逻辑代码中掺杂事务管理的代码,Spring 推荐通过 @Transactional 注解的方式来实现声明式事务管理,也是日常开发中最常用的。
常见的linux命令
我自己常用的 Linux 命令有 top 查看系统资源、ps 查看进程、netstat 查看网络连接、ping 测试网络连通性、find 查找文件、chmod 修改文件权限、kill 终止进程、df 查看磁盘空间、free 查看内存使用、service 启动服务、mkdir 创建目录、rm 删除文件、rmdir 删除目录、cp 复制文件、mv 移动文件、zip 压缩文件、unzip 解压文件等等这些。
git命令
git clone <repository-url>
:克隆远程仓库。git status
:查看工作区和暂存区的状态。git add <file>
:将文件添加到暂存区。git commit -m "message"
:提交暂存区的文件到本地仓库。git log
:查看提交历史。git merge <branch-name>
:合并指定分支到当前分支。git checkout <branch-name>
:切换分支。git pull
:拉取远程仓库的更新。
内容来源
三分恶的面渣逆袭:javabetter.cn/sidebar/san…
二哥的 Java 进阶之路(GitHub 已有 13000+star):github.com/itwanger/to…
最后,把二哥的座右铭送给大家:没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。共勉 💪。
来源:juejin.cn/post/7451638008409554994
00后小破站员工"开盒"用户事件,后续来了
"开盒"事件
这两天,关于「2025 年 1 月 B 站员工"开盒"(公开曝光他人隐私)用户并删除用户视频」的事件有了新的进展。
可能有读者还没听说这事儿,这里简单梳理一下时间线。
事情的起因,是 B 站员工袁某某(Next0820)在某个游戏视频底下回复了一条 22 年的评论,先是通过收入对 B 站用户(黄金鼠塔)进行了嘲讽:
双方随后开始产生争执,过程中,该员工透露自己"身份不一般",并借助内部人员优势对用户进行"开盒"恐吓:
此时该工作人员将自己账号的头像改成了受害者头像黑白照片。
但此等恐吓并未吓到这位 B 站用户,甚至还被用户回了一句"我身-份-证拍的很帅" 🤣🤣🤣
再之后,该工作人员开始进行了下一步行动:向用户网页端注入代码跳转到显示"你的账号已经被封禁!"的界面,且删除用户在 B 站的所有的稿件和动态。
跳转的原理十分简单暴力:通过给线上网页版的代码插入一段 JS,针对某些用户会跳转到特定界面。
这位 B 站用户平常使用的是客户端,短期并未受到影响 🤣🤣🤣
但很快,另外一位 B 站用户(罗德兰屑罗素)爆料也遇到了相同的遭遇。由于这位用户日常使用的是网页版,受到影响后,把这事儿通过工单的形式上报给官方,经过调查核实后,官方给出了对应的处理方案:
- 确认是内部员工,行为触及底线,已进行开除处理;
- 准备将该情况上报到监管部门;
- 内部对这个事情再做一次通报批评,引以为戒,相关主管人员也进行处罚;
- 对被删除的内容,进行恢复处理;
对于此事件,虽然性质恶劣,但官方处理方案还算得体。一些网友将「个人行为」上升到「平台调性」,我觉得也不甚合适。尤其涉事人员还是位 00 后,实在不好点评。
但作为"程序员"对于此事能够发生(且不止一次)还是觉得十分震惊,为什么一个"前端工程师"能够如此随意查看线上用户的隐私数据,而且还能如此随意修改线上代码,内部一点 review 都没有吗,这有点属于草台到极致了。
对此,你怎么看?你平时看 B 站多吗,和 B 站网友评论/弹幕的情况多吗?欢迎评论区交流。
...
回归主题。
来一道和「校招」相关的算法题。
题目描述
平台:LeetCode
题号:442
给你一个长度为 的整数数组 nums
,其中 nums
的所有整数都在范围 内,且每个整数出现一次或两次。请你找出所有出现 两次 的整数,并以数组形式返回。
你必须设计并实现一个时间复杂度为 且仅使用常量额外空间的算法解决此问题。
示例 1:
输入:nums = [4,3,2,7,8,2,3,1]
输出:[2,3]
示例 2:
输入:nums = [1,1,2]
输出:[1]
示例 3:
输入:nums = [1]
输出:[]
提示:
nums
中的每个元素出现一次或两次
原地哈希
给定数组长度为 ,且所有数范围在 ,找出出现次数超过一次的所有数字。
利用值域与数字下标空间大小的等同关系,我们可以构造一种对应“关系”,使得每个数出现在它应该出现的位置:对于值为 的数字,我们将其应该出现在的位置定为 。
基于此,我们从前往后遍历 ,并尝试将当前处理到的 放到目标位置 处。如果一个数在尝试移动到它应该出现的位置时,发现 ,则说明该数字出现了超过一次。此时我们将 加入答案,由于此时没有发生交换,而 占用的仍是其他数字的目标位置,为了防止 与其他数字发生交换后,再次被检验并重复加入答案,我们将 置为负数,并在遍历过程中跳过负数。
Java 代码:
class Solution {
public List<Integer> findDuplicates(int[] nums) {
List<Integer> ans = new ArrayList<>();
int n = nums.length;
for (int i = 0; i < n; i++) {
int t = nums[i];
if (t < 0 || t - 1 == i) continue;
if (nums[t - 1] == t) {
ans.add(t);
nums[i] *= -1;
} else {
int c = nums[t - 1];
nums[t - 1] = t;
nums[i--] = c;
}
}
return ans;
}
}
C++ 代码:
class Solution {
public:
vector<int> findDuplicates(vector<int>& nums) {
vector<int> ans;
int n = nums.size();
for (int i = 0; i < n; i++) {
int t = nums[i];
if (t < 0 || t - 1 == i) continue;
if (nums[abs(t) - 1] == t) {
ans.push_back(t);
nums[i] *= -1;
} else {
int c = nums[abs(t) - 1];
nums[abs(t) - 1] = t;
nums[i--] = c;
}
}
return ans;
}
};
Python 代码:
class Solution:
def findDuplicates(self, nums: List[int]) -> List[int]:
ans = []
n, i = len(nums), 0
while i < n:
t = nums[i]
if t >= 0 and t - 1 != i:
if nums[abs(t) - 1] == t:
ans.append(t)
nums[i] *= -1
else:
c = nums[abs(t) - 1]
nums[abs(t) - 1] = t
nums[i] = c
i -= 1
i += 1
return ans
TypeScript 代码:
function findDuplicates(nums: number[]): number[] {
const ans = [];
const n = nums.length;
for (let i = 0; i < n; i++) {
const t = nums[i];
if (t < 0 || t - 1 === i) continue;
if (nums[Math.abs(t) - 1] === t) {
ans.push(t);
nums[i] *= -1;
} else {
const c = nums[Math.abs(t) - 1];
nums[Math.abs(t) - 1] = t;
nums[i--] = c;
}
}
return ans;
};
- 时间复杂度:
- 空间复杂度:
来源:juejin.cn/post/7462066293891661876
“小红书”海外版正式更名“ rednote”,突然爆红的背后带给开发者哪些思考?
序言
小红书在昨天的更新中,已经在Appstore正式将海外版本名称改为
rednote - share, connect, love
爆红趣事
自从“Tiktok”陷入前一阵的风波之后,不少海外玩家纷纷涌入了“抖音”、“快手”和“小红书”的热门社区类App。
同时这一爆发性事件也被网友调侃“入侵行为”。
永远忘不了2025年1月15日这沉重的一天。八国联军入侵了我的抖音 我刷不到我的同胞们了
。
还有有趣的网友发出珍藏的表情包,与海外玩家互动。比如:
更有颜值控(LSP)的玩家发了"激进的言论"
。比如:
也有心细的网友发现了新版本岗位工作
的需求。比如:
对开发机遇
Register
既然想使用国内的App,首先要解决的是注册问题。大多数App都需要在注册的时候,使用手机号和验证码来验证用户的真实性,那么所谓接码平台
绝对是老外注册的不二之选。
Study Chinese
既然有需要将英语翻译成中文的工作,那么反向思维一下?老外会不会提升对于学习中文的需求
?于是乎带着疑惑,在点点数据搜索'Chinese'发现,已经有了这类产品。占据榜首的当属HelloChinese
。毫不夸张地说,这种产品迎来了属于自己的高光时刻。
通过查看榜单排名变化,可以清晰地看到。排名攀爬的趋势极速上涨
。
常言道机会总是留给有准备的人
,如果没有造势的本事,就不如顺势而为。
希望大家都可以早日遇到自己的风口
,最后祝大家大吉大利,今晚过审!
来源:juejin.cn/post/7462260074183589915
一个7000Star的项目一年能赚多少钱?
往事
又一年过去了,不知不觉从事前端开发已经快7年了,7年时间其实也不算特别长,但是从行业欣欣向荣干到了行业巅峰再到如今的行业没了,我是没想到的。
虽然目前还没有失业(感觉快了),但这么多年也没赚到什么钱(工资在杭州的行业里垫底),没有在行业正盛时进入一个大公司算是最大的遗憾了。
这么多年只待过两家公司,做的绝大部分事情都是普通的业务开发,尤其是第二家公司,虽然从公司人数上来说是第一家公司的200多倍,但所负责的业务复杂度远不如第一家公司。
公司业务之外,个人在技术上这么多年热衷于干的事情大致分为三个阶段:
一:维护自己的个人网站,开发各种小工具,小游戏,乐此不疲,持续了三四年,不过纯属自娱自乐,没啥人用,更别提收益,所以在服务器和域名到期后就关闭了。
二:写技术文章,从偶然写了一篇爆款文章后开始一发不可收拾,两年时间写了100多篇,主战掘金,混到了lv6,各种原因下目前已经停更,过去的一年只写了8篇,总的数据量可能没有之前的一篇高,只写纯技术现在肯定是没人看的。
三:维护开源项目,也是过去一年主要干的事情,当初机缘巧合下写了一个思维导图demo,没想到会变成现在的样子,早知道初版的代码就认真写了。
做以上这些事情的动机除了兴趣外,更大的原因其实是想通过这些弥补自己在公司所做的项目太普通、没有大厂经历、毕业院校一般等不足,说白了就是给面试加成,在之前确实有点用,但是对于目前这种僧多粥少的情况来说可忽略不计,毕竟在杭州这个互联网大城,比我牛的人可太多了。
所以现在目标转变为了:赚钱。
之前总有一点赚钱羞耻感,提供了服务,提供了代码,却不好意思提钱,总觉得自己的项目还不够好,但是现在生活所迫,也管不了太多了。
说了这么多有的没的,接下来还是聊聊我这个项目。
首先还是稍微介绍一下,simple-mind-map
(思绪思维导图)是一个开源的思维导图库和软件,既提供了一个js
库来用于开发,也提供了一个软件直接使用。
更新
过去一年的一些数据分享:
- 版本:一共更新了26个版本;
- Star:Star数量由2000涨到了7000+;
- Issue:Issue数量由500涨到了1000+;
- 下载:NPM包下载数量由20000涨到了40000+;
- 交流群:交流群人数由300涨到了800+;
更新点非常多,接下来简单分享其中一些比较大的:
- 功能
支持导出txt文件、支持导入导出Excel文件、导入导出Freemind软件文件、新增了向左逻辑结构图、节点文本编辑支持类原地编辑、支持拖拽调整节点宽度、大幅优化了节点富文本的渲染逻辑。
- 插件
新增了手绘风格插件、彩虹线条插件、演示插件、节点标记插件、节点外框插件、节点编号插件、待办插件。
- 性能
虽然我一直觉得思维导图节点数量控制在几百个比较适合阅读和编辑,但是经常有想要支持几千个节点的人来向我吐槽太卡了,那是肯定的,实现上是基于SVG+DOM,数量多了肯定会卡,其实性能这块一直在尝试优化,概括来讲也分为几个阶段:
一:数据节点数据改变,完全重新创建和渲染所有节点,实现上最简单,但是基本不可用,节点数量达到几十个就已经慢的不行;
二:根据具体操作手动修改需要更新的部分,比如插入节点操作,那么只创建新插入的节点及其父节点,这种方式可行,但是缺点也很明显,不够通用,每种操作都需要特定的更新逻辑,维护成本很高,扩展很难;
三:缓存和复用节点实例,将已经创建好的节点实例缓存起来,根据节点数据的uid进行查找和复用,如果节点数据没有更新,那么直接使用缓存数据,否则重新创建并更新缓存,改成这种方式后,性能提升了一个数量级,但是更新细粒度其实也只到了节点级,并没有精确到节点的具体内容上,比如只更新了节点文本,最好是只重新创建节点的文本内容部分,其他图标等部分不需要。
四:为了应对几千个节点量级的使用,增加了一个所谓的性能模式,其实就是只渲染可视区域的节点,这种方式其实无论多少个节点,只要可视区域的节点不多,性能下限都不会太低。
谦虚点说,simple-mind-map
可以算是开源的思维导图中功能、颜值和体验都最强的之一(不能违反广告法),如果你对自由节点和概要需求不高的话,那么完全可以代替目前市面上的收费思维导图软件。
理想文档
这是今年和我一个朋友(后端开发大佬)抽空一起做的另一个开源项目,一个简单的在线云文档,汇聚了一些优秀的开源项目,比如:流程图、幻灯片、电子表格、文档等等,当然还有我的思维导图,其实就是给这些项目加了一个将数据存储到数据库的功能。
虽然目前整体比较简陋,但是胜在一个齐全和性价比。
有兴趣的朋友可以关注一下:github.com/wanglin2/lx…
收益
最后来回收一下标题,聊聊收益的问题。
simple-mind-map
目前只有以下几种获取收益的方式:
1.打赏:经常做开源的朋友都知道,愿意给你打赏的人是很少的,所以可以忽略不计,当然偶尔也会有几个比较大方的朋友;
2.收费插件:simple-mind-map
是插件化架构的项目,在提供了十几个免费的插件后,尝试做了几个收费的插件,但是以过去一年的情况来看,买的人很少,毕竟需求这东西很灵活,免费的就是刚需,收费的就变成不必要的了;
3.收费咨询:虽然项目的文档算是比较完善的,并且第一支持的语言是中文,所以并不存在什么看不懂的情况,但还是有很多人来问各种能在文档上找到答案的问题,回答多了也就不想回答了,毕竟你不愿看文档,我也不愿看,所以现在私聊只接受付费咨询;
4.去版权费用:虽然项目基本的开源协议为MIT
,最宽松的开源协议了,但是也不意味着完全没有要求,商用的话需要保留项目的版权,所以如果不愿意保留,那么会收一笔去版权费用,但是大家都知道开源协议就是一个君子协议,自觉的人是很少的,所以一般都是我发现了主动去沟通维护权益;
其实还有一些其他乱七八糟的方式,但基本上都很不稳定,过去的一年,收益全部加起来其实也不到我月工资的一半,而前面提到了,我的工资在杭州是垫底的,所以总结就一句话:
做开源没有钱途,还是安心当牛马打工吧!
当然,新的一年也会探索一些新的赚钱方式,有好建议的朋友欢迎留言~
来源:juejin.cn/post/7456469629474226210
AI赋能剪纸艺术,剪映助力多地文旅点亮新春
近日,一场别开生面的文化盛宴在社交媒体拉开帷幕。多地文旅纷纷在官方账号发布剪纸风格的视频,以独特的视角展现当地丰富的文旅资源,将传统非遗文化与春节的喜庆氛围完美融合,这一创新形式收获网友大量点赞。
在这些令人眼前一亮的视频中,各地的标志性景点和特色风土人情以剪纸艺术的形式生动呈现。细腻的线条勾勒出西安大雁塔的宏伟庄严,鲜艳的色彩展现出塞上江南的瑰丽,精致的图案描绘出江南水乡的温婉秀丽。每一幅剪纸都仿佛在诉说着一个地方的故事,让大众在感受剪纸艺术魅力的同时,领略到祖国大地的壮美多姿。
图片来源:陕西文旅、威海文旅、内蒙古文旅的官方社交媒体账号
记者注意到,本次剪纸效果采用了剪映提供的“中式剪纸”模板功能。作为字节跳动旗下的视频创作工具产品,剪映团队发挥技术优势,将AI新技术与传统剪纸艺术深度融合,为创作者提供了便捷且强大的创作工具。通过AI算法,用户只需上传照片素材,就能快速生成效果精细的剪纸风格视频,大大降低了创作门槛,让更多人参与到创作中来。
除了风景类的剪纸视频模板,剪映在春节期间还推出了丰富多样的其他模板,如人物剪纸模板。用户可以通过这些模板,将自己或身边人的形象创作为剪纸风格的人物,为视频增添更多趣味性和个性化元素。无论是阖家团圆的场景,还是展现个人风采的画面,都能通过这些模板以独特的剪纸艺术形式呈现。
剪映相关负责人表示,新春将至,希望通过AI技术的应用让剪纸艺术突破地域和传统展示形式的限制,激发更多人对家乡的热爱,鼓励大家用这种新颖的方式秀出自己家乡的风景,共同分享美好。(作者:刘洪)
收起阅读 »synchronized就该这么学
先赞后看,Java进阶一大半
早期sychonrized重量级锁开销大,于是JDK1.5引入了ReentrantLock,包含现在很多偏见都是认为ReentrantLock性能要优于sychonrized。但JDK1.6引入的锁升级,不断迭代,怕是性能往往还优于ReentrantLock。
我是南哥,相信对你通关面试、拿下Offer有所帮助。
敲黑板:本文总结了多线程相关的synchronized、volatile常见的面试题!
⭐⭐⭐收录在《Java学习/进阶/面试指南》:https://github/JavaSouth
精彩文章推荐
1. synchronized
1.1 可重入锁
面试官:知道可重入锁有哪些吗?
可重入意味着获取锁的粒度是线程而不是调用,如果大家知道这个概念,会更容易理解可重入锁的作用。
既然获取锁的粒度是线程,意味着线程自己是可以获取自己的内部锁的,而如果获取锁的粒度是调用则每次经过同步代码块都需要重新获取锁。
举个例子。线程A获取了某个对象锁,但在线程代码的流程中仍需再次获取该对象锁,此时线程A可以继续执行不需要重新再获取该对象锁。另外线程如果要使用父类的同步方法,由于可重入锁也无需再次获取锁。
在Java中,可重入锁主要有ReentrantLock、synchronized。
1.2 synchronized实现原理
面试官:你先说说synchronized的实现原理?
synchronized的实现是基于monitor的。任何对象都有一个monitor与之关联,当monitor被持有后,对象就会处于锁定状态。而在同步代码块的开始位置,在编译期间会被插入monitorenter指令。
当线程执行到monitorenter指令时,就会尝试获取monitor的所有权,如果获取得到则代表获得锁资源。
1.3 synchronized缺点
面试官:那synchronized有什么缺点?
在Java SE 1.6还没有对synchronized进行了各种优化前,很多人都会称synchronized为重量级锁,因为它对资源消耗是比较大的。
- synchronized需要频繁的获得锁、释放锁,这会带来了不少性能消耗。
- 另外没有获得锁的线程会被操作系统进行挂起阻塞、唤醒。而唤醒操作需要保存当前线程状态,切换到下一个线程,也就是进行上下文切换。上下文切换是很耗费资源的一种操作。
1.4 保存线程状态
面试官:为什么上下文切换要保存当前线程状态?
这就跟读英文课文时查字典一样,我们要先记住课文里的页数,查完字典好根据页数翻到英文课文原来的位置。
同理,CPU要保证可以切换到上一个线程的状态,就需要保存当前线程的状态。
1.5 锁升级
面试官:可以怎么解决synchronized资源消耗吗?
上文我有提到Java SE 1.6对synchronized进行了各种优化,具体的实现是给synchronized引入了锁升级的概念。synchronized锁一共有四种状态,级别从低到高依次是无锁、偏向锁、轻量级锁、重量级锁。
大家思考下,其实多线程环境有着各种不同的场景,同一个锁状态并不能够适应所有的业务场景。而这四种锁状态就是为了适应各种不同场景来使得线程并发的效率最高。
- 没有任何线程访问同步代码块,此时synchronized是无锁状态。
- 只有一个线程访问同步代码块的场景的话,会进入偏向锁状态。偏向锁顾名思义会偏向访问它的线程,使其加锁、解锁不需要额外的消耗。
- 有少量线程竞争的场景的话,偏向锁会升级为轻量级锁。而轻量级采用CAS操作来获得锁,CAS操作不需要获得锁、释放锁,减少了像synchronized重量级锁带来的上下文切换资源消耗。
- 轻量级锁通过CAS自旋来获得锁,如果自旋10次失败,为了减少CPU的消耗则锁会膨胀为重量级锁。此时synchronized重量级锁就回归到了悲观锁的状态,其他获取不到锁的都会进入阻塞状态。
1.6 锁升级优缺点
面试官:它们都有什么优缺点呢?
由于每个锁状态都有其不同的优缺点,也意味着有其不同的适应场景。
- 偏向锁的优点是加锁和解锁操作不需要额外的消耗;缺点是如果线程之间存在锁竞争,偏向锁会撤销,这也带来额外的撤销消耗;所以偏向锁适用的是只有一个线程的业务场景。
- 轻量级锁状态下,优点是线程不会阻塞,提高了程序执行效率;但如果始终获取不到锁的线程会进行自旋,而自旋动作是需要消耗CPU的;所以轻量级锁适用的是追求响应时间、同时同步代码块执行速度快的业务场景。
- 重量级锁的优点是不需要自旋消耗CPU;但缺点很明显,线程会阻塞、响应时间也慢;重量级锁更适用在同步代码块执行速度较长的业务场景。
2. volatile
2.1 指令重排序
面试官:重排序知道吧?
指令重排序字面上听起来很高级,但只要理解了并不难掌握。我们先来看看指令重排序究竟有什么作用。
指令重排序的主要作用是可以优化编译器和处理器的执行效率,提高程序性能。例如多条执行顺序不同的指令,可以重排序让轻耗时的指令先执行,从而让出CPU流水线资源供其他指令使用。
但如果指令之间存在着数据依赖关系,则编译器和处理器不会对相关操作进行指令重排序,避免程序执行结果改变。这个规则也称为as-if-serial语义
。例如以下代码。
String book = "JavaGetOffer"; // A
String avator = "思考的陈"; // B
String msg = book + abator; // C
对于A、B,它们之间并没有依赖关系,谁先执行对程序的结果没有任何影响。但C却依赖于A、B,不能出现类似C -> A -> B或C -> B -> A或A -> C -> B或B -> C -> A之类的指令重排,否则程序执行结果将改变。
2.2 重排序的问题
面试官:那重排序不会有什么问题吗?
在单线程环境下,有as-if-serial语义
的保护,我们无需担心程序执行结果被改变。但在多线程环境下,指令重排序会出现数据不一致的问题。举个多线程的例子方便大家理解。
int number = 0;
boolean flag = false;
public void method1() {
number = 6; // A
flag = true; // B
}
public void method2() {
if (flag) { // C
int i = number * 6; // D
}
}
假如现在有两个线程,线程1执行method1
、线程2执行method2
。因为method1
其中的A、B之间没有数据依赖关系,可能出现B -> A的指令重排序,大家注意这个指令重排序会影响到线程2执行的结果。
当B指令执行后A指令还没有执行number = 6
,此时如果线程2执行method2
同时给i赋值为0 * 6
。很明显程序运行结果和我们预期的并不一致。
2.3 volatile特性
面试官:有什么办法可以解决?
关于上文的重排序问题,可以使用volatile关键字来解决。volatile一共有以下特性:
- 可见性。volatile修饰的变量每次被修改后的值,对于任何线程都是可见的,即任何线程会读取到最后写入的变量值。
- 原子性。volatile变量的读写具有原子性。
- 禁止代码重排序。对于volatile变量操作的相关代码不允许重排序。
int number = 0;
volatile boolean flag = false;
public void method1() {
number = 6; // A
flag = true; // B
}
public void method2() {
if (flag) { // C
int i = number * 6; // D
}
}
由于volatile具有禁止代码重排序的特性,所以不会出现上文的B -> A的指令重排序。另外volatile具有可见性,falg的修改对线程2来说是可见的,线程会立刻感知到flag = ture
从而执行对i的赋值。以上问题可以通过volatile解决,和使用synchronized加锁是一样的效果。
另外大家注意一点,volatile的原子性指的是对volatile的读、写操作的原子性,但类似于volatile++
这种复合操作是没有原子性的。
2.5 可见性原理
面试官:那volatile可见性的原理是什么?
内存一共分为两种,线程的本地内存和线程外的主内存。对于一个volatile修饰的变量,任何线程对该变量的修改都会同步到主内存。而当读一个volatile修饰的变量时,JMM(Java Memory Model)会把该线程对应的本地内存置为无效,从而线程读取变量时读取的是主内存。
线程每次读操作都是读取主内存中最新的数据,所以volatile能够实现可见性的特性。
2.3 volatile局限性
面试官:volatile有什么缺点吗?
企业生产上还是比较少用到volatile的,对于加锁操作会使用的更多些。
- synchronized加锁操作虽然开销比volatile大,但却适合复杂的业务场景。而volatile只适用于状态独立的场景,例如上文对flag变量的读写。
- volatile编写的代码是比较难以理解的,不清楚整个流程和原理很难维护代码。
- 类似于
volatile++
这种复合操作,volatile不能确保原子性。
⭐⭐⭐本文收录在《Java学习/进阶/面试指南》:https://github/JavaSouth
我是南哥,南就南在Get到你的点赞点赞点赞。
创作不易,不妨点赞、收藏、关注支持一下,各位的支持就是我创作的最大动力❤️
来源:juejin.cn/post/7435894119103430665
耗时三个月,我高仿了一个起点小说阅读器
前言
起因是最近看小说的APP广告越来越多,但不少书源内容也时常出现问题。正好摆烂太久让我很有负罪感,就想着趁着这个契机学点新的东西。公司里用的都是vue技术栈,所以我想着用vue3做个小项目,顺便熟悉一下vue3的语法。从八月开始,断断续续搞了点Demo,直到年底稍微有点空闲,才开始着手把整个项目完善起来。
项目地址
github
gitee
支持平台
平台 | 是否支持 |
---|---|
H5 | 是 |
Android | 是 |
IOS | 是 |
小程序 | 需要修改renderjs |
项目介绍
eReader 是一款基于 uni-app 开发的小说阅读器,功能完善,使用便捷,支持跨平台部署。移动端完全由前端实现,无需后端支持,打包后即为一个独立的APP,极大降低了部署和维护成本。H5端由于跨域问题需要启用一个简单的后端服务器,但移动端打包后完全开箱即用。
技术架构与部署
- uni-app 的跨平台特性使得该项目在移动端和H5端之间无缝切换,移动端是纯前端实现,不依赖额外的服务器。
- H5端需要启用后端服务器来解决跨域问题,但移动端完全是前端应用,避免了额外的服务器负担,极大简化了部署和维护流程。
- 使用技术栈如下
- Vue3 + TypeScript:项目基于Vue3和TypeScript实现。
- Node + Express:H5端使用Node + Express搭建了一个简单的后台,负责爬取数据。
- uni.request:在APP端通过uni.request获取数据,不用启用后端应用。
- Cheerio:用Cheerio来解析HTML,提取书籍信息。
- uni.setStorage:数据缓存使用了uni.setStorage存储。
- 阅读引擎:主要是用 canvas.measureText 来计算文本宽度,通过JS计算宽高分页,支持两端对齐、标点避头等排版优化。
- 分页:分页计算用了uni-app的 renderjs 操作Canvas, uni.createCanvasContext 在APP端性能表现不佳,应尽量避免使用。
- 海报分享:海报分享功能使用了 limeui-painter 。
平台功能
- 丰富的书源:内置多个书源,满足大多数阅读需求,并支持灵活切换。
- 全面的功能:包括书架管理、小说搜索、阅读器设置(夜间模式、字体、背景主题、翻页方式)、章节缓存等,功能齐全。
- 个性化体验:支持书签、目录跳转、缓存、夜间模式等用户自定义设置。
- 逻辑闭环:书源管理、阅读设置、书签等功能平滑切换,确保使用流畅、体验一致。
- 详细功能列表
- 书架:可以加入/移除书架、置顶小说、分享(APP端)、查看详情、搜索、小说排序和浏览历史等功能。
- 分组:可以管理小说分组,支持新增、删除、修改、置顶等操作。
- 精选推荐:集成了 夸克热搜 的书单推荐,帮助大家发现热门书籍。
- 我的:包括书源管理、浏览历史、夜间模式、关于、意见反馈、缓存清除和分享等设置。
- 小说搜索:内置了 12 个书源,基本能满足大部分人的阅读需求。
- 书籍详情:展示书籍信息、简介、目录等,支持分享功能。
- 阅读器:支持添加/移除书架、添加/删除书签、查看目录、白天/夜间模式切换、翻页方式、字号和背景主题切换等多项个性化设置。此外,还支持其余书源切换和章节缓存(包括缓存全部、缓存后20章和缓存当前章节后的所有章节)。
- 目录:支持目录查看、缓存状态、书签、章节跳转、快速跳转(比如去当前章节、去底部)等功能。
项目结构
|-- undefined
|-- .prettierignore
|-- .prettierrc.js
|-- index.html
|-- package.json
|-- tsconfig.json
|-- vite.config.ts
|-- src
|-- App.vue
|-- env.d.ts
|-- main.ts
|-- manifest.json
|-- pages.json
|-- type.d.ts
|-- uni.scss
|-- api #请求接口
| |-- common.ts
|-- components
| |-- BookTip.vue #阅读页第一次打开提示
| |-- Expand.vue #书籍详情简介收起与展开
| |-- share.vue #分享组件
| |-- TabBar.vue #重写tabbar,没使用uni自带tabbar
| |-- global #全局组件
| | |-- g-confirm.vue #确认和输入弹窗
| | |-- g-icon-fonts.vue #图标
| | |-- g-page.vue #每个页面根元素,主要是做主题切换,设置全局css样式(uniapp的APP.vue没有根元素)
| | |-- g-popup.vue #底部和中间弹窗封装
| | |-- g-statusbar.vue #顶部statusbar占位组件,h5端高度为0,app端有默认高度
| |-- painter #海报绘制组件
| |-- popover #书架排序气泡窗
|-- directives #vLongPress指令封装
| |-- index.ts
|-- pages
| |-- blank #我的-跳转页面
| | |-- about.vue #关于我们
| | |-- agreement.vue #用户协议
| | |-- feedback.vue #意见反馈
| | |-- history.vue #浏览历史
| | |-- origin.vue #书源管理
| | |-- policy.vue #隐私政策
| |-- bookDetail #书籍详情页
| |-- catalogs #目录页
| |-- groupDetail #分组详情页
| |-- reader #阅读器
| | |-- index.vue
| | |-- index_v1.vue #第一版,使用columns布局分页
| | |-- index_v2.vue #第二版,使用canvas.measureText计算宽度,js计算宽高进行分页(算法不完善,可以看看思路)
| | |-- readerLayout.ts #第三版,感谢 [@前端一锅煮] 大佬的分享
| | |-- components
| | |-- Origin.vue #换源组件
| | |-- Renderjs.vue #使用uniapp的rendejs获取 document 文档对象
| | |-- Renderjs_v2.vue #第二版renderjs
| |-- search #搜索页
| |-- tabBar #自定义tabbar
| |-- book.vue #精选
| |-- home.vue #书架
| |-- personal.vue #我的
| |-- components
| |-- addGr0up.vue #书架、分组详情里[移至分组]功能
| |-- bookDetail.vue #书架、分组详情里长按展示详情功能
| |-- groupItem.vue #分组项
|-- parser #app端数据解析
| |-- catalog.ts #目录解析
| |-- content.ts #章节内容解析
| |-- index.ts
| |-- search.ts #搜索内容解析
| |-- source.ts #内置书源
| |-- top.ts #精选内容解析
|-- static
|-- store #store
| |-- AppOption.ts #app的系统信息
| |-- index.ts #一些缓存相关数据处理:书架、历史、缓存章节、搜索历史等
|-- styles
|-- types
|-- utils
|-- Config.ts
|-- Control.ts
|-- index.ts
|-- request.ts #请求处理和响应拦截
|-- RequestHeader.ts #最初是想伪造请求头的,但是uni的app端ua固定了
后续功能优化
- 错误处理:当前未处理极端情况下的错误请求,导致产品在特定条件下可能不够健壮,后续会加强异常处理。
- 网络字体支持:项目打包后APK约15MB,内置字体包增大了文件体积,后续会考虑支持网络字体加载以实现更丰富的阅读体验。
- 书源导入与更新:第三方书源存在不稳定性,网站变动可能导致解析错误。后续会考虑支持书源离线导入和在线更新,有助于解决此问题。
- 听书功能:作为干眼症患者,听书功能对我来说还是非常重要的,未来计划加入该功能。
- 去除广告:第三方书源可能包含广告和无关链接,影响阅读体验。后续考虑支持长按选择内容去除,并应用到所有章节,将极大提升阅读质量。
项目展示
h5表现
- 书架
- 精选
- 我的
- 搜索
- 详情
- 阅读器
app端表现(IOS)
Android端未完整测试,可能存在部分兼容问题
- 书架(亮)
- 搜索(亮)
- 书源管理(亮)
- 我的(亮)
- 浏览历史(亮)
- 分组(暗)
- 分组详情(暗)
- 我的(暗)
- 意见反馈(暗)
- 详情(暗)
- 分享
- 阅读器
总结
- 最初只是为了学习新技术栈,项目框架、组件设计没考虑太多。但随着功能的增加,组件复用和方法抽象的需求变得明显,过程中也渐渐感觉到有些力不从心。
- 尽管仍有一些缺漏,但是整体来看来这个项目已经勉强算得上是一个完整的、功能闭环的产品。作为一个人独立完成,自己也算是比较满意了。
- 开发过程中遇到了不少挑战,比如阅读器排版引擎就经历了三次重构,才最终达到了理想效果。那段时间搞得头都要秃了(本来所剩无几的发量越加稀少)。 后续会写写教程,记录下开发过程中遇到的坑。
相关
水了几篇文章,回家过年咯(逃~)
- 从零开始手撸一个阅读器--排版引擎的实现(1)
- 从零开始手撸一个阅读器--书源解析功能的实现(2)
- 从零开始手撸一个阅读器--换源功能的实现(3)
- 从零开始手撸一个阅读器--数据结构与数据缓存(4)
- 从零开始手撸一个阅读器--夜间/白天主题色切换(5)
参考
感谢下面两位大佬的文章
来源:juejin.cn/post/7460023342592901183
如何进行千万级别数据跑批优化
最近观看公司前辈文档,看到对大数据量跑批的优化方案,参照自己的理解和之前相关经验整理了一份优化方案~
Background
定义: 跑批通常指代的是我们应用程序在固定日期针对某一批大量数据定时进行特定的处理,在金融业务中一般跑批的场景有分户日结、账务计提、账单逾期、不良资产处理等等,它具有高连贯性特点,通常我们执行完跑批后还要对跑批数据进行进一步处理,比如发 MQ 给下游消费,数仓拉取分析等。。。
跑批最怕的就是上来就干,从不考虑涉及到第三方接口时的响应时间、大事务等问题。
Problem
针对大数据量跑批会有很多的问题,比如我们要在指定日期指定时间的大数据量去处理,还要保证处理期间尽可能的高效,在出现错误时也要进行相应的补偿措施,避免影响到其它业务等 ~
- OOM : 查询跑批数据,未进行分片处理,随着业务纵向发展数据膨胀一旦上来,就容易导致 OOM 悲剧;
- 未对数据进行批量处理: 针对业务中间的处理未采用批量处理的思维,造成花费大量的时间,另外频繁的 IO 也是问题之一;
- 避免大事务: 直接用 @Transaction 去覆盖所有的业务是不可取的,问题定位困难不说,方法处理时间变久了;
- 下游接口的承受能力: 下游的承载能力也要在我们的考虑范围之内,比如大数量分批一直发,你是爽了,下游没有足够的能力消费就会造成灾难性的问题;
- 任务时间上的隔离: 通常大数据量跑批后面还有一些业务上的处理,对于时间和健壮性上要严格控制;
- 失败任务补偿: 分布式任务调度创建跑批任务,然后拆分子任务并发到消息队列,线程池执行任务调用远程接口,这中间的任何步骤都有可能会出问题导致任务失败;
Analyze
通过以上问题的总结,我们可以得出要完整的进行大数据量跑批任务我们的代码设计需要具备以下的几点素质:
- 健壮性: 跑批任务是要通过定时的去处理这些数据,不能因为其中一条数据出现异常从而导致整批数据无法继续进行操作,所以它必须是健壮的;
- 可靠性: 针对于异常数据我们后续可进行补偿处理,所以它必须是可靠的;
- 隔离性: 避免干扰任何其他应用程序的正常运行;
- 高性能: 通常跑批任务要处理的数据量较大,我们不能让它处理的时间过于久,这样会挤压后续的其它连贯性业务处理时间,所以我们必须考虑其性能处理;
Solution
大数据量的数据是很庞大的,如果一次性都加载到内存里面将会是灾难性的后果,因此我们要对大数据量数据进行分割处理,这是防止 OOM 必要的一环!此外,监控、异常等方法措施也要实施到位,到问题出现再补救就晚了~
1、数据库问题
使用数据库扫表问题:
遍历数据对数据库的压力是很大的,越往后速度越慢;
解决:
遍历数据库越往后查压力越大,可以设置在每次查询的时候携带上一次的极值,让你分页查找的offect永远控制在0;
2、分片广播
分片: 在生产环境中,都是采用集群部署,如果一个跑批任务只跑在一个机器上,那效率肯定很低,我们可以利用 xxl-job「分片广播」 和 「动态分片」 功能;
分布式调度幂等: 分布式任务调度只能保证准时调到一个节点上,而且通常都有失败重试的功能。所以任务幂等都是要的,一般都是通过分布式锁来实现,这里遵循简单原则使用数据库就可以了,可以通过在任务表里 insert 一条唯一的任务记录,通过唯一键来防止重复调度。
除了用唯一键,还可以在记录中增加一个状态字段,使用乐观锁来更新状态。比如开始是初始化状态,更新成正在运行的状态,更新失败说明别的节点已经在跑这个任务。当然分布式锁的实现方案有很多,比如 redis、zk 等等。
集群分布式任务调度 xxl-job: 执行器集群部署时,“分片广播” 以执行器为维度进行分片,任务路由策略选择”分片广播”情况下,一次任务调度将会广播触发对应集群中所有执行器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务;
- 分片任务场景:10个执行器的集群来处理10w条数据,每台机器只需要处理1w条数据,耗时降低10倍;
- 广播任务场景:广播执行器机器运行shell脚本、广播集群节点进行缓存更新等
// Index 是属于 Total 第几个序列(从0开始)
int shardIndex = XxlJobHelper.getShardIndex();
// Total 是总的执行器数量
int shardTotal = XxlJobHelper.getShardTotal();
3、分批获取
- 设置步长: 分派到一个 Pod 负责的数据也是庞大的,一下查出来耗时太久容易导致超时,通常我们会引入步长的概念,比如分派给 Pod 1w条数据,我们可以将它划分 10 次查出,一次查出 1k 数据,进而避免了数据库查询数据耗时太久 ~
- 空间换时间: 跑批可能会涉及到数据准备的过程,边循环跑批数据边去查找所需的数据,涉及多个 for 嵌套的循环处理时,可以采用空间换时间的思想,将数据加载到内存中进行筛选查找,但是要做好 OOM 防范措施,比如用包装类接查找出来的数据等等,毕竟内存不是无限大的!
- 深分页: 分批查询时 limit 的偏移量越大,执行时间越长。比如 limit a, b 会查询前 a + b 条数据,然后丢弃前 a 条数据,select * 会查询所有的列,也会有回表操作。我们可以使用 子查询 优化 SQL ,先查出 id 后分页,尽量用覆盖索引 来优化;
4、事务控制
- 这些操作自身是无法回滚的,这就会导致数据的不一致。可能 RPC 调用成功了,但是本地事务回滚了,可是 PRC 调用无法回滚了;
- 在事务中有远程调用,就会拉长整个事务导致本事务的数据库连接一直被占用,从而导致数据库连接池耗尽或者单个链接超时,因此要熟悉调用链路,将事务粒度控制在最小范围内;
5、充分利用服务器资源
需要充分利用服务器资源,采用多线程,MySQL的CPU在罚息期间也是低于 50%、IOPS 使用率低于 50%;
其实跑数据是 io 密集型的,不需要非得压榨服务器资源 ~
6、MQ 消费任务并行
MQ 消费消息队列的消息时要在每个节点上同时跑多个子任务才能资源利用最大化。那么就使用到线程池了,如果选择的是Kafka或者 RocketMQ,他们的客户端本来就是线程池消费的,只需要合理调整客户端参数就可以了。如果使用的是 Redis,那就需要自己创建一个线程池,然后让一个 EventLoop 线程从 Redis 队列中取任务。放入线程池中运行,因为我们已经使用 Redis 队列做缓冲,所以线程池的队列长度设为0,这里直接使用JDK提供的 SynchronousQueue。(这里以java为例)
7、动态调整并发度
跑批任务中能动态调整速度是很重要的,有 2 个地方可以进行操作:
- 任务中调用远程接口,这个速度控制其实用 Thread.sleep() 就好了。
- 控制任务并发度,就是有多少个线程同时运行任务。这个控制可以通过调整线程池的线程数来实现,但是线程池动态调整线程数比较麻烦。动态调整可以通过开源的限流组件来实现,比如 Guava 的 RateLimiter。可以在每次调用远程接口前调用限流组件来控制并发速度。
8、失败任务如何继续
一般分布式调度路径:
- 分布式 任务调度创建跑批任务;
- 拆分子任务 多线程 并发的发送到 消息队列 ;
- 线程池 执行任务调用远程接口;
在这个链条中,可能导致任务失败或者中止的原因无非下面几个。
- 服务器 Pod 因为其它业务影响重启导致任务中止;
- 任务消费过程中失败,达到最大的重试次数;
- 业务逻辑不合理或者数据膨胀导致 OOM ;
- 消费时调用远程接口超时(这个很多人专注自己的业务逻辑从而忽略第三方接口的调用)
其实解决起来也简单,因为其它因素导致失败,你需要记录下任务的进度,然后在失败的点去再次重试 ~
- 记录进度: 我们需要知道这个任务执行到哪里了,同时也要记录更新的时间,这样才知道补偿哪里,比如进行跑批捞取时,要记录我们捞取的数据区间 ~
- 任务重试: 编写一个补偿式的任务(比如FixJob),定时的去扫面处在中间态的任务,如果扫到就触发补偿机制,将这个任务改成待执行状态投入消息队列;
9、下游接口时间
跑批最怕的就是上来就干,从不考虑涉及到第三方接口时的响应时间,如果不考虑第三方接口调用时间,那么在测试时候你会发现频繁的 YGC,这是很致命的问题,属于你设计之外的事件,但也是你必须要考虑的~
解决起来也简单,在业务可以容忍的情况下,我们可以将调用接口的业务逻辑设计一个中间态,然后挂起我们的这个业务,随后用定时任务去查询我们的业务结果,在收到信息后继续我们的业务逻辑,避免它一直在内存中堆积 ~
10、线程安全
在进行跑批时,一般会采用多线程的方式进行处理,因此要考虑线程安全的问题,比如使用线程安全的容器,使用JUC包下的工具类。
11、异常 & 监控
- 异常: 要保证程序的健壮性,做好异常处理,不能因为一处报错,导致整个任务执行失败,对于异常的数据可以跳过,不影响其他数据的正常执行;
- 监控: 一般大数据量跑批是业务核心中的核心,一次异常就是很大的灾难,对业务的损伤不可预估,因此要配置相应的监控措施,在发送异常前及时察觉,进而做补偿措施;
Reference
来源:juejin.cn/post/7433315676051406888
别再混淆了!一文带你搞懂@Valid和@Validated的区别
上篇文章我们简单介绍和使用了一下Springboot的参数校验,同时也用到了 @Valid 注解和 @Validated 注解,那它们之间有什么不同呢?
区别
先总结一下它们的区别:
- 来源
- @Validated :是Spring框架特有的注解,属于Spring的一部分,也是JSR 303的一个变种。它提供了一些 @Valid 所没有的额外功能,比如分组验证。
- @Valid:Java EE提供的标准注解,它是JSR 303规范的一部分,主要用于Hibernate Validation等场景。
- 注解位置
- @Validated : 用在类、方法和方法参数上,但不能用于成员属性。
- @Valid:可以用在方法、构造函数、方法参数和成员属性上。
- 分组
- @Validated :支持分组验证,可以更细致地控制验证过程。此外,由于它是Spring专有的,因此可以更好地与Spring的其他功能(如Spring的依赖注入)集成。
- @Valid:主要支持标准的Bean验证功能,不支持分组验证。
- 嵌套验证
- @Validated :不支持嵌套验证。
- @Valid:支持嵌套验证,可以嵌套验证对象内部的属性。
这些理论性的东西没什么好说的,记住就行。我们主要看分组和嵌套验证是什么,它们怎么用。
实操阶段
话不多说,通过代码来看一下分组和嵌套验证。
为了提示友好,修改一下全局异常处理类:
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 参数校检异常
* @param e
* @return
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseResult handle(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
StringJoiner joiner = new StringJoiner(";");
for (ObjectError error : bindingResult.getAllErrors()) {
String code = error.getCode();
String[] codes = error.getCodes();
String property = codes[1];
property = property.replace(code ,"").replaceFirst(".","");
String defaultMessage = error.getDefaultMessage();
joiner.add(property+defaultMessage);
}
return handleException(joiner.toString());
}
private ResponseResult handleException(String msg) {
ResponseResult result = new ResponseResult<>();
result.setMessage(msg);
result.setCode(500);
return result;
}
}
分组校验
分组验证是为了在不同的验证场景下能够对对象的属性进行灵活地验证,从而提高验证的精细度和适用性。一般我们在对同一个对象进行保存或修改时,会使用同一个类作为入参。那么在创建时,就不需要校验id,更新时则需要校验用户id,这个时候就需要用到分组校验了。
对于定义分组有两点要特别注意:
- 定义分组必须使用接口。
- 要校验字段上必须加上分组,分组只对指定分组生效,不加分组不校验。
有这样一个需求,在创建用户时校验用户名,修改用户时校验用户id。下面对我们对这个需求进行一个简单的实现。
- 创建分组
CreationGr0up 用于创建时指定的分组:
public interface CreationGr0up {
}
UpdateGr0up 用于更新时指定的分组:
public interface UpdateGr0up {
}
- 创建用户类
创建一个UserBean用户类,分别校验 username
字段不能为空和id
字段必须大于0,然后加上CreationGr0up
和 UpdateGr0up
分组。
/**
* @author 公众号-索码理(suncodernote)
*/
@Data
public class UserBean {
@NotEmpty( groups = {CreationGr0up.class})
private String username;
@Min(value = 18)
private Integer age;
@Email(message = "邮箱格式不正确")
private String email;
@Min(value = 1 ,groups = {UpdateGr0up.class})
private Long id;
}
- 创建接口
在ValidationController 中新建两个接口 updateUser
和 createUser
:
@RestController
@RequestMapping("validation")
public class ValidationController {
@GetMapping("updateUser")
public UserBean updateUser(@Validated({UpdateGr0up.class}) UserBean userBean){
return userBean;
}
@GetMapping("createUser")
public UserBean createUser(@Validated({CreationGr0up.class}) UserBean userBean){
return userBean;
}
}
- 测试
先对 createUser
接口进行测试,我们将id的值设置为0,也就是不满足id必须大于0的条件,同样 username 不传值,即不满足 username 不能为空的条件。 通过测试结果我们可以看到,虽然id没有满足条件,但是并没有提示,只提示了username不能为空。
再对 updateUser
接口进行测试,条件和测试 createUser
接口的条件一样,再看测试结果,和 createUser
接口测试结果完全相反,只提示了id最小不能小于1。
至此,分组功能就演示完毕了。
嵌套校验
介绍嵌套校验之前先看一下两个概念:
- 嵌套校验(Nested Validation) 指的是在验证对象时,对对象内部包含的其他对象进行递归验证的过程。当一个对象中包含另一个对象作为属性,并且需要对这个被包含的对象也进行验证时,就需要进行嵌套校验。
- 嵌套属性指的是在一个对象中包含另一个对象作为其属性的情况。换句话说,当一个对象的属性本身又是一个对象,那么这些被包含的对象就可以称为嵌套属性。
有这样一个需求,在保存用户时,用户地址必须要填写。下面来简单看下示例:
- 创建地址类 AddressBean
在AddressBean 设置 country
和city
两个属性为必填项。
@Data
public class AddressBean {
@NotBlank
private String country;
@NotBlank
private String city;
}
- 修改用户类,将AddressBean作为用户类的一个嵌套属性
特别提示:想要嵌套校验生效,必须在嵌套属性上加 @Valid
注解。
@Data
public class UserBean {
@NotEmpty(groups = {CreationGr0up.class})
private String username;
@Min(value = 18)
private Integer age;
private String email;
@Min(value = 1 ,groups = {UpdateGr0up.class})
private Long id;
//嵌套验证必须要加上@Valid
@Valid
@NotNull
private AddressBean address;
}
- 创建一个嵌套校验测试接口
@PostMapping("nestValid")
public UserBean nestValid(@Validated @RequestBody UserBean userBean){
System.out.println(userBean);
return userBean;
}
- 测试
我们在传参时,只传 country
字段,通过响应结果可以看到提示了city
字段不能为空。
可以看到使用了 @Valid
注解来对 Address 对象进行验证,这会触发对其中的 Address 对象的验证。通过这种方式,可以确保嵌套属性内部的对象也能够参与到整体对象的验证过程中,从而提高验证的完整性和准确性。
总结
本文介绍了@Valid
注解和@Validated
注解的不同,同时也进一步介绍了Springboot 参数校验的使用。不管是 JSR-303、JSR-380又或是 Hibernate Validator ,它们提供的参数校验注解都是有限的,实际工作中这些注解可能是不够用的,这个时候就需要我们自定义参数校验了。下篇文章将介绍一下如何自定义一个参数校验器。
来源:juejin.cn/post/7344958089429434406
Java 实现责任链模式 + 策略模式:优雅处理多级请求的方式
一、什么是责任链模式?
责任链模式(Chain of Responsibility Pattern) 是一种行为设计模式,它允许将请求沿着一个处理链传递,直到链中的某个对象处理它。这样,发送者无需知道哪个对象将处理请求,所有的处理对象都可以尝试处理请求或将请求传递给链上的下一个对象。

核心思想:将请求的发送者与接收者解耦,通过让多个对象组成一条链,使得请求沿着链传递,直到被处理。
责任链模式(Chain of Responsibility Pattern) 是一种行为设计模式,它允许将请求沿着一个处理链传递,直到链中的某个对象处理它。这样,发送者无需知道哪个对象将处理请求,所有的处理对象都可以尝试处理请求或将请求传递给链上的下一个对象。
核心思想:将请求的发送者与接收者解耦,通过让多个对象组成一条链,使得请求沿着链传递,直到被处理。
二、责任链模式的特点
- 解耦请求发出者和处理者:请求的发送者不需要知道具体的处理者是谁,增强了系统的灵活性和扩展性。
- 动态组合处理逻辑:可以根据需要动态改变链的结构,添加或移除处理者。
- 职责单一:责任链模式可以将每个验证逻辑封装到一个独立的处理器中,每个处理器负责单一的验证职责,符合单一职责原则。
- 可扩展性: 增加新的验证逻辑时,处理者只需继承一个统一的接口,并添加新的处理器,而不需要修改现有的代码。
- 清晰的流程: 将所有验证逻辑组织在一起,使得代码结构更加清晰,易于理解。
- 解耦请求发出者和处理者:请求的发送者不需要知道具体的处理者是谁,增强了系统的灵活性和扩展性。
- 动态组合处理逻辑:可以根据需要动态改变链的结构,添加或移除处理者。
- 职责单一:责任链模式可以将每个验证逻辑封装到一个独立的处理器中,每个处理器负责单一的验证职责,符合单一职责原则。
- 可扩展性: 增加新的验证逻辑时,处理者只需继承一个统一的接口,并添加新的处理器,而不需要修改现有的代码。
- 清晰的流程: 将所有验证逻辑组织在一起,使得代码结构更加清晰,易于理解。
三、责任链模式和策略模式结合的意义
- 责任链模式的作用:
- 用于动态处理请求,将多个处理逻辑串联起来。
- 策略模式的作用:
- 用于封装一组算法,使得可以在运行时动态选择需要的算法。
结合两者:
- 责任链模式负责串联和传递请求,而策略模式定义了每一个处理者的具体处理逻辑。
- 两者结合可以实现既动态构建责任链,又灵活应用不同策略来处理请求的需求。
- 责任链模式的作用:
- 用于动态处理请求,将多个处理逻辑串联起来。
- 策略模式的作用:
- 用于封装一组算法,使得可以在运行时动态选择需要的算法。
结合两者:
- 责任链模式负责串联和传递请求,而策略模式定义了每一个处理者的具体处理逻辑。
- 两者结合可以实现既动态构建责任链,又灵活应用不同策略来处理请求的需求。
四、责任链模式解决的问题
- 耦合过高:将请求的处理者从请求的发送者中解耦,使得处理者可以独立扩展或变更。
- 复杂的多条件判断:避免在代码中使用过多
if-else
或 switch-case
语句。 - 灵活性不足:通过链的动态组合可以轻松调整请求的传递逻辑或插入新的处理者。
- 代码重复:每个处理者只专注于处理它关心的部分,减少重复代码。
- 耦合过高:将请求的处理者从请求的发送者中解耦,使得处理者可以独立扩展或变更。
- 复杂的多条件判断:避免在代码中使用过多
if-else
或switch-case
语句。 - 灵活性不足:通过链的动态组合可以轻松调整请求的传递逻辑或插入新的处理者。
- 代码重复:每个处理者只专注于处理它关心的部分,减少重复代码。
五、代码中的责任链模式解析
场景 1:商品上架逻辑(多重校验)
实现一个类似的场景——商品上架逻辑(如校验商品信息、库存信息等),可以按照以下步骤实现:
- 定义责任链抽象接口
public interface MerchantAdminAbstractChainHandler extends Ordered {
/**
* 执行责任链逻辑
*
* @param requestParam 责任链执行入参
*/
void handler(T requestParam);
/**
* @return 责任链组件标识
*/
String mark();
}
- 定义商品上架的责任链标识:
public enum ChainBizMarkEnum {
MERCHANT_ADMIN_CREATE_PRODUCT_TEMPLATE_KEY,
MERCHANT_ADMIN_PRODUCT_UPSHELF_KEY; // 新增商品上架责任链标识
}
- 定义每个处理器的通用行为:
@Component
public final class MerchantAdminChainContext implements ApplicationContextAware, CommandLineRunner {
/**
* 应用上下文,通过Spring IOC获取Bean实例
*/
private ApplicationContext applicationContext;
/**
* 保存商品上架责任链实现类
*
* Key:{@link MerchantAdminAbstractChainHandler#mark()}
* Val:{@link MerchantAdminAbstractChainHandler} 一组责任链实现 Spring Bean 集合
*
* 比如有一个商品上架模板创建责任链,实例如下:
* Key:MERCHANT_ADMIN_CREATE_PRODUCT_TEMPLATE_KEY
* Val:
* - 验证商品信息基本参数是否必填 —— 执行器 {@link ProductInfoNotNullChainFilter}
* - 验证商品库存 —— 执行器 {@link ProductInventoryCheckChainFilter}
*/
private final Map> abstractChainHandlerContainer = new HashMap<>();
/**
* 责任链组件执行
* @param mark 责任链组件标识
* @param requestObj 请求参数
*/
public void handler(String mark,T requestObj){
// 根据 mark 标识从责任链容器中获取一组责任链实现 Bean 集合
List abstractChainHandlers = abstractChainHandlerContainer.get(mark);
if (CollectionUtils.isEmpty(abstractChainHandlers)) {
throw new RuntimeException(String.format("[%s] Chain of Responsibility ID is undefined.", mark));
}
abstractChainHandlers.forEach(each -> each.handler(requestObj));
}
/**
* 执行方法,接收可变参数
* 本方法主要用于初始化和处理商品上架抽象责任链容器
* 它从Spring容器中获取所有MerchantAdminAbstractChainHandler类型的Bean,
* 并根据它们的mark进行分类和排序,以便后续处理
*
* @param args 可变参数,可能包含方法运行所需的额外信息
* @throws Exception 如果方法执行过程中遇到错误,抛出异常
*/
@Override
public void run(String... args) throws Exception {
// 从 Spring IOC 容器中获取指定接口 Spring Bean 集合
Map chainFilterMap = applicationContext.getBeansOfType(MerchantAdminAbstractChainHandler.class);
// 遍历所有获取到的Bean,并将它们根据mark分类存入抽象责任链容器中
chainFilterMap.forEach((beanName, bean) -> {
// 判断 Mark 是否已经存在抽象责任链容器中,如果已经存在直接向集合新增;如果不存在,创建 Mark 和对应的集合
List abstractChainHandlers = abstractChainHandlerContainer.getOrDefault(bean.mark(), new ArrayList<>());
abstractChainHandlers.add(bean);
abstractChainHandlerContainer.put(bean.mark(), abstractChainHandlers);
});
// 遍历抽象责任链容器,对每个 Mark 对应的责任链实现类集合进行排序
abstractChainHandlerContainer.forEach((mark, chainHandlers) -> {
// 对每个 Mark 对应的责任链实现类集合进行排序,优先级小的在前
chainHandlers.sort(Comparator.comparing(Ordered::getOrder));
});
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
- 定义商品上架的责任链处理器:
@Component
public class ProductInfoNotNullChainFilter implements MerchantAdminAbstractChainHandler {
@Override
public void handler(ProductUpShelfReqDTO requestParam) {
if (StringUtils.isEmpty(requestParam.getProductName())) {
throw new RuntimeException("商品名称不能为空!");
}
if (requestParam.getPrice() == null || requestParam.getPrice() <= 0) {
throw new RuntimeException("商品价格必须大于0!");
}
System.out.println("商品信息非空校验通过");
}
@Override
public int getOrder() {
return 1;
}
@Override
public String mark() {
return ChainBizMarkEnum.MERCHANT_ADMIN_PRODUCT_UPSHELF_KEY.name();
}
}
@Component
public class ProductInventoryCheckChainFilter implements MerchantAdminAbstractChainHandler {
@Override
public void handler(ProductUpShelfReqDTO requestParam) {
if (requestParam.getStock() <= 0) {
throw new RuntimeException("商品库存不足,无法上架!");
}
System.out.println("商品库存校验通过");
}
@Override
public int getOrder() {
return 2;
}
@Override
public String mark() {
return ChainBizMarkEnum.MERCHANT_ADMIN_PRODUCT_UPSHELF_KEY.name();
}
}
- 调用责任链进行处理:
@Service
@RequiredArgsConstructor
public class ProductServiceImpl {
private final MerchantAdminChainContext merchantAdminChainContext;
public void upShelfProduct(ProductUpShelfReqDTO requestParam) {
// 调用责任链进行校验
merchantAdminChainContext.handler(
ChainBizMarkEnum.MERCHANT_ADMIN_PRODUCT_UPSHELF_KEY.name(),
requestParam
);
System.out.println("商品上架逻辑开始执行...");
// 后续的商品上架业务逻辑
}
}
实现一个类似的场景——商品上架逻辑(如校验商品信息、库存信息等),可以按照以下步骤实现:
- 定义责任链抽象接口
public interface MerchantAdminAbstractChainHandler extends Ordered {
/**
* 执行责任链逻辑
*
* @param requestParam 责任链执行入参
*/
void handler(T requestParam);
/**
* @return 责任链组件标识
*/
String mark();
}
- 定义商品上架的责任链标识:
public enum ChainBizMarkEnum {
MERCHANT_ADMIN_CREATE_PRODUCT_TEMPLATE_KEY,
MERCHANT_ADMIN_PRODUCT_UPSHELF_KEY; // 新增商品上架责任链标识
}
@Component
public final class MerchantAdminChainContext implements ApplicationContextAware, CommandLineRunner {
/**
* 应用上下文,通过Spring IOC获取Bean实例
*/
private ApplicationContext applicationContext;
/**
* 保存商品上架责任链实现类
*
* Key:{@link MerchantAdminAbstractChainHandler#mark()}
* Val:{@link MerchantAdminAbstractChainHandler} 一组责任链实现 Spring Bean 集合
*
* 比如有一个商品上架模板创建责任链,实例如下:
* Key:MERCHANT_ADMIN_CREATE_PRODUCT_TEMPLATE_KEY
* Val:
* - 验证商品信息基本参数是否必填 —— 执行器 {@link ProductInfoNotNullChainFilter}
* - 验证商品库存 —— 执行器 {@link ProductInventoryCheckChainFilter}
*/
private final Map
/**
* 责任链组件执行
* @param mark 责任链组件标识
* @param requestObj 请求参数
*/
public void handler(String mark,T requestObj){
// 根据 mark 标识从责任链容器中获取一组责任链实现 Bean 集合
List
if (CollectionUtils.isEmpty(abstractChainHandlers)) {
throw new RuntimeException(String.format("[%s] Chain of Responsibility ID is undefined.", mark));
}
abstractChainHandlers.forEach(each -> each.handler(requestObj));
}
/**
* 执行方法,接收可变参数
* 本方法主要用于初始化和处理商品上架抽象责任链容器
* 它从Spring容器中获取所有MerchantAdminAbstractChainHandler类型的Bean,
* 并根据它们的mark进行分类和排序,以便后续处理
*
* @param args 可变参数,可能包含方法运行所需的额外信息
* @throws Exception 如果方法执行过程中遇到错误,抛出异常
*/
@Override
public void run(String... args) throws Exception {
// 从 Spring IOC 容器中获取指定接口 Spring Bean 集合
Map
// 遍历所有获取到的Bean,并将它们根据mark分类存入抽象责任链容器中
chainFilterMap.forEach((beanName, bean) -> {
// 判断 Mark 是否已经存在抽象责任链容器中,如果已经存在直接向集合新增;如果不存在,创建 Mark 和对应的集合
List
abstractChainHandlers.add(bean);
abstractChainHandlerContainer.put(bean.mark(), abstractChainHandlers);
});
// 遍历抽象责任链容器,对每个 Mark 对应的责任链实现类集合进行排序
abstractChainHandlerContainer.forEach((mark, chainHandlers) -> {
// 对每个 Mark 对应的责任链实现类集合进行排序,优先级小的在前
chainHandlers.sort(Comparator.comparing(Ordered::getOrder));
});
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
@Component
public class ProductInfoNotNullChainFilter implements MerchantAdminAbstractChainHandler {
@Override
public void handler(ProductUpShelfReqDTO requestParam) {
if (StringUtils.isEmpty(requestParam.getProductName())) {
throw new RuntimeException("商品名称不能为空!");
}
if (requestParam.getPrice() == null || requestParam.getPrice() <= 0) {
throw new RuntimeException("商品价格必须大于0!");
}
System.out.println("商品信息非空校验通过");
}
@Override
public int getOrder() {
return 1;
}
@Override
public String mark() {
return ChainBizMarkEnum.MERCHANT_ADMIN_PRODUCT_UPSHELF_KEY.name();
}
}
@Component
public class ProductInventoryCheckChainFilter implements MerchantAdminAbstractChainHandler {
@Override
public void handler(ProductUpShelfReqDTO requestParam) {
if (requestParam.getStock() <= 0) {
throw new RuntimeException("商品库存不足,无法上架!");
}
System.out.println("商品库存校验通过");
}
@Override
public int getOrder() {
return 2;
}
@Override
public String mark() {
return ChainBizMarkEnum.MERCHANT_ADMIN_PRODUCT_UPSHELF_KEY.name();
}
}
@Service
@RequiredArgsConstructor
public class ProductServiceImpl {
private final MerchantAdminChainContext merchantAdminChainContext;
public void upShelfProduct(ProductUpShelfReqDTO requestParam) {
// 调用责任链进行校验
merchantAdminChainContext.handler(
ChainBizMarkEnum.MERCHANT_ADMIN_PRODUCT_UPSHELF_KEY.name(),
requestParam
);
System.out.println("商品上架逻辑开始执行...");
// 后续的商品上架业务逻辑
}
}
上述代码实现了一个基于 责任链模式 的电商系统,主要用于处理复杂的业务逻辑,如商品上架模板的创建。这种模式的设计使得每个业务逻辑通过一个独立的处理器(Handler)进行处理,并将这些处理器串联成一个链,通过统一的入口执行每一步处理操作。
1. 代码的组成部分与职责解析
(1) 责任链抽象接口:MerchantAdminAbstractChainHandler
- 定义了责任链中的基础行为:
void handler(T requestParam)
- 定义了该处理器的具体逻辑。
- 这是责任链的核心方法,每个处理器都会接收到传入的参数
requestParam
,并根据具体的业务逻辑进行相应的处理。
- 设计思想:
T
是一个泛型参数,可以适配不同类型的业务场景(如对象校验、数据处理等)。- 如果某个处理器不满足条件,可以抛出异常或者提供返回值来中断后续处理器的运行。
- 每个处理器只负责完成自己的一部分逻辑,保持模块化设计。
(2) 抽象处理器接口:MerchantAdminAbstractChainHandler
- 定义了责任链中每个节点的通用行为:
void handler(T requestParam)
- 责任链的核心方法,定义了如何处理传入的请求参数
requestParam
。 - 每个实现类都会根据具体的业务需求,在该方法中实现自己的处理逻辑,
比如参数校验、数据转换
等。 - 如果某个处理环节中发生错误,可以通过抛出异常中断责任链的执行。
handler(T requestParam)
:执行具体的处理逻辑。mark()
:返回处理器所属的责任链标识(Mark
)。
- 责任链的核心方法,定义了如何处理传入的请求参数
String mark()
- 返回当前处理器所属的责任链标识(Mark)。
- 不同的责任链可以通过
mark()
值进行分组管理。 - 比如在商品上架创建责任链中,
mark()
可以返回MERCHANT_ADMIN_CREATE_PRODUCT_TEMPLATE_KEY
。
int getOrder()
- 用于定义处理器的执行顺序。
- 通过实现
Ordered
接口的getOrder()
方法,开发者可以灵活地控制每个处理器在责任链中的执行顺序。 - 默认值为
Ordered.LOWEST_PRECEDENCE
(优先级最低),可以根据需求覆盖此方法返回更高的优先级(数值越小优先级越高)。
- 通过继承
Ordered
接口来用于指定处理器的执行顺序,优先级小的会先执行。(模版如下)
import org.springframework.core.Ordered;
/**
* 商家上架责任链处理器抽象接口
*
* @param 处理参数的泛型类型(比如请求参数)
*/
public interface MerchantAdminAbstractChainHandler extends Ordered {
/**
* 执行责任链的具体逻辑
*
* @param requestParam 责任链执行的入参
*/
void handler(T requestParam);
/**
* 获取责任链处理器的标识(mark)
*
* 每个处理器所属的责任链标识需要唯一,用于区分不同的责任链。
*
* @return 责任链组件标识
*/
String mark();
/**
* 获取责任链执行顺序
*
* Spring 的 {@link Ordered} 接口方法,数值越小优先级越高。
* 默认返回 `Ordered.LOWEST_PRECEDENCE`,表示优先级最低。
*
* @return 处理器的执行顺序。
*/
@Override
default int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
(2) 责任链上下文:MerchantAdminChainContext
- 负责管理责任链的初始化和执行:
- 在 Spring 容器启动时 (
CommandLineRunner
),扫描实现了MerchantAdminAbstractChainHandler
接口的所有 Spring Bean,并根据它们的mark()
属性将它们归类到不同的链条中。 - 在链条内部,根据
Ordered
的优先级对处理器进行排序。 - 提供统一的
handler()
方法,根据标识 (Mark
) 执行对应的责任链。
- 在 Spring 容器启动时 (
(3) 业务服务层:ProductInventoryCheckChainFilter
- 通过
MerchantAdminChainContext
调用对应的责任链,完成业务参数校验逻辑。 - 责任链完成校验后,后续可以继续执行其他具体的业务逻辑。
责任链的执行流程
通过 MerchantAdminChainContext
,上述两个处理器会被自动扫描并加载到责任链中。运行时,根据 mark()
和 getOrder()
的值,系统自动按顺序执行它们。
五、Java 实现责任链模式 + 策略模式
以下是实现一个责任链 + 策略模式的完整 Java 示例。
场景:模拟用户请求的审核流程(如普通用户审批、管理员审批、高级管理员审批),并结合不同策略处理请求。
1. 定义处理请求的接口
// 抽象处理者接口
public interface RequestHandler {
// 设置下一个处理者
void setNextHandler(RequestHandler nextHandler);
// 处理请求的方法
void handleRequest(UserRequest request);
}
// 抽象处理者接口
public interface RequestHandler {
// 设置下一个处理者
void setNextHandler(RequestHandler nextHandler);
// 处理请求的方法
void handleRequest(UserRequest request);
}
2. 定义用户请求类
// 请求类
public class UserRequest {
private String userType; // 用户类型(普通用户、管理员等)
private String requestContent; // 请求内容
public UserRequest(String userType, String requestContent) {
this.userType = userType;
this.requestContent = requestContent;
}
public String getUserType() {
return userType;
}
public String getRequestContent() {
return requestContent;
}
}
// 请求类
public class UserRequest {
private String userType; // 用户类型(普通用户、管理员等)
private String requestContent; // 请求内容
public UserRequest(String userType, String requestContent) {
this.userType = userType;
this.requestContent = requestContent;
}
public String getUserType() {
return userType;
}
public String getRequestContent() {
return requestContent;
}
}
3. 定义不同的策略(处理逻辑)
// 策略接口
public interface RequestStrategy {
void process(UserRequest request);
}
// 普通用户处理策略
public class BasicUserStrategy implements RequestStrategy {
@Override
public void process(UserRequest request) {
System.out.println("普通用户的请求正在处理:" + request.getRequestContent());
}
}
// 管理员处理策略
public class AdminUserStrategy implements RequestStrategy {
@Override
public void process(UserRequest request) {
System.out.println("管理员的请求正在处理:" + request.getRequestContent());
}
}
// 高级管理员处理策略
public class SuperAdminStrategy implements RequestStrategy {
@Override
public void process(UserRequest request) {
System.out.println("高级管理员的请求正在处理:" + request.getRequestContent());
}
}
// 策略接口
public interface RequestStrategy {
void process(UserRequest request);
}
// 普通用户处理策略
public class BasicUserStrategy implements RequestStrategy {
@Override
public void process(UserRequest request) {
System.out.println("普通用户的请求正在处理:" + request.getRequestContent());
}
}
// 管理员处理策略
public class AdminUserStrategy implements RequestStrategy {
@Override
public void process(UserRequest request) {
System.out.println("管理员的请求正在处理:" + request.getRequestContent());
}
}
// 高级管理员处理策略
public class SuperAdminStrategy implements RequestStrategy {
@Override
public void process(UserRequest request) {
System.out.println("高级管理员的请求正在处理:" + request.getRequestContent());
}
}
4. 实现责任链模式的处理者
// 具体处理者,结合策略
public class RequestHandlerImpl implements RequestHandler {
private RequestStrategy strategy; // 策略
private RequestHandler nextHandler; // 下一个处理者
public RequestHandlerImpl(RequestStrategy strategy) {
this.strategy = strategy;
}
@Override
public void setNextHandler(RequestHandler nextHandler) {
this.nextHandler = nextHandler;
}
@Override
public void handleRequest(UserRequest request) {
// 策略处理
strategy.process(request);
// 将请求传递给下一个处理者
if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}
// 具体处理者,结合策略
public class RequestHandlerImpl implements RequestHandler {
private RequestStrategy strategy; // 策略
private RequestHandler nextHandler; // 下一个处理者
public RequestHandlerImpl(RequestStrategy strategy) {
this.strategy = strategy;
}
@Override
public void setNextHandler(RequestHandler nextHandler) {
this.nextHandler = nextHandler;
}
@Override
public void handleRequest(UserRequest request) {
// 策略处理
strategy.process(request);
// 将请求传递给下一个处理者
if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}
5. 测试责任链 + 策略模式
public class ChainStrategyExample {
public static void main(String[] args) {
// 创建策略
RequestStrategy basicStrategy = new BasicUserStrategy();
RequestStrategy adminStrategy = new AdminUserStrategy();
RequestStrategy superAdminStrategy = new SuperAdminStrategy();
// 创建责任链处理者,并设置链条
RequestHandler basicHandler = new RequestHandlerImpl(basicStrategy);
RequestHandler adminHandler = new RequestHandlerImpl(adminStrategy);
RequestHandler superAdminHandler = new RequestHandlerImpl(superAdminStrategy);
basicHandler.setNextHandler(adminHandler);
adminHandler.setNextHandler(superAdminHandler);
// 模拟用户请求
UserRequest basicRequest = new UserRequest("普通用户", "请求访问资源 A");
UserRequest adminRequest = new UserRequest("管理员", "请求修改资源 B");
UserRequest superAdminRequest = new UserRequest("高级管理员", "请求删除资源 C");
// 处理请求
System.out.println("处理普通用户请求:");
basicHandler.handleRequest(basicRequest);
System.out.println("\n处理管理员请求:");
adminHandler.handleRequest(adminRequest);
System.out.println("\n处理高级管理员请求:");
superAdminHandler.handleRequest(superAdminRequest);
}
}
public class ChainStrategyExample {
public static void main(String[] args) {
// 创建策略
RequestStrategy basicStrategy = new BasicUserStrategy();
RequestStrategy adminStrategy = new AdminUserStrategy();
RequestStrategy superAdminStrategy = new SuperAdminStrategy();
// 创建责任链处理者,并设置链条
RequestHandler basicHandler = new RequestHandlerImpl(basicStrategy);
RequestHandler adminHandler = new RequestHandlerImpl(adminStrategy);
RequestHandler superAdminHandler = new RequestHandlerImpl(superAdminStrategy);
basicHandler.setNextHandler(adminHandler);
adminHandler.setNextHandler(superAdminHandler);
// 模拟用户请求
UserRequest basicRequest = new UserRequest("普通用户", "请求访问资源 A");
UserRequest adminRequest = new UserRequest("管理员", "请求修改资源 B");
UserRequest superAdminRequest = new UserRequest("高级管理员", "请求删除资源 C");
// 处理请求
System.out.println("处理普通用户请求:");
basicHandler.handleRequest(basicRequest);
System.out.println("\n处理管理员请求:");
adminHandler.handleRequest(adminRequest);
System.out.println("\n处理高级管理员请求:");
superAdminHandler.handleRequest(superAdminRequest);
}
}
六、为何责任链模式和策略模式结合使用?
- 责任链控制流程,策略定义处理逻辑:
- 责任链模式将处理请求的逻辑连接成链,便于动态调整请求传递的流程。
- 策略模式将处理逻辑封装为独立的策略,可以灵活复用和替换。
- 职责分离:
- 责任链模式负责管理请求的传递,策略模式专注于实现具体的业务逻辑。
- 结合使用可以让代码结构更清晰,职责分配更明确。
- 增强灵活性和可扩展性:
- 责任链可以动态增删处理者,策略可以动态选择或扩展新的处理逻辑,两者结合大大增强了系统的适配性和扩展性。
- 责任链控制流程,策略定义处理逻辑:
- 责任链模式将处理请求的逻辑连接成链,便于动态调整请求传递的流程。
- 策略模式将处理逻辑封装为独立的策略,可以灵活复用和替换。
- 职责分离:
- 责任链模式负责管理请求的传递,策略模式专注于实现具体的业务逻辑。
- 结合使用可以让代码结构更清晰,职责分配更明确。
- 增强灵活性和可扩展性:
- 责任链可以动态增删处理者,策略可以动态选择或扩展新的处理逻辑,两者结合大大增强了系统的适配性和扩展性。
通过责任链模式与策略模式的结合,可以应对复杂的处理流程和多变的业务需求,同时保持代码的简洁与高内聚的设计结构。
来源:juejin.cn/post/7457366224823124003
权限模型-ABAC模型
权限模型-ABAC模型
📝 ABAC 的概念
ABAC 的概念
ABAC(Attribute-Based Access Control)基于属性的访问控制的权限模型,是一种细粒度的权限控制模型,通过对请求中的各种属性进行分析和匹配,实现对权限的灵活的、动态的控制
。
ABAC(Attribute-Based Access Control)基于属性的访问控制的权限模型,是一种细粒度的权限控制模型,通过对请求中的各种属性进行分析和匹配,实现对权限的灵活的、动态的控制
。
ABAC 的组成部分
主体(Subject)
: 发起访问资源请求的用户或实体(如应用程序、系统)。主体具有多种属性,例如角色、身份、部门、敏感级别、创建时间。
💡Tips: 实际中可能就是存储用户信息的记录表、发起请求的设备信息等。
对象(Object)
: 被访问的资源或数据。对象可以是文件、数据库表、API 接口等,同样具备多种属性,如文件名、文件类型、敏感级别、创建时间等。操作(Action)
: 用户试图对资源的操作 ,例如”读”、“写”、“创建“、”删除“、”复制“等。环境(Environment)
: 外部环境属性,如访问时间、地点、网络状态、安全级别等,用于动态调整访问策略。策略(Policy)
: 定义允许或拒绝的访问的规则。策略基于主体、对象、操作和环境属性的组合,通过逻辑规则决定是否允许访问。
💡Tips: 策略如何定义?
一般来说 策略都有自己的语法设计,以 XML、JSON 这种形式去描述一个访问策略如何构成。 策略也是访问规则,本文中不再做区分。
主体(Subject)
: 发起访问资源请求的用户或实体(如应用程序、系统)。主体具有多种属性,例如角色、身份、部门、敏感级别、创建时间。
💡Tips: 实际中可能就是存储用户信息的记录表、发起请求的设备信息等。
对象(Object)
: 被访问的资源或数据。对象可以是文件、数据库表、API 接口等,同样具备多种属性,如文件名、文件类型、敏感级别、创建时间等。操作(Action)
: 用户试图对资源的操作 ,例如”读”、“写”、“创建“、”删除“、”复制“等。环境(Environment)
: 外部环境属性,如访问时间、地点、网络状态、安全级别等,用于动态调整访问策略。策略(Policy)
: 定义允许或拒绝的访问的规则。策略基于主体、对象、操作和环境属性的组合,通过逻辑规则决定是否允许访问。
💡Tips: 策略如何定义?
一般来说 策略都有自己的语法设计,以 XML、JSON 这种形式去描述一个访问策略如何构成。 策略也是访问规则,本文中不再做区分。
ABAC 工作的基本原理
ABAC 的基本原理是:系统根据主体、对象、操作、环境的属性,以及预定义的策略,动态生成访问决策。
简单流程
- 发起请求 : 通常一个访问请求由主体、对象(资源)、环境、操作中的一个或者多个组成。每个组成部分又有各自的属性,需要用到各个组成部分的属性,去动态构建一个访问规则。
例如 中午 12 点后禁止 A 部门的人访问 B 系统这个访问规则。
中午 12 点以后:环境(时间属性)
A 部门的人:主体(身份属性)
B 系统:对象(被访问的资源)
访问: 操作
- 匹配属性:在预设的访问规则库中查找与请求匹配的规则或规则集合。
- 规则评估:根据匹配的访问规则中的具体规则来评估请求。将请求中的属性值与规则中的条件进行对比,判断请求是否满足规则中的条件。例如请求中包含了访问的时间在 12 点以后,那么访问控制系统就会对比访问时间这个属性值。
- 返回结果:向用户返回最终的规则执行的结果。
💡Tips: 图中只是演示了一个大致的工作流程,实际要设计一个 ABAC 权限系统要复杂的多。
因为所有的规则条件是动态的、逻辑也是动态执行的。
ABAC的难点
试想以下场景:
- 当前文档是文档的拥有者且是拥有者才能编辑。
- 售卖的产品只能是上海地区的用户才能可见。
- 中午 12 点后禁止 A 部门的人访问 B 系统。
如果使用 RBAC 模型很难实现以上的需求。RBAC 是静态的权限模型
,没有对象的属性动态参与计算的,所以很难实现以上场景。
ABAC 系统非常灵活但实现比较困难,
1.属性收集和管理复杂度
- 属性管理
访问规则依赖属性
和属性值
去构建,特别是动态属性(实时位置,时间),如何确保获取到最新的属性值。
💡Tips: 属性是否可以动态增加也是构建系统的一部分,例如用户属性中你增加了一项职业,那该属性的值类型和获取该值的方法如何定义也是属性管理的一个难点。
- 数据一致性和同步
分布式系统中,各种属性的数据源可能分散在不同的系统中,如何准确的、高效的获取该属性和属性值。
访问规则依赖属性
和属性值
去构建,特别是动态属性(实时位置,时间),如何确保获取到最新的属性值。
💡Tips: 属性是否可以动态增加也是构建系统的一部分,例如用户属性中你增加了一项职业,那该属性的值类型和获取该值的方法如何定义也是属性管理的一个难点。
分布式系统中,各种属性的数据源可能分散在不同的系统中,如何准确的、高效的获取该属性和属性值。
2.访问规则的复杂度
- 条件逻辑
构建一个访问规则通常包含复杂的条件,条件可能是大于、小于、区间、地理位置等。这些条件需要仔细的定义和维护。
- 多条件组合
访问规则需要涵盖不同属性的组合条件,属性组合的数量随着属性的增加呈指数型增长。
- 策略管理
如果访问规则在数量一直增长,访问规则的生命周期(更新、删除)将变得复杂。如果其中属性的变动也会影响现有存在的访问规则。
- 动态性
ABAC进行决策时需要实时评估所有相关属性的当前值,并与策略条件进行匹配。这种评估会增加计算的开销,尤其在处理大量请求时对计算资源要求更高。
构建一个访问规则通常包含复杂的条件,条件可能是大于、小于、区间、地理位置等。这些条件需要仔细的定义和维护。
访问规则需要涵盖不同属性的组合条件,属性组合的数量随着属性的增加呈指数型增长。
如果访问规则在数量一直增长,访问规则的生命周期(更新、删除)将变得复杂。如果其中属性的变动也会影响现有存在的访问规则。
ABAC进行决策时需要实时评估所有相关属性的当前值,并与策略条件进行匹配。这种评估会增加计算的开销,尤其在处理大量请求时对计算资源要求更高。
3.透明性
- 可追溯性
ABAC的动态决策过程复杂,审计和跟踪某个过程中的条件匹配和组合算法变得困难。为了便于审计和问题排查,ABAC 系统通常需要记录详细的决策日志,这增加了额外的复杂性。
- 决策透明度
复杂的条件和组合逻辑使得管理员在排查和解释某个请求的决策较为困难,用户请求被拒绝可能难以理解其原因,这对系统性提出了挑战。
ABAC的动态决策过程复杂,审计和跟踪某个过程中的条件匹配和组合算法变得困难。为了便于审计和问题排查,ABAC 系统通常需要记录详细的决策日志,这增加了额外的复杂性。
复杂的条件和组合逻辑使得管理员在排查和解释某个请求的决策较为困难,用户请求被拒绝可能难以理解其原因,这对系统性提出了挑战。
ABAC 的实现
标准实现-XACML
XACML 是一种基于 XML 的标准访问控制控制策略语言,用于定义和管理复杂的访问控制需求。
💡Tips: XACML 是 ABAC 的一个标准实现,用 XML 文件定义访问的策略集,然后提交 XACML 引擎来进行权限决策。由于这个太过复杂,这里不讲述了。有兴趣的可以看下官网XACML version 3.0。(当然还有其他标准的实现)
ABAC 的权限系统设计的核心
目前 ABAC 系统没有单独使用的,基本都是搭配RBAC (基于角色的权限模型)来使用。目前已有的类似方案如 AWS的 IAM (Identity And Access Management)也都是借鉴了 ABAC 的设计理念来实现的精细权限控制。
一个 ABAC 系统通常需要考虑以下核心的三个关键步骤:
- 属性管理:属性的定义、结构和属性值的获取。
- 访问规则:访问规则的结构化和语法定义。
- 规则编辑器:规则编辑器和规则匹配引擎。
💡Tips: 目前这里探讨的设计因为没有具体的场景,这里的所说的设计权做参考。顺便一提属性管理、规则编辑器、访问规则其实设计的思路和CDP系统非常相似。
属性管理
属性有动态属性和静态属性,如性别那就是静态的,年龄、角色、地理位置这些都是动态的。
属性管理的难点是属性的定义和属性值获取。
- 属性的定义
一般来说属性的定义包括属性名、字段类型、来源(获取该属性值的方式)。
业务确认属性的范围,也就是说设计时确认了目前业务要用到哪些属性就不能进行更改(修改、删除)操作。如果是需要动态的进行属性的新增、修改就需要更抽象的灵活设计。
💡Tips: 如果其他的访问规则中使用了该属性,修改和删除都会影响使用该属性的访问规则。
- 属性值的获取
属性和属性值的来源单一
例如用户的属性就一张用户表,那直接读取用户表就好了。如果你的用户数据是分散在不同来源的,需要考虑的如何聚合数据和保证数据一致性的问题。
访问规则
目前现在是有一些ABAC 的设计系统大多都是采用 JSON 语言描述访问规则。该 JSON 中包含了访问规则中所用到的属性、条件、操作等。示例如下:
{
"subject": {
"role": "manager",
"department": "finance"
},
"object": {
"type": "document",
"sensitivity": "confidential"
},
"action": "view",
"environment": {
"ip": "192.168.1.*",
"time": {
"start": "09:00",
"end": "18:00"
}
},
"effect": "allow"
}
💡 实际开发中根据业务场景,系统中描述JSON 中的结构语义都有自己的规范。
规则编辑和规则匹配
ABAC 的核心部分是**如何构建一个规则编辑器和规则匹配引擎
**,这个规则编辑器需要满足各种复杂条件的组合(这里的条件指的是一个条件或多个条件)。这里的条件之间的关系可能不止“且”的关系,可能还存在“或”。
一些地方描述构建规则为动态 SQL 的构建,但是这种方式需要对应的资源需要映射为数据库记录且有对应的属性存在表结构中,简单就是理解是宽表+属性。可有些属性是没办法在数据库结构中体现的如访问位置、访问时间等,这些就需要在规则匹配引擎中做设计了。
💡Tips: 目前所说的规则编辑和规则匹配都是为了动态 SQL 的构建,这块比较有通用性(有些数据权限设计采用的就是该种思路。)。至于其他方式需要考虑具体的业务环境。
规则编辑器通常都设计成管理界面,通过界面上选取中的属性和条件构成一个JSON ,然后提交到规则匹配引擎去执行,将JSON 转换成动态 SQL,发起访问请求时去拿到访问规则构建的SQL去执行。
💡Tips 这里的规则匹配引擎最主要的工作就是根据 JSON 中的描述的规则,动态生成一段 SQL。
总结
事实上 ABAC 现在没有什么标准建模,借鉴ABAC 的设计思维达到你想要的基于属性控制权限就可以了。至于采用何种方式、是否搭配其他权限模型,具体业务、具体分析。
来源:juejin.cn/post/7445219433017376780
用java做一套离线且免费的智能语音系统,ASR+LLM+TTS
其实调用第三方接口完成一个智能语音系统是非常简单的,像阿里、科大讯飞、微软都有相关接口,直接根据官方文档集成就可以,但想要离线的就要麻烦一点了,主要是想不花钱,现在人工智能基本是python的天下,不得不感慨,再不学python感觉自己要被淘汰了。
言归正传,首先说一下标题中的ASR+LLM+TTS,ASR就是语音识别,LLM就是大语言模型,TTS就是文字转语音,要想把这几个功能做好对电脑性能要求还是蛮高的,本次方案是一个新的尝试也是减少对性能的消耗
1.先看效果
生成的音频效果放百度网盘 通过网盘分享的文件:result.wav
链接: pan.baidu.com/s/19ImtqunH… 提取码: hm67
听完之后是不是感觉效果很好,但是。。。后面再说吧
2.如何做
2.1ASR功能
添加依赖
<!-- 获取音频信息 -->
<dependency>
<groupId>org</groupId>
<artifactId>jaudiotagger</artifactId>
<version>2.0.3</version>
</dependency>
<!-- 语音识别 -->
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.7.0</version>
</dependency>
<dependency>
<groupId>com.alphacephei</groupId>
<artifactId>vosk</artifactId>
<version>0.3.32</version>
</dependency>
代码实现,需要提前下载模型,去vosk官网下载:VOSK Models (alphacephei.com)中文模型一个大的一个小的,小的识别速度快准确率低,大的识别速度慢准确率高
提前预加载模型,提升识别速度
private static final Model model = loadModel();
private static Model loadModel() {
try {
String path=System.getProperty("user.dir");
return new Model(path+"\vosk-model-small-cn-0.22");
} catch (Exception e) {
throw new RuntimeException("Failed to load model", e);
}
}
语音转文字方法实现
public String voiceToText(String filePath) {
File file = new File(filePath);
LibVosk.setLogLevel(LogLevel.DEBUG);
String msg = null;
int sampleRate = 0;
RandomAccessFile rdf = null;
/**
* "r": 以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。
* "rw": 打开以便读取和写入。
* "rws": 打开以便读取和写入。相对于 "rw","rws" 还要求对“文件的内容”或“元数据”的每个更新都同步写入到基础存储设备。
* "rwd" : 打开以便读取和写入,相对于 "rw","rwd" 还要求对“文件的内容”的每个更新都同步写入到基础存储设备。
*/
try {
rdf = new RandomAccessFile(file, "r");
sampleRate=toInt(read(rdf));
System.out.println(file.getName() + " SampleRate:" + sampleRate); // 采样率、音频采样级别 8000 = 8KHz
rdf.close();
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
try (
InputStream ais = AudioSystem.getAudioInputStream(file);
Recognizer recognizer = new Recognizer(model, 16000)) {
int bytes;
byte[] b = new byte[1024];
while ((bytes = ais.read(b)) >= 0) {
recognizer.acceptWaveForm(b, bytes);
}
String result=recognizer.getResult();
JSONObject jsonObject = JSONObject.parseObject(result);
msg=jsonObject.getString("text");
} catch (UnsupportedAudioFileException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
return msg;
}
2.2LLM问答功能
这个就要请出神奇的羊驼ollama(链接:Ollama),下载即用非常简单,可以运行大部分主流大语言模型,在官网models选择要加载的模型在控制台运行对应的命令即可
添加依赖
<dependency>
<groupId>io.springboot.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
<version>1.0.3</version>
</dependency>
加入配置
spring:
ai:
ollama:
base-url: http://10.3.0.178:11434 //接口地址 默认端口11434
chat:
options:
model: qwen2 //模型名称
enabled: true
代码实现,引入OllamaChatClient,然后调用call方法
@Resource
private OllamaChatClient ollamaChatClient;
//msg为提问的信息
String ask=ollamaChatClient.call(msg);
2.3TTS功能
添加依赖
<dependency>
<groupId>com.hynnet</groupId>
<artifactId>jacob</artifactId>
<version>1.18</version>
</dependency>
代码实现
public boolean localTextToSpeech(String text, int volume, int speed,String outPath) {
try {
// 调用dll朗读方法
ActiveXComponent ax = new ActiveXComponent("Sapi.SpVoice");
// 音量 0 - 100
ax.setProperty("Volume", new Variant(volume));
// 语音朗读速度 -10 到 +10
ax.setProperty("Rate", new Variant(speed));
// 输入的语言内容
Dispatch dispatch = ax.getObject();
// 本地执行朗读
// Dispatch.call(dispatch, "Speak", new Variant(text));
//开始生成语音文件,构建文件流
ax = new ActiveXComponent("Sapi.SpFileStream");
Dispatch sfFileStream = ax.getObject();
//设置文件生成格式
ax = new ActiveXComponent("Sapi.SpAudioFormat");
Dispatch fileFormat = ax.getObject();
// 设置音频流格式
Dispatch.put(fileFormat, "Type", new Variant(22));
// 设置文件输出流格式
Dispatch.putRef(sfFileStream, "Format", fileFormat);
// 调用输出文件流打开方法,创建一个音频文件
Dispatch.call(sfFileStream, "Open", new Variant(outPath), new Variant(3), new Variant(true));
// 设置声音对应输出流为输出文件对象
Dispatch.putRef(dispatch, "AudioOutputStream", sfFileStream);
// 设置音量
Dispatch.put(dispatch, "Volume", new Variant(volume));
// 设置速度
Dispatch.put(dispatch, "Rate", new Variant(speed));
// 执行朗读
Dispatch.call(dispatch, "Speak", new Variant(text));
// 关闭输出文件
Dispatch.call(sfFileStream, "Close");
Dispatch.putRef(dispatch, "AudioOutputStream", null);
// 关闭资源
sfFileStream.safeRelease();
fileFormat.safeRelease();
// 关闭朗读的操作
dispatch.safeRelease();
ax.safeRelease();
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
到此为止功能就全部实现了,在不联网的情况下也可以免费使用,但这个TTS生成出来的语音太机器了,所以在上面的示例中我说但是,因为机器音实在太难受了,所以用了另一个方案,但这个方案需要联网,暂时看是不需要收费(后续使用发现是有接口调用次数限制)
,这个大家就看原作者介绍吧(ikfly/java-tts: java-tts 文本转语音 (github.com))
最终实现效果来看还是能接受的吧,整个过程速度还比较快,python的语音相关项目也看了很多,如果部署一个python的TTS服务,效果可能还会好点,但是我试过的速度都有点慢啊,还是容我再继续研究一下吧
来源:juejin.cn/post/7409329136555048999
从MySQL迁移到PostgreSQL经验总结
背景
最近一两周在做从MySQL迁移到PostgreSQL
的任务(新项目,历史包袱较小,所以迁移比较顺利), 感觉还是有一些知识,可以拿出来分享,希望对大家有所帮助。
最近一两周在做从MySQL迁移到PostgreSQL
的任务(新项目,历史包袱较小,所以迁移比较顺利), 感觉还是有一些知识,可以拿出来分享,希望对大家有所帮助。
为什么要转到PostgreSQL
因架构团队安全组安全需求,需要将Mysql迁移到PostgreSQL。实际迁移下来,发现PostgreSQL挺优秀的,比MySQL严谨很多,很不错。
因架构团队安全组安全需求,需要将Mysql迁移到PostgreSQL。实际迁移下来,发现PostgreSQL挺优秀的,比MySQL严谨很多,很不错。
迁移经验
引入PostgreSQL驱动,调整链接字符串
pagehelper方言调整
涉及order, group,name, status, type 等关键字,要用引号
括起来
JSON字段及JsonTypeHandler
项目中用到了比较多的JSON字段。在mysql中,也有JSON字段类型,但是有时候我们用了varchar或text,在mybatis typehandler中是当成字符来处理的。但是在postgresql中,相对严谨,如果字段类型是json,那么在java中会被封装为PGObject,所以我们原来的JsonTypeHandler就要被改造。
/**
* JSON类型处理器
*
* @author james.h.fu
* @create 2024/10/9 20:45
*/
@Slf4j
public class JsonTypeHandler extends BaseTypeHandler {
private static final ObjectMapper mapper = new ObjectMapper();
static {
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, Boolean.FALSE);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
private final Class clazz;
private TypeReferenceextends T> typeReference;
public JsonTypeHandler(Class clazz) {
if (clazz == null) throw new IllegalArgumentException("Type argument cannot be null");
this.clazz = clazz;
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
setObject(ps, i, parameter);
}
@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
return toObject(rs, columnName);
}
@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return toObject(rs, columnIndex);
}
@Override
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return toObject(cs, columnIndex);
}
protected TypeReferenceextends T> getTypeReference() {
return new TypeReference() {};
}
private String toJson(T object) {
try {
return mapper.writeValueAsString(object);
} catch (Exception ex) {
log.error("JsonTypeHandler error on toJson content:{}", JsonUtil.toJson(object), ex);
throw new RuntimeException("JsonTypeHandler error on toJson", ex);
}
}
private T toObject(String content) {
if (!StringUtils.hasText(content)) {
return null;
}
try {
if (clazz.getName().equals("java.util.List")) {
if (Objects.isNull(typeReference)) {
typeReference = getTypeReference();
}
return (T) mapper.readValue(content, typeReference);
}
return mapper.readValue(content, clazz);
} catch (Exception ex) {
log.error("JsonTypeHandler error on toObject content:{},class:{}", content, clazz.getName(), ex);
throw new RuntimeException("JsonTypeHandler error on toObject", ex);
}
}
// protected boolean isPostgre() {
// SqlSessionFactory sqlSessionFactory = SpringUtil.getBean(SqlSessionFactory.class);
// Configuration conf = sqlSessionFactory.getConfiguration();
// DataSource dataSource = conf.getEnvironment().getDataSource();
// try (Connection connection = dataSource.getConnection()) {
// String url = connection.getMetaData().getURL();
// return url.contains("postgresql");
// } catch (SQLException e) {
// throw new RuntimeException("Failed to determine database type", e);
// }
// }
@SneakyThrows
private void setObject(PreparedStatement ps, int i, T parameter) {
PGobject jsonObject = new PGobject();
jsonObject.setType("json");
jsonObject.setValue(JsonUtil.toJson(parameter));
ps.setObject(i, jsonObject);
}
@SneakyThrows
private T toObject(ResultSet rs, String columnName) {
Object object = rs.getObject(columnName);
return toObject(object);
}
@SneakyThrows
private T toObject(ResultSet rs, int columnIndex) {
Object object = rs.getObject(columnIndex);
return toObject(object);
}
@SneakyThrows
private T toObject(CallableStatement rs, int columnIndex) {
Object object = rs.getObject(columnIndex);
return toObject(object);
}
public T toObject(Object object) {
if (object instanceof String json) {
return this.toObject(json);
}
if (object instanceof PGobject pgObject) {
String json = pgObject.getValue();
return this.toObject(json);
}
return null;
}
}
<result column="router_info" jdbcType="OTHER" property="routerInfo" typeHandler="***.cms.cmslib.mybatis.JsonTypeHandler"/>
<set>
<if test="routerInfo != null">
router_info = #{routerInfo,typeHandler=***.cms.cmslib.mybatis.JsonTypeHandler}
if>
set>
where id = #{id}
如果JSON中存储是的List, Map,Set等类型时, 会存在泛型类型中类型擦除的问题
。因此,如果存在这种情况,我们需要扩展子类,在子类中提供详细的类型信息TypeReference
。
/**
* @author james.h.fu
* @create 2024/12/9 20:45
*/
public class ComponentUpdateListJsonTypeHandler extends JsonTypeHandler>
{
public ComponentUpdateListJsonTypeHandler(Class<List<ComponentUpdate>> clazz) {
super(clazz);
}
@Override
protected TypeReference getTypeReference() {
return new TypeReference<List<ComponentUpdate>>() {
};
}
}
- pgsql不支持mysql insert ignore语法, pgsql提供了类似的语法:
INSERT INTO orders (product_id, user_id)
VALUES (101, 202)
ON CONFLICT (product_id, user_id) DO NOTHING;
项目中用到了比较多的JSON字段。在mysql中,也有JSON字段类型,但是有时候我们用了varchar或text,在mybatis typehandler中是当成字符来处理的。但是在postgresql中,相对严谨,如果字段类型是json,那么在java中会被封装为PGObject,所以我们原来的JsonTypeHandler就要被改造。
/**
* JSON类型处理器
*
* @author james.h.fu
* @create 2024/10/9 20:45
*/
@Slf4j
public class JsonTypeHandler extends BaseTypeHandler {
private static final ObjectMapper mapper = new ObjectMapper();
static {
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, Boolean.FALSE);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
private final Class clazz;
private TypeReferenceextends T> typeReference;
public JsonTypeHandler(Class clazz) {
if (clazz == null) throw new IllegalArgumentException("Type argument cannot be null");
this.clazz = clazz;
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
setObject(ps, i, parameter);
}
@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
return toObject(rs, columnName);
}
@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return toObject(rs, columnIndex);
}
@Override
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return toObject(cs, columnIndex);
}
protected TypeReferenceextends T> getTypeReference() {
return new TypeReference() {};
}
private String toJson(T object) {
try {
return mapper.writeValueAsString(object);
} catch (Exception ex) {
log.error("JsonTypeHandler error on toJson content:{}", JsonUtil.toJson(object), ex);
throw new RuntimeException("JsonTypeHandler error on toJson", ex);
}
}
private T toObject(String content) {
if (!StringUtils.hasText(content)) {
return null;
}
try {
if (clazz.getName().equals("java.util.List")) {
if (Objects.isNull(typeReference)) {
typeReference = getTypeReference();
}
return (T) mapper.readValue(content, typeReference);
}
return mapper.readValue(content, clazz);
} catch (Exception ex) {
log.error("JsonTypeHandler error on toObject content:{},class:{}", content, clazz.getName(), ex);
throw new RuntimeException("JsonTypeHandler error on toObject", ex);
}
}
// protected boolean isPostgre() {
// SqlSessionFactory sqlSessionFactory = SpringUtil.getBean(SqlSessionFactory.class);
// Configuration conf = sqlSessionFactory.getConfiguration();
// DataSource dataSource = conf.getEnvironment().getDataSource();
// try (Connection connection = dataSource.getConnection()) {
// String url = connection.getMetaData().getURL();
// return url.contains("postgresql");
// } catch (SQLException e) {
// throw new RuntimeException("Failed to determine database type", e);
// }
// }
@SneakyThrows
private void setObject(PreparedStatement ps, int i, T parameter) {
PGobject jsonObject = new PGobject();
jsonObject.setType("json");
jsonObject.setValue(JsonUtil.toJson(parameter));
ps.setObject(i, jsonObject);
}
@SneakyThrows
private T toObject(ResultSet rs, String columnName) {
Object object = rs.getObject(columnName);
return toObject(object);
}
@SneakyThrows
private T toObject(ResultSet rs, int columnIndex) {
Object object = rs.getObject(columnIndex);
return toObject(object);
}
@SneakyThrows
private T toObject(CallableStatement rs, int columnIndex) {
Object object = rs.getObject(columnIndex);
return toObject(object);
}
public T toObject(Object object) {
if (object instanceof String json) {
return this.toObject(json);
}
if (object instanceof PGobject pgObject) {
String json = pgObject.getValue();
return this.toObject(json);
}
return null;
}
}
<result column="router_info" jdbcType="OTHER" property="routerInfo" typeHandler="***.cms.cmslib.mybatis.JsonTypeHandler"/>
<set>
<if test="routerInfo != null">
router_info = #{routerInfo,typeHandler=***.cms.cmslib.mybatis.JsonTypeHandler}
if>
set>
where id = #{id}
如果JSON中存储是的List泛型类型中类型擦除的问题
。因此,如果存在这种情况,我们需要扩展子类,在子类中提供详细的类型信息TypeReference
。
/**
* @author james.h.fu
* @create 2024/12/9 20:45
*/
public class ComponentUpdateListJsonTypeHandler extends JsonTypeHandler>
{
public ComponentUpdateListJsonTypeHandler(Class<List<ComponentUpdate>> clazz) {
super(clazz);
}
@Override
protected TypeReference getTypeReference() {
return new TypeReference<List<ComponentUpdate>>() {
};
}
}
- pgsql不支持mysql insert ignore语法, pgsql提供了类似的语法:
INSERT INTO orders (product_id, user_id)
VALUES (101, 202)
ON CONFLICT (product_id, user_id) DO NOTHING;
但是与mysql insert ignore并不完全等价, 关于这个点如何改造, 需要结合场景或者业务逻辑来斟酌定夺.
- pgsql也不支持INSERT ... ON DUPLICATE KEY UPDATE, 如果代码中有使用这个语法, pgsql提供了类似的语法:
INSERT INTO users (email, name, age)
VALUES ('test@example.com', 'John', 30)
ON CONFLICT (email)
DO UPDATE SET
name = EXCLUDED.name,
age = EXCLUDED.age;
EXCLUDED 是一个特殊的表别名,用于引用因冲突而被排除(Excluded)的、尝试插入的那条数据.
CONFLICT也可以直接面向唯一性约束. 假如users有一个唯一性约束: unique_email_constraint, 上述SQL可以改成:
INSERT INTO users (email, name, age)
VALUES ('test@example.com', 'John', 30)
ON CONFLICT ON CONSTRAINT unique_email_constraint
DO UPDATE SET
name = EXCLUDED.name,
age = EXCLUDED.age;
- 分页:mysql的分页使用的是: limit B(offset),A(count), 但是pgsql不支持这种语法, pgsql支持的是如下两种:
(1)、limit A offset B; (2)、OFFSET B ROWS FETCH NEXT A ROWS ONLY;
- pgsql查询区分大小写, 而mysql是不区分的
- 其它情况 (1)、代码中存在取1个数据的场景,原来mysql写法是
limit 0,1
, 要调整为limit 1
; (2)、在mysql中BIT(1)或tinyint(值0,1)可以转换为Boolean。但是在pgsql中不支持。需要明确使用boolean类型或INT类型, 或者使用typerhandler处理。
ALTER TABLE layout
ALTER COLUMN init_instance TYPE INT2
USING CASE
WHEN init_instance = B'1' THEN 1
WHEN init_instance = B'0' THEN 0
ELSE NULL
END;
update component c
set init_instance = cp.init_instance
from component_publish cp
where c.init_instance is null and c.id = cp.component_id ;
(3)、迁移数据后,统一将自增列修改
DO $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN
SELECT
tc.sequencename
FROM pg_sequences tc
LOOP
EXECUTE format('ALTER SEQUENCE %I RESTART WITH 100000', rec.sequencename);
RAISE NOTICE 'Reset sequence % to 100000', rec.sequencename;
END LOOP;
END $$;
总结
在日常开发中,我们一定要再严谨一些,规范编码。这样能让写我的代码质量更好,可移植性更高。
附录
在PostgreSQL 中,有哪些数据类型?
PostgreSQL 支持多种数据类型,下面列出一些常用的数据类型:
- 数值类型
smallint
:2字节整数integer
:4字节整数bigint
:8字节整数decimal
或numeric
:任意精度的数值real
:4字节浮点数double precision
:8字节浮点数smallserial
:2字节序列整数serial
:4字节序列整数bigserial
:8字节序列整数
- 字符与字符串类型
character varying(n)
或varchar(n)
:变长字符串,最大长度为ncharacter(n)
或char(n)
:定长字符串,长度为ntext
:变长字符串,没有长度限制
- 日期/时间类型
date
:存储日期(年月日)time [ (p) ] [ without time zone ]
:存储时间(时分秒),可指定精度p,默认不带时区time [ (p) ] with time zone
:存储时间(时分秒),可指定精度p,带时区timestamp [ (p) ] [ without time zone ]
:存储日期和时间,默认不带时区timestamp [ (p) ] with time zone
:存储日期和时间,带时区interval
:存储时间间隔
- 布尔类型
boolean
:存储真或假值
- 二进制数据类型
bytea
:存储二进制字符串
- 几何类型
point
:二维坐标点line
:无限长直线lseg
:线段box
:矩形框path
:闭合路径或多边形polygon
:多边形circle
:圆
- 网络地址类型
cidr
:存储IPv4或IPv6网络地址inet
:存储IPv4或IPv6主机地址和可选的CIDR掩码macaddr
:存储MAC地址
- 枚举类型
enum
:用户定义的一组排序标签
- 位串类型
bit( [n] )
:固定长度位串bit varying( [n] )
:变长位串
- JSON类型
json
:存储JSON数据jsonb
:存储JSON数据,以二进制形式存储,并支持查询操作
- UUID类型
uuid
:存储通用唯一标识符
- XML类型
xml
:存储XML数据
这些数据类型可以满足大多数应用的需求。在创建表时,根据实际需要选择合适的数据类型是非常重要的。
在MyBatis中,jdbcType有哪些?
jdbcType
是 MyBatis 和其他 JDBC 相关框架中用于指定 Java 类型和 SQL 类型之间映射的属性。以下是常见的 jdbcType
值及其对应的 SQL 数据类型:
- NULL:表示 SQL NULL 类型
- VARCHAR:表示 SQL VARCHAR 或 VARCHAR2 类型
- CHAR:表示 SQL CHAR 类型
- NUMERIC:表示 SQL NUMERIC 类型
- DECIMAL:表示 SQL DECIMAL 类型
- BIT:表示 SQL BIT 类型
- TINYINT:表示 SQL TINYINT 类型
- SMALLINT:表示 SQL SMALLINT 类型
- INTEGER:表示 SQL INTEGER 类型
- BIGINT:表示 SQL BIGINT 类型
- REAL:表示 SQL REAL 类型
- FLOAT:表示 SQL FLOAT 类型
- DOUBLE:表示 SQL DOUBLE 类型
- DATE:表示 SQL DATE 类型(只包含日期部分)
- TIME:表示 SQL TIME 类型(只包含时间部分)
- TIMESTAMP:表示 SQL TIMESTAMP 类型(包含日期和时间部分)
- BLOB:表示 SQL BLOB 类型(二进制大对象)
- CLOB:表示 SQL CLOB 类型(字符大对象)
- ARRAY:表示 SQL ARRAY 类型
- DISTINCT:表示 SQL DISTINCT 类型
- STRUCT:表示 SQL STRUCT 类型
- REF:表示 SQL REF 类型
- DATALINK:表示 SQL DATALINK 类型
- BOOLEAN:表示 SQL BOOLEAN 类型
- ROWID:表示 SQL ROWID 类型
- LONGNVARCHAR:表示 SQL LONGNVARCHAR 类型
- NVARCHAR:表示 SQL NVARCHAR 类型
- NCHAR:表示 SQL NCHAR 类型
- NCLOB:表示 SQL NCLOB 类型
- SQLXML:表示 SQL XML 类型
- JAVA_OBJECT:表示 SQL JAVA_OBJECT 类型
- OTHER:表示 SQL OTHER 类型
- LONGVARBINARY:表示 SQL LONGVARBINARY 类型
- VARBINARY:表示 SQL VARBINARY 类型
- LONGVARCHAR:表示 SQL LONGVARCHAR 类型
在使用 MyBatis 或其他 JDBC 框架时,选择合适的 jdbcType
可以确保数据正确地在 Java 和数据库之间进行转换。
来源:juejin.cn/post/7460410854775455794
trae 深度体验:使用trae完美开发微信小程序
trae 深度体验:使用trae完美开发微信小程序
安装 trae
安装 trae 教程和使用文档大家可以参考官方文档,很详细。使用过 vscode 的用户几乎可以无缝切换过来。官方文档:docs.trae.ai/docs/what-i…
目前只支持 mac 系统,windows 预计 2 月份上线。
如果遇到下面的错误,请科学上网解决;
trae 项目实战:开发微信小程序
插件安装
要想在 trae 中完美体验小程序开发首先需要安装必要的两个插件WXML、微信小程序开发工具
WXML:微信小程序 .wxml 文件代码高亮,标签、属性的智能补全(同时支持原生小程序、mpvue 和 wepy 框架,并提供 code snippets)
微信小程序开发工具:提供小程序预览、打包上传、代码补全、语法高亮、项目模版等功能
安装 “wxml”插件
按照 vscode、trae 的插件安装方式安装就可以顺利安装:
安装 “微信小程序开发工具”插件
这个工具安装有一些曲折,按照 vscode 的使用习惯,首先在插件市场按名称搜索,结果大出意料,没有😄。
不知道是哪里出现了问题,按照官方文档指引去下载。
打开官方的网址 docs.trae.ai/docs/manage…, 全是英文,没关系,使用豆包 APP 打开网页,让豆包总结网页内容就行 😄:
文档中提到了两种方式:
- 从 Trae 的插件市场中安装(没搜索到微信小程序开发工具插件,此路不通😭)
- 把插件下载到本地,使用本地安装的方式。看下面动图:
右下角提示,直接安装失败!此路也不行。作为一个程序员折腾是我的本能,看看 trae 的 AI 能力能不能提供帮助。
顺便遇到个 bug:
插件安装失败后,图中的两个按钮点击了都没有任何反应,只能重启 trae 才能解决。
- 求助 trae 的 AI
使用快捷键 command + U 打开右侧边栏,输入要问的问题:
看到上图,这个插件我们已经安装,在 trae chat 中给到的建议是里面有 "小程序开发助手"插件,但是没有提到如何安装。
更换模型,在 chat 的对话框右侧点击切换模型,使用 gpt-4o,来解决插件安装的问题:
多次尝试后,回答还是一如既往的固执。
在AI 给到的回复当中有个插件的命令,不过这个命令适合 vscode。
点击运行按钮试试,此时 trae 会自动打开 terminal,直接执行命令
提示安装成功,但是给 vscode 安装了。继续提问:
嗯,还是 vscode 命令,不过也没关系,更换为 trae 就行了:
trae --install-extension /Users/oo7/Downloads/crazyurus.miniprogram-vscode-extension-1.5.1.vsix
等待命令执行完毕:
安装成功。
至此两个插件就安装完毕,可以做小程序的开发了。
小结
在trae中安装用于微信小程序开发的“WXML”和“微信小程序开发工具”插件,过程各有不同:
- “WXML”插件:按照vscode、trae常规的插件安装方式即可顺利安装。
- “微信小程序开发工具”插件:在trae插件市场和vscode插件市场均搜索不到,通过从官方文档下载插件本地安装失败,求助trae的AI起初未得到有效解决,最终通过将适用于vscode的安装命令修改为适用于trae的命令
trae --install-extension /xxxx/crazyurus.miniprogram-vscode-extension-1.5.1.vsix
,成功安装。 - 安装完成两个插件后,即可进行小程序开发。 同时,安装插件失败时存在点击重试和关闭按钮无反应的bug,需重启trae解决。
- 点击
chat
区域的run
按钮一定要检测命令的安全性(不然遇到非法的命令直接运行结果很严重),同时也建议trae
只复制命令到终端即可。
小程序项目开发
在 trae 中开发小程序,还需要下载微信开发者工具,也许有人会问既然有了微信开发者工具为什么还要使用 trae?
- 微信开发者工具编写代码远远没有使用 trae 写代码快,bug 多,没有 AI。
- trae 功能插件丰富、UI nice、拥有免费的 AI👍。
- 微信开发者工具不能少,微信开发者工具有实时渲染,代码检测、性能分析、一键上传代码等微信小程序必须的功能。
使用 微信开发者工具打开你的项目,并点击打开模拟器和分离窗口,如下图:
然后打开 trae 编辑器,在你的桌面布局中配置如下排列方式:
这样我们就可以实现一边写代码一边调试效果的目地。
编写页面
代码编写
我已经有这样一个页面,不过界面太难看了,使用 Trae 来调试他:
页面 wxml 代码 :
<!--pages/tools/index.wxml-->
<navigation-bar
title="{{pageTitle}}"
back="{{false}}"
>
</navigation-bar>
<scroll-view
type="custom"
scroll-y
enable-flex="{{false}}"
scroll-with-animation="{{true}}"
enable-back-to-top="{{true}}"
enable-passive="{{true}}"
show-scrollbar="{{false}}"
refresher-default-style="white"
bounces="{{true}}"
fast-deceleration="{{true}}"
lower-threshold="{{50}}"
style="width: 100%; height: 100%;"
>
<sticky-section>
<view class="toolbox" wx:if="{{tools.length > 0}}">
<view class="item" wx:for="{{toolList}}">
<navigator open-type="navigate" hover-class url="{{item.url}}">
<image src="{{item.imageUrl}}" fade-in="{{true}}" mode="widthFix"></image>
<text class="title">{{item.title}}</text>
<view class="description">
<text><span class="iconfont icon-Fire-fill-red"></span>{{100}}</text>
<text class="description_right">去创作 <span class="iconfont icon-ChevronRight" style="font-size: 12px;"></span></text>
</view>
</navigator>
</view>
</view>
</sticky-section>
</scroll-view>
界面样式实在太丑了,对 .description 进行样式修改。在 index.wxss 文件中,选中 .description 的样式,在悬浮工具条中点击添加到对话,然后我们在对话区域输入我们的修改要求,trae 进行回答。然后点击应用按钮,可以直接把新的代码插入到源文件对应的行。并且 trae 还很贴心的显示了新旧代码的区分。
最后完成页面的修改,看效果:
index.wxss
@import '../../asseat/iconfont.wxss';
.toolbox {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-around;
}
.toolbox .item {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-around;
width: 45%;
background-color: white;
margin-bottom: 20px;
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.1);
}
.toolbox .item image{
/* height: 50px;*/
/* max-width: 100px; */
width: 100%;
overflow: hidden;
/* border-radius: 5px; */
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
.toolbox .item .title {
line-height: 40px;
font-size: 15px;
/* white-space: normal; */
align-items: center;
width: 100%;
padding-left: 10px;
font-weight: 400;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3)
}
.description {
display: flex;
flex-direction: row; /* 修改为列布局 */
flex-wrap: nowrap;
}
.description .iconfont{
font-size: 12px;
}
.description text {
display: inline;
line-height: 20px;
font-size: 12px;
width: 100%;
padding-left: 10px;
font-weight: 400;
/* text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3) */
}
.description text:first-child{
color: red;
}
.description .description_right{
font-size: 12px;
text-align: right;
width: 95%;
display: inline-block;
margin-right: 5px;
color: #3cc51f;
}
如果我们需要回溯代码的历史记录,我们可以选中代码,然后在工具条中选择编辑即可查看。
再来看动图,效果杠杠的🐂:
使用设计图转换为小程序代码
首先我们准备一个页面的设计图
然后使用快捷键 command+U打开右侧的chat 区域,把设计图粘贴进去,并进行对话。输入对话内容:把上图的样式布局转换为微信小程序的代码。看下面动图:
这样会生成对应微信小程序的3个文件: index.wxml、index.wxss、index.js ,然后我们使用应用按钮,将代码插入到对应的文件即可。看最后的效果:
看着效果还行,如果在使用过程中效果不是很好,可以多尝试几次。
小结
1、我们在编写代码过程中与AI 助手聊天,可以指定Trae中的内容(例如代码、文件、文件夹和工作区)作为AI助手阅读和理解的上下文。这 可确保AI助手的响应更符合您的需求。
大家在使用AI的过程中,普遍感觉就是AI不能代替程序员,写出来的代码基础就不能用,原因就是一般的 AI 无法理解用户的工程文件结构黑内容,更无法知道你文件之间、代码直接的关系。trae 做到了,通过项目、文件、代码直接的逻辑生成的答案更贴合实际情况,所以效果会更好些。
2、将图片直接转换为代码依赖强大的多模态模型,大大减低了程序员的工作量。不需要依赖任何内容,将生成的内容稍微修改就可以直接使用, good job 👍。
代码管理
trae 无缝集成了 git 的代码管理功能,我们只需要点点按钮就可以了。可以通过下面的两种方式激活代码管理:
- 如果当前打开的文件夹没有 Git 仓库,请单击初始化仓库以为其初始化一个仓库。初始化完成后,源代码控制将被启用。
- 单击发布到 GitHub直接将此文件夹发布到 GitHub 仓库。发布后,可以访问源代码控制。
Trae配置
熟悉 vscode 的用户,对于配置 Trae 也很简单,使用快捷键 command+, 打开设置项:
根据自己的喜好配置即可。
总结
- 安装 Trae:可参考官方文档进行安装,使用过 VS Code 的用户能无缝切换。
- 插件安装
WXML 插件:按常规方式顺利安装,可实现代码高亮、智能补全等功能。
微信小程序开发工具插件:在市场搜索无果,本地安装失败。最终将适用于 VS Code 的命令修改后成功安装。安装失败时存在按钮无响应的 Bug,需重启 Trae 解决。
Trae 的插件市场有部分插件是无法搜索到(具体原因未知),遇到无法安装的插件建议使用离线安装的方式,使用命令安装,
- 小程序项目开发
结合工具:同时使用微信开发者工具和 Trae,微信开发者工具于实时渲染等,Trae用于高效代码编写和利用 AI 功能。
代码编写:可选中代码向 Trae 的 AI 提出修改要求,直接将新代码插入源文件,还能查看代码历史记录。
- 设计图转换代码:依赖多模态的能力,可以在 chat 区域,粘贴设计图并对话,可生成小程序代码文件,效果不佳时可多次尝试。
- 代码管理:无缝集成 Git 功能,可通过初始化仓库或发布到 GitHub 激活源代码控制。
- 配置 Trae:熟悉 VS Code 的用户可使用快捷键打开设置项进行个性化配置。
来源:juejin.cn/post/7462947628474171403
后端:没空,先自己 mock 去
前言
后端开发忙,不给你接口?
后端抱怨你在测试过程中,频繁的给脏数据?
后端修个接口很慢没法测试?

有了 mockjs ,这些问题将迎刃而解。不要 998,pnpm i 带回家!
后端开发忙,不给你接口?
后端抱怨你在测试过程中,频繁的给脏数据?
后端修个接口很慢没法测试?
有了 mockjs ,这些问题将迎刃而解。不要 998,pnpm i 带回家!
真这么丝滑?
请看我的使用方式:
当后端接口无法满足要求,且不能及时更改时。例如后端返回
{
"err_no": 0,
"err_msg": "success",
"data": [
{
"comment_id": "7337487924836287242",
"user_info": {
"user_name": "陈陈陈_",
}
}
],
}
但我此时希望增加一个 user_type
来确定页面的展示。
那我就直接起一个文件:user.js
,把刚才的响应 copy 过来,并追加改动
myMock('/api/v1/user', 'post', () => {
return {
"err_no": 0,
"err_msg": "success",
"data": [
{
"comment_id": "7337487924836287242",
"user_info": {
"user_name": "陈陈陈_",
"user_type": "admin",
}
}
],
}
});
如此一来,这个请求就被无缝替换为了我们的 mock,可以随便测试了。
请看我的使用方式:
当后端接口无法满足要求,且不能及时更改时。例如后端返回
{
"err_no": 0,
"err_msg": "success",
"data": [
{
"comment_id": "7337487924836287242",
"user_info": {
"user_name": "陈陈陈_",
}
}
],
}
但我此时希望增加一个 user_type
来确定页面的展示。
那我就直接起一个文件:user.js
,把刚才的响应 copy 过来,并追加改动
myMock('/api/v1/user', 'post', () => {
return {
"err_no": 0,
"err_msg": "success",
"data": [
{
"comment_id": "7337487924836287242",
"user_info": {
"user_name": "陈陈陈_",
"user_type": "admin",
}
}
],
}
});
如此一来,这个请求就被无缝替换为了我们的 mock,可以随便测试了。
如何接入 mockjs
有的同学就要问了,主播主播,你的 mockjs 确实很方便,怎么接入比较好呀。别急,我们一步一步来
- 安装 mockjs
pnpm i mockjs
如果是使用 ts 的同学,可能需要额外安装 type 类型包:@types/mockjs
- 新建一个 mock 文件夹,在 mock/index.ts 放入基本路径
// 各种 mock 的文件,视条件而定,我这里有俩文件就引入了俩
import './login/user.js';
import './model/model.js';
有的同学就要问了,主播主播,你的 mockjs 确实很方便,怎么接入比较好呀。别急,我们一步一步来
- 安装 mockjs
pnpm i mockjs
如果是使用 ts 的同学,可能需要额外安装 type 类型包:@types/mockjs
- 新建一个 mock 文件夹,在 mock/index.ts 放入基本路径
// 各种 mock 的文件,视条件而定,我这里有俩文件就引入了俩
import './login/user.js';
import './model/model.js';
并且在你的项目入口 ts 中引入 mock/index.ts
import './mock/index'; // 引入 mock 配置
- 导出一个 myMock 方法,并追加一个 baseUrl 方便直接联动你的 axios
import { ENV_TEST } from '@/api/config/interceptor';
import Mock from 'mockjs';
export const myMock = (
path: string,
method: 'get' | 'post',
callback: (options: any) => any
) => {
Mock.mock(`${ENV_TEST}${path}`, method, callback);
};
如此一来,你就可以在 mock 文件夹下去搞了,比如:
我想新增一个服务模块的各类接口的 mock,那么我就新增一个 service 文件夹,在其下增加一个 index.ts,并对对应路径进行 mock
myMock('/api/v1/service', 'get', () => {
return {
code: 0,
msg: 'hello service',
data: null,
};
});
另外,别忘了在 mock/index.ts 引入文件
不显示在 network 中?
需要说明的是,这样走 mock 是不会触发真正的请求的,相当于 xhr 直接被 mock 拦截了下来并给了你返回值。所以你无法在 network 中看到你的请求。
这是个痛点,目前比较好的解决方案还是起一个单独的服务来 mock。但这样也就意味着,需要另起一个项目来单独做 mock,太不优雅了。
有没有什么办法,既可以走上述简单的mock,又可以在需要的时候起一个服务来查看 network,并且不需要额外维护两套配置呢?
有的兄弟,有的。
import express from 'express';
import bodyParser from 'body-parser';
import Mock from 'mockjs';
import './login/user.js';
import './model/model.js';
import { ENV_TEST } from './utils/index.js';
const app = express();
const port = 3010;
// 使用中间件处理请求体和CORS
app.use(bodyParser.json());
// 设置CORS头部
app.use(( _ , res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
next();
});
// 设置Mock路由的函数
const setupMockRoutes = () => {
const mockApis = Mock._mocked || {};
// 遍历每个Mock API,并生成对应的路由
Object.keys(mockApis).forEach((key) => {
const { rurl, rtype, template } = mockApis[key];
const route = rurl.replace(ENV_TEST, ''); // 去掉环境前缀
// 根据请求类型(GET, POST, 等)设置路由
app[rtype.toLowerCase()](route, (req, res) => {
const data =
typeof template === 'function' ? template(req.body || {}) : template;
res.json(Mock.mock(data)); // 返回模拟数据
});
});
};
// 设置Mock API路由
setupMockRoutes();
// 启动服务器
app.listen(port, () => {
process.env.NODE_ENV = 'mock'; // 设置环境变量
console.log(`Mock 服务已启动,访问地址: http://localhost:${port}`);
});
直接在 mock 文件夹下追加这个启动文件,当你需要看 network 的时候,将环境切换为 mock 环境即可。本质是利用了 Mock._mocked
可以拿到所有注册项,并用 express 起了一个后端服务响应这些注册项来实现的。
在拥有了这个能力的基础上,我们就可以调整我们的命令
"scripts": {
"dev": "cross-env NODE_ENV=test vite",
"mock": "cross-env NODE_ENV=mock vite & node ./src/mock/app.js"
},
顺便贴一下我的 env 配置:
export const ENV_TEST = 'https://api-ai.com/fuxi';
export const ENV_MOCK = 'http://localhost:3010/';
let baseURL: string = ENV_TEST;
console.log('目前环境为:' + process.env.NODE_ENV);
switch (process.env.NODE_ENV) {
case 'mock':
baseURL = ENV_MOCK;
break;
case 'test':
baseURL = ENV_TEST;
break;
case 'production':
break;
default:
baseURL = ENV_TEST;
break;
}
export { baseURL };
这样一来,如果你需要看 network ,就 pnpm mock,如果不需要,就直接 pnpm dev,完全不需要其他心智负担。
三个字:
参数相关
具体的 api 可查阅:github.com/nuysoft/Moc… 相关的文章也非常多,就不展开说明了。
如果这篇文章对你有帮助,不妨点个赞吧~
来源:juejin.cn/post/7460091261762125865
小程序开发体验差,你试过 PageSpy 了吗?
做过小程序的人都知道,小程序的开发体验很糟糕,其中一个很难受的点就是调试。虽然在电脑上开发可以用调试工具,但众所周知很多问题得到真机上才能暴露出来,而小程序真机调试用的 vconsole 又很鸡肋,屏幕小,输入难,还是阉割版(阉割了网络、存储功能,不知道出于什么考虑)。另一个缺陷是,无论是开发工具还是 vconsole,你都只能在「本机」上运行,测试同学要是离你很远的话,想喊你看个 bug,只能截图。
今天介绍一个神奇的工具,全方位的提升小程序的调试体验。
PageSpy 简介
github:github.com/HuolalaTech…
PageSpy 是由货拉拉大前端开源的一款用于远程调试 Web 的工具,它可以针对远程页面提供类似 Chrome DevTools 的调试体验,无论网页实际运行在哪里。除了实时调试,它还支持离线录制,将已经发生的用户操作和历史日志录制下来随时回放。
除了 web 平台,它还把同样的调试功能带到了小程序上。我们来看看使用 PageSpy 调试小程序有什么不一样的体验。
部署和接入
PageSpy 分为服务端、调试端网页和客户端 SDK,官方文档有详细的部署和接入说明,这里不再赘述:
部署指南:http://www.pagespy.org/#/docs/depl…
小程序的 SDK 以 npm 包的方式提供:
import PageSpy from '@huolala-tech/page-spy-wechat';
const $pageSpy = new PageSpy({
api: "<your-pagespy-host>",
})
详细参考:http://www.pagespy.org/#/docs/mini…
在线调试
针对小程序,目前 PageSpy 内置了四个模块:输出,网络,存储,系统。
1. 输出
1. 大屏看日志
比手机小屏上的 vconsole 爽多了,而且不受设备限制,无论小程序运行在什么设备上,都能通过调试端网页远程看到运行情况。
2. 远程执行代码
vconsole 输入很难受,而 PC 键盘输入的效率就很高,PageSpy 打破了小程序无法执行远程代码的限制。这一功能需要安装插件 @huolala-tech/page-spy-plugin-mp-eval
来支持。不过需要注意上线的时候要去掉,小程序对远程执行代码审查很严格,把该插件带到线上去的话很可能审核不通过。
3. 运行上下文
PageSpy 的远程执行代码和你自己写的代码运行在 「同一个上下文」。这有什么意义呢?
你可以自己试一下:
例如你在你的代码里为全局变量加一个字段:wx.a = 123
,在 vconsole 里,你是获取不到这个变量的,反之亦然。
甚至 getCurrentPages 和 getApp 也不能用:
冷知识:小程序的 vconsole 和你的代码 不在一个上下文!
vconsole 是把用户环境的日志通过代理打印到了自己的上下文,又把 wx.xxx 之类的 api 代理到用户上下文去执行。微信似乎只想把它当成一个查看日志的窗口,而不希望用户利用它随意执行代码。
PageSpy 就不会有这个问题,它和你的代码运行在同一个上下文空间,可以直接和你的代码进行交互。
2. 网络
微信小程序自带的 vconsole 阉割了网络模块,所以在真机调试时看不到网络请求日志,非常的不方便。
来看 PageSpy 的网络面板:
和 Chrome 很像。通过 wx.request 发起的请求都可以记录到,而图片、字体之类的资源类请求还看不到,目前来说已经能带来很大帮助了。
3. 存储
小程序的 vconsole 同样也没有 storage 面板🤦🏻,只提供了一个清除 storage 的按钮,令人费解。
来看 PageSpy 的存储面板:
PageSpy 的 web 版 SDK 有 localStorage,sessionStorage,cookie,indexedDB 等多种存储方式,小程序原生只有一个 storage。不过未来倒是可能支持小程序的「本地临时文件」。
4. 系统
系统面板就是把你调用 wx.getSystemInfo、wx.getSetting 等系统 API 能获取到的信息,在这里更清晰、直观的列了出来。例如用户说他某个功能不起效,你看一下这里,可能就知道是不是因为他的系统版本过低,或者某个权限没开导致的。
用户授权信息:
5. 页面呢 ??
如果你用过 web 版的 PageSpy,会发现小程序版的比 web 版的少了一个「页面」模块。因为小程序本身的限制,没有办法拿到页面的 dom 结构,也就没法像 web 一样远程调试界面,这是目前唯一输给 vconsole 的点。也许未来发明了什么黑科技,或者官方良心发现放出一些接口,这个功能才得以实现。
离线录制
PageSpy 不仅支持实时调试,还支持离线录制。假如你在调试小程序的时候发现了一个问题而恰巧又没有连上实时调试,或者你想把某次操作记录存下来慢慢研究或者分享给其他人,就可以用到这个功能。
首先安装插件 @huolala-tech/page-spy-plugin-mp-data-harbor
import PageSpy from '@huolala-tech/page-spy-wechat';
// 引入离线录制插件
import DataHarborPlugin from '@huolala-tech/page-spy-plugin-mp-data-harbor';
// 注册插件
const harbor = new DataHarborPlugin(config);
PageSpy.registerPlugin(harbor);
// 实例化 pageSpy
const $pageSpy = new PageSpy();
添加了该插件之后,小程序的一切日志就会被离线的记录在内存中,之后你可以在需要的时候,调用 $pageSpy.showPanel()
方法呼出一个弹窗,就可以将刚刚记录的日志传到 PageSpy 后台:
在 PageSpy 的调试端,进入「日志回放」页面,就可以看到刚刚上传的日志:
兼容性
小程序有那么多平台,每家都有差异,PageSpy 都支持吗?
是的,PageSpy 目前支持绝大部分市面上的小程序类型:微信、支付宝、抖音、百度、mpaas... 官方给出了4个小程序平台的包:
如果是用原生框架写的小程序,目前官方针对使用量较大的微信和支付宝提供了专门的原生 SDK:
@huolala-tech/page-spy-wechat
@huolala-tech/page-spy-alipay
如今很多小程序使用的是 uniapp 或 taro 之类的跨端框架,官方也提供了相应的 SDK:
@huolala-tech/page-spy-uniapp
@huolala-tech/page-spy-taro
如果你要开发抖音、百度、钉钉之类的冷门平台小程序,只要 uniapp 或者 taro 支持,那就可以用上 PageSpy。
除此之外,uniapp 编译的原生 APP,React Native,甚至鸿蒙应用,它都支持,全平台制霸了属于是。
扩展性
插件系统
前文提到的很多功能都依赖于插件,实际上 PageSpy 的所有功能模块都是通过插件实现的,输出、网络、存储、系统这些是内置插件,不需额外配置,而远程执行代码、离线日志是可选的插件。
除此之外你还可以开发自定义的插件,PageSpy 的核心功能是在客户端和调试端之间建立了一个双向的消息通道,通过自定义插件,你可以利用这条通道做你想做的任何事情。例如观测埋点上报,远程执行指令,甚至通过它远程聊天,发送语音视频消息,也不是不可能。
插件文档:
http://www.pagespy.org/#/docs/plug…
开源贡献
最后,不要忘了 PageSpy 是个开源软件,通过插件实现不了的,还可以贡献代码:
github:github.com/HuolalaTech…
来源:juejin.cn/post/7461626575207825434
如何统一管理枚举类?
Hello,大家好,今天我们来聊一下关于系统中的枚举是如何统一进行管理的。
业务场景
我们公司有这样的一个业务场景前端表单中 下拉选择的枚举值,是需要从后端获取的。那么这时候有个问题,我们不可能每次新增加一个枚举,都需要 改造获取枚举 的相关接口(getEnum),所以我们就需要对系统中的所有枚举类,进行统一的一个管理。
核心思路
为了解决这个问题,我们采用了如下的方案
- 当服务启动时,统一对 枚举类 进行 注册发现
- 枚举管理类,对外暴露一个方法,可以根据我的key 去获取对应的枚举值
相关实现
枚举定义
基于以上的思想,我们对枚举类定义了如下的规范,例如
@**IsEnum**
public enum BooleanEnum implements BaseEnums {
YES("是", "1"),
NO("否", "2")
;
private String text;
@EnumValue
private String value;
YesNoEnum(String text, String value) {
this.text = text;
this.value = value;
}
public String getText() {
return text;
}
@Override
public String getValue() {
return value;
}
@Override
public String toString() {
return "YesNoEnum{" +
"text='" + text + '\'' +
", value=" + value +
'}';
}
@Override
public EnumRequest toJson() {
return new EnumRequest(value, text);
}
@JsonCreator
public static YesNoEnum fromCode(String value) {
for (YesNoEnum status : YesNoEnum.values()) {
if (status.value.equals(value)) {
return status;
}
}
return null; // 或抛出异常
}
}
- 所有枚举均使用 @IsEnum进行标记(这是一个自定义注解)
- 所有枚举均实现 BaseEnums 接口(具体作用后续会提到)
- 所有的枚举的 value 值 统一使用 String 类型,并使用
@EnumValue
注解进行标记
- 这主要是为了兼容Mybatis Plus 查表时 基础数据类型与枚举类型的转化
- 如果将
value
定义为Object类型,Mybatis Plus 无法正确识别,会报错
- 使用
@JsonCreator
注解标记转化函数
- 这个注解是有Jackson提供的,使用了从 基础数据类型到枚举类型的转化
注册发现
那么我们是如何实现服务的注册发现的呢?在这里主要是 使用了 SpringBoot 提供的接口CommandLineRunner
(关于CommandLineRunner
可以参考这篇文章blog.csdn.net/python113/a…
在应用启动时,我们回去扫描 枚举所在的 包,通过 类上 是否包含 IsEnum
注解,判断是否需要对外暴露
// 指定要扫描的包
String basePackage = "com.xxx.enums";
// 创建扫描器
ClassPathScanningCandidateComponentProvider scanner =
new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(EnumMarker.class));
// 扫描指定包
Set<BeanDefinition> beans = scanner.findCandidateComponents(basePackage);
// 注册枚举
for (org.springframework.beans.factory.config.BeanDefinition beanDefinition : beans) {
try {
Class<?> enumClass = Class.forName(beanDefinition.getBeanClassName());
if (Enum.class.isAssignableFrom(enumClass)) {
enumManager.registerEnum((Class<? extends Enum<?>>) enumClass);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
ClassPathScanningCandidateComponentProvider
是 Spring 框架中的一个类,主要用于扫描指定路径下符合条件的候选组件(通常是 Bean 定义)。它允许我们在类路径中扫描并筛选符合特定条件(如标注特定注解、实现某接口等)的类,以实现自动化配置、依赖注入等功能。
典型用途
在基于注解的 Spring 应用中,我们可以使用它来动态扫描特定包路径下的类并注册成 Spring Bean。例如,Spring 扫描 @Component
、@Service
、@Repository
等注解标注的类时就会用到它。
主要方法
addIncludeFilter(TypeFilter filter)
:添加一个包含过滤器,用于筛选扫描过程中包含的类。findCandidateComponents(String basePackage)
:扫描指定包路径下的候选组件(符合条件的类),并返回符合条件的BeanDefinition
对象集合。addExcludeFilter(TypeFilter filter)
:添加一个排除过滤器,用于排除特定类。
最终呢,会将找到的枚举值,放在一个EnumManager中的一个Map集合中
private final Map<Class<?>, List<Enum<?>>> enumMap = new HashMap<>(); // 类与枚举类型的映射关系
private final Map<String, List<Enum<?>>> enumNameMap = new HashMap<>(); // 名称与枚举的映射管理
public <E extends Enum<E>> void registerEnum(Class<? extends Enum<?>> enumClass) {
Enum<?>[] enumConstants = enumClass.getEnumConstants();
final List<Enum<?>> list = Arrays.asList(enumConstants);
enumMap.put(enumClass, list);
enumNameMap.put(enumClass.getSimpleName(), list);
}
enumClass.getEnumConstants()
是 Java 反射 API 中的一个方法,用于获取某个枚举类中定义的所有枚举实例。getEnumConstants()
会返回一个包含所有枚举常量的数组,每个元素都是该枚举类的一个实例。
这样子我们就可以通过枚举的名称或者class 获取枚举列表返回给前端
enumMap.get(enumClass); enumNameMap.get(enumName);
请求与响应
我们项目中使用的序列化器 是Jackson,通过 @JsonValue
与@JsonCreator
两个注解实现的。
@JsonValue
:对象序列化为json时会调用这个注解标记的方法
@JsonValue
public EnumRequest toJson() {
return new EnumRequest(value, text);
}
@JsonCreator
:json反序列化对象时会调用这个注解标记的方法
@JsonCreator
public static YesNoEnum fromCode(String value) {
for (YesNoEnum status : YesNoEnum.values()) {
if (status.value.equals(value)) {
return status;
}
}
return null; // 或抛出异常
}
但是这里有个坑,我们SpringBoot的版本是2.5,使用 @JsonCreator
时会报错,这时候只需要降低jackson 的版本就可以了
// 排除 spring-boot-starter-web 中的jsckson
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.10.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.10.5</version>
</dependency>
Mybatis Plus 枚举中的使用
- 在applicaton.yml文件中添加如下参数
# 在控制台输出sql
mybatis-plus:
type-enums-package: com.xxx.enums // 定义枚举的扫描包
- 将枚举值中 存入到数据库的字段 使用
@EnumValue
注解进行标记,例如:上面提供的YesNoEnum
类中的value字段使用@EnumValue
进行了标记 数据库中实际保存的就是 value 的值(1/2) - 将domian 中 直接定义为枚举类型就可以了,是不是非常简单呢?
以上就是本篇文章的全部内容,如果有更好的方案欢迎小伙伴们评论区讨论!
来源:juejin.cn/post/7431838327844995098
【谈一谈】Redis是AP还是CP?
【谈一谈】Redis是AP还是CP?
再说这个话题之前,这里的是
AP
和CP
不是"A片"和"C骗"啊 !~哈哈哈,就离谱,博文后面我会解释下的
我说下自己对
Redis
的感觉,我一直很好奇Redis
,不仅仅是当缓存用那么简单,包括的它的底层设计
所以,思考再三,我决定先从
Redis
基础开始写(基础是王道!~万丈高楼平地起,我米开始!~嘿嘿)
一、总纲图:
二、什么是CAP?
要想谈一谈我们本文的主题
AP
和CP
,可能有的小伙伴会说: 这我也不是 怎么熟悉啊!
那么我们先复习下大名鼎鼎的
CAP
理论
CAP
理论
看下面的这张图,我们会发现
CAP
对应的三个单词【建议自己画画图,印象深刻】
C
: 一致性(Consistency)--
- 每次读取都会收到最新的写入数据或者错误信息
- (
注
:这里面的一致性,指的是强一致性
,不是市面上所说的所有节点在相同时间看到是一样的数据)
A
:可用性(Availability)--
- 每个请求都会收到非错误地响应,但是这个响应的信息不保证是最新的 ,只保证可用
P
:分区容错性(Partition Tolerance)--
- 就是网络节点间丢弃或者延迟一定数量(就是任意数量)信息,也不影响大局,系统还是能够正常运行
好了,我们言归正传,回到我们的主题上面
三、为啥说Redis是AP?不是CP?
我们知道,
Redis
是一个开源的内存数据库,且是执行单线程处理
但是网上,若是喜欢读博客的小伙伴,会发现很多人说这样一句话:
- 单机的
Redis
是CP
的,集群的REDIS
是AP
的
这句话真的对吗? 大家在看下文前,倾思考思考!~我当时读到第一反应就是疑惑,于是我就去查询大量资料
有的人说:
CAP
是针对分布式场景中,如果是单机REDIS
,就压根儿和什么分布式不着边,都没P
了!!还说哈AP
和CP
??
- 在单机的
REDIS
中,应为只有一个实例,那么他的一致性是有保障的,如果这个节点挂了,就没有可用性可言了,所以他是CP
系统
我在这里说下,以上两个观点都特么错的!!!以偏概全,混淆是非!~就是AP!!
~哈哈哈!你可能会说:我去,那你证明啊,这特么为啥是错的啊!,别急嘛!我们往下读,让你心服口服,嘿嘿
REDIS
是AP
的理由
第一点: 一致性
我们都知道,
REDIS
的设计目标是高性能,高扩展和高可用性 ,
而且
REDIS
的一致性模型是最终一致性:
(什么意思呢?)就是在某个时间点读取的数据可能不是最新的,但殊途同归,最终会达到一致的状态
为什么Redis
无法保持强一致性??
主要原因: 异步复制
- 因为
Redis
在分布式的设计中采用的是异步复制
,者导致在节点之间存在数据在同步和延迟不一致的情况存在
换句话说:
- 当某个节点的数据发生改变,
Redis
会将这个节点的修改操作发送给其他节点进行同步~(这是正常步骤,没毛病是不,我们继续往下看) - 但是(不怕一万,就怕万一来了,哈哈哈)因为网络传输的延迟,拥塞等原因,这些操作没有立即被被其他节点收到和执行,
- 从而产生节点之间数据不一致的情况!!!
- 当某个节点的数据发生改变,
抛开上面的影响点,
节点故障
对Redis
的一致性影响也是很大的
举个例子:
当一个节点宕机时,这个节点的 数据就可能同步不到其他节点上,这就会导致数据在节点间不一致
你可能有疑惑?那
Redis
不是有哨兵和复制等机制吗?
但是,问题就是但是,哈哈~这些机制是能提高系统的可用性和容错性,能完全解决吗?
~(你没看错,就是完全解决,能吗??)不能吧,自己主观推下也能想到那种万一场景吧!!!
你说既然异步不行,那么我就用同步机制就不好了!!不就是
CP
了???
~~no!no!NO !哈哈哈,年轻人,想的太简单了哈!
我们看看官网是怎么说的()
Redis
客户端可以使用WAIT
命令请求特定数据进行同步复制- 使用
WAIT
,只能说发生故障时丢失写操作的概率会大大降低,且是在难以触发的故障模式情况下 - 但是!!
WAIT
只能确保数据在Redis
实例中有指定数量的副本被确认
不能将一组
REdis
转换为具有强一制性的CP
系统
什么意思?
在故障转移期间,由于
Redis
的持久化配置,当中已确认的写操作,仍然可能会丢失
完结!~
士不可以不弘毅,任重而道远,诸君共勉!~
来源:juejin.cn/post/7338721296866574376
若依框架——防重复提交自定义注解
防重复提交
1、自定义防重复提交注解
/**
* 自定义注解防止表单重复提交
*
* @author ruoyi
*
*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit
{
/**
* 间隔时间(ms),小于此时间视为重复提交
*/
public int interval() default 5000;
/**
* 提示消息
*/
public String message() default "不允许重复提交,请稍候再试";
}
@Inherited:
该元注解表示如果一个类使用了这个 RepeatSubmit 注解,那么它的子类也会自动继承这个注解。这在某些需要对一组相关的控制器方法进行统一重复提交检查的场景下很有用,子类无需再次显式添加该注解。
@Target(ElementType.METHOD):
表明这个注解只能应用在方法上。在实际应用中,通常会将其添加到控制器类的处理请求的方法上,比如 Spring MVC 的 @RequestMapping 注解修饰的方法。
@Retention(RetentionPolicy.RUNTIME):
意味着该注解在运行时仍然存在,可以通过反射机制获取到。这样在运行时,通过 AOP(面向切面编程)等技术拦截方法调用时,就能够读取到注解的属性值,从而实现重复提交的检查逻辑。
@Documented:
这个元注解用于将注解包含在 JavaDoc 中。当生成项目文档时,使用了该注解的方法会在文档中显示该注解及其属性,方便其他开发者了解该方法具有防止重复提交的功能以及相关的配置参数。
/**
* 间隔时间(ms),小于此时间视为重复提交
*/
public int interval() default 5000;
定义了一个名为 interval 的属性,类型为 int,表示两次提交之间允许的最小时间间隔,单位是毫秒。默认值为 5000,即 5 秒。如果两次提交的时间间隔小于这个值,就会被视为重复提交。
/**
* 提示消息
*/
public String message() default "不允许重复提交,请稍候再试";
定义了一个名为 message 的属性,类型为 String,用于在检测到重复提交时返回给客户端的提示消息。默认消息为 “不允许重复提交,请稍候再试”。开发者可以根据具体业务需求,在使用注解时自定义这个提示消息。
2、防止重复提交的抽象类
抽象类可以自己有 具体方法
/**
* 防止重复提交拦截器
*
* @author ruoyi
*/
@Component
public abstract class RepeatSubmitInterceptor implements HandlerInterceptor
{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
{
if (handler instanceof HandlerMethod)
{
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
if (annotation != null)
{
if (this.isRepeatSubmit(request, annotation))
{
AjaxResult ajaxResult = AjaxResult.error(annotation.message());
ServletUtils.renderString(response, JSON.toJSONString(ajaxResult));
return false;
}
}
return true;
}
else
{
return true;
}
}
/**
* 验证是否重复提交由子类实现具体的防重复提交的规则
*
* @param request 请求信息
* @param annotation 防重复注解参数
* @return 结果
* @throws Exception
*/
public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation);
}
2.1、preHandle 方法
自定义抽象类拦截器 RepeatSubmitInterceptor 实现了 HandlerInterceptor 接口,重写 preHandle 方法
preHandle方法是负责拦截请求的
- 如果isRepeatSubmit方法返回true,表示当前请求是重复提交。此时会创建一个包含错误信息的AjaxResult对象,错误信息就是RepeatSubmit注解中设置的message。然后通过ServletUtils.renderString方法将AjaxResult对象转换为 JSON 字符串,并将其作为响应返回给客户端,同时返回false,阻止请求继续处理。
- 如果方法上不存在RepeatSubmit注解,或者isRepeatSubmit方法返回false,表示当前请求不是重复提交,就返回true,允许请求继续执行后续的处理流程。
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
{
if (handler instanceof HandlerMethod)
{
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
if (annotation != null)
{
if (this.isRepeatSubmit(request, annotation))
{
AjaxResult ajaxResult = AjaxResult.error(annotation.message());
ServletUtils.renderString(response, JSON.toJSONString(ajaxResult));
return false;
}
}
return true;
}
else
{
return true;
}
}
参数说明:
- HttpServletRequest request:提供了关于当前 HTTP 请求的信息,如请求头、请求参数、请求方法等。
- HttpServletResponse response:用于设置 HTTP 响应,例如设置响应头、响应状态码、写入响应内容等。
- Object handler:代表即将被执行的处理器对象,在 Spring MVC 中,它通常是一个 HandlerMethod,但也可能是其他类型。
方法解释:
if (handler instanceof HandlerMethod)
{
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
首先检查 handler 是否是 HandlerMethod 类型的 , 不是的话,直接放行,不做重复提交检查, 因为该拦截器主要针对被 @RepeatSubmit 注解标记的方法进行处理。
如果 handler 是 HandlerMethod 类型的话,将 handler 转换成为 HandlerMethod 并获取对应的 Method 对象。
然后通过 getMethod() 方法 获取 方法,并通过 getAnnotation 方法获取 RepeatSubmit 注解 ,
if (annotation != null) {
if (this.isRepeatSubmit(request, annotation)) {
AjaxResult ajaxResult = AjaxResult.error(annotation.message());
ServletUtils.renderString(response, JSON.toJSONString(ajaxResult));
return false;
}
}
return true;
判断是否获取到 RepeatSubmit 注解,没有获取到,返回 true , 允许请求继续执行后续的处理流程。
运用 isRepeatSubmit 方法 判断是否是 重复提交
如果当前请求是重复提交 将注解的 错误信息 封装给结果映射对象
并调用 renderString 方法 将字符串渲染到客户端
/**
* 将字符串渲染到客户端
*
* @param response 渲染对象
* @param string 待渲染的字符串
*/
public static void renderString(HttpServletResponse response, String string)
{
try
{
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
}
catch (IOException e)
{
e.printStackTrace();
}
}
2.2、isRepeatSubmit 方法
判断是否重复提交 true 重复提交 false 不重复提交
/**
* 验证是否重复提交由子类实现具体的防重复提交的规则
*
* @param request 请求信息
* @param annotation 防重复注解参数
* @return 结果
* @throws Exception
*/
public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation);
public final String REPEAT_PARAMS = "repeatParams";
public final String REPEAT_TIME = "repeatTime";
// 令牌自定义标识
@Value("${token.header}")
private String header; // token.header = "Authorization"
@Autowired
private RedisCache redisCache;
@SuppressWarnings("unchecked")
@Override
public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation)
{
String nowParams = "";
if (request instanceof RepeatedlyRequestWrapper)
{
RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
nowParams = HttpHelper.getBodyString(repeatedlyRequest);
}
// body参数为空,获取Parameter的数据
if (StringUtils.isEmpty(nowParams))
{
nowParams = JSON.toJSONString(request.getParameterMap());
}
Map<String, Object> nowDataMap = new HashMap<String, Object>();
nowDataMap.put(REPEAT_PARAMS, nowParams);
nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
// 请求地址(作为存放cache的key值)
String url = request.getRequestURI();
// 唯一值(没有消息头则使用请求地址)
String submitKey = StringUtils.trimToEmpty(request.getHeader(header));
// 唯一标识(指定key + url + 消息头)
String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey;
Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
if (sessionObj != null)
{
Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
if (sessionMap.containsKey(url))
{
Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval()))
{
return true;
}
}
}
Map<String, Object> cacheMap = new HashMap<String, Object>();
cacheMap.put(url, nowDataMap);
redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS);
return false;
}
@SuppressWarnings("unchecked")
注解 @SuppressWarnings("unchecked"): 这个注解用于抑制编译器的 “unchecked” 警告。在代码中,可能存在一些未经检查的类型转换操作,使用该注解可以告诉编译器忽略这些警告。
String nowParams = "";
初始化一个字符串变量 nowParams 用于存储当前请求的参数。
if (request instanceof RepeatedlyRequestWrapper)
{
RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
nowParams = HttpHelper.getBodyString(repeatedlyRequest);
}
判断 当前请求是否是 RepeatedlyRequestWrapper 类型的
RepeatedlyRequestWrapper 是自定义的 允许多次请求的请求体 (详情见备注)
如果是的话,强转对象,并且 通过 getBodyString 方法 (详情见备注) 获取请求体的字符串内容,并且赋值给 nowParams
// body参数为空,获取Parameter的数据
if (StringUtils.isEmpty(nowParams))
{
nowParams = JSON.toJSONString(request.getParameterMap());
}
Map<String, Object> nowDataMap = new HashMap<String, Object>();
nowDataMap.put(REPEAT_PARAMS, nowParams); //REPEAT_PARAMS = "repeatParams"
nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); //REPEAT_TIME = "repeatTime"
if (StringUtils.isEmpty(nowParams)):如果通过上述方式获取的 nowParams 为空,说明请求体可能为空,此时通过 JSON.toJSONString(request.getParameterMap()) 将请求参数转换为 JSON 字符串,并赋值给 nowParams。这样无论请求参数是在请求体中还是在 URL 参数中,都能获取到。
- Map<String, Object> nowDataMap = new HashMap<String, Object>(); 创建一个新的 HashMap 用于存储当前请求的数据。
- nowDataMap.put(REPEAT_ PARAMS, nowParams); 将获取到的请求参数存入 nowDataMap 中,使用常量 REPEAT_PARAMS 作为键。
- nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); 将当前时间戳存入 nowDataMap 中,使用常量 REPEAT_TIME 作为键。
// 请求地址(作为存放cache的key值)
String url = request.getRequestURI();
// 唯一值(没有消息头则使用请求地址)
String submitKey = StringUtils.trimToEmpty(request.getHeader(header));
// 唯一标识(指定key + url + 消息头)
String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey;
// REPEAT_SUBMIT_KEY = "repeat_submit:"
- String url = request.getRequestURI(); 获取当前请求的 URI。
- String submitKey = StringUtils.trimToEmpty(request.getHeader(header)); 从请求头中获取指定的键值(header 变量可能是在类中定义的一个常量,表示要获取的请求头字段),并去除两端的空白字符。如果请求头中不存在该字段,则返回空字符串。
- String cacheRepeatKey = CacheConstants . REPEAT_SUBMIT_KEY + url + submitKey; 使用一个常量 CacheConstants.REPEAT _SUBMIT_KEY 与请求 URI 和 submitKey 拼接生成一个唯一的缓存键 cacheRepeatKey。这个键用于在缓存中存储和检索与该请求相关的重复提交信息。
Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
if (sessionObj != null)
{
Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
if (sessionMap.containsKey(url))
{
Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval()))
{
return true;
}
}
}
Map<String, Object> cacheMap = new HashMap<String, Object>();
cacheMap.put(url, nowDataMap);
redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS);
rerurn false;
通过缓存键 先去 redis 中,看 是否存在相同的缓存信息 如果存在,说明之前有过类似的请求 ,进入判断
因为这里 redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS); 传了map,所以说 redisCache.getCacheObject(cacheRepeatKey); 得到的map,就是同样的类型的,所以键值就是 url 。
检查 sessionMap 这个 Map 中是否包含以当前请求的 url 作为键的记录。这一步是因为在缓存的数据结构中,url 被用作内层键来存储每个请求的具体数据。如果存在这个键,说明之前已经有针对该 url 的请求被缓存。
接下来
调用 compareParams 方法比较当前请求的数据 nowDataMap 和之前请求的数据 preDataMap 的参数是否相同,同时调用 compareTime 方法比较当前请求时间和之前请求时间的间隔是否小于 @RepeatSubmit 注解中配置的 interval 时间。如果参数相同且时间间隔小于设定值,说明当前请求可能是重复提交,返回 true。
如果缓存中不存在当前请求 url 的记录,或者当前请求不被判定为重复提交,则执行以下操作: Map<String, Object> cacheMap = new HashMap<String, Object>();:创建一个新的 HashMap 用于存储当前请求的数据。 cacheMap.put(url, nowDataMap);:将当前请求的 url 作为键,nowDataMap(包含当前请求参数和时间)作为值存入 cacheMap。 redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS);:将 cacheMap 以 cacheRepeatKey 为键存入 Redis 缓存中,缓存时间为 @RepeatSubmit 注解中配置的 interval 时间,时间单位为毫秒。这样下次相同 url 的请求过来时,就可以从缓存中获取到之前的请求数据进行比较。
2.2.1、compareParams 方法
/**
* 判断参数是否相同
*/
private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap)
{
String nowParams = (String) nowMap.get(REPEAT_PARAMS);
String preParams = (String) preMap.get(REPEAT_PARAMS);
return nowParams.equals(preParams);
}
2.2.2、compareTime 方法
/**
* 判断两次间隔时间
*/
private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap, int interval)
{
long time1 = (Long) nowMap.get(REPEAT_TIME);
long time2 = (Long) preMap.get(REPEAT_TIME);
if ((time1 - time2) < interval)
{
return true;
}
return false;
}
备注:
RepeatedlyRequestWrapper
一个自定义的请求包装类,允许多次读取请求体
/**
* 构建可重复读取inputStream的request
*
* @author ruoyi
*/
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper
{
private final byte[] body;
public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException
{
super(request);
request.setCharacterEncoding(Constants.UTF8);
response.setCharacterEncoding(Constants.UTF8);
body = HttpHelper.getBodyString(request).getBytes(Constants.UTF8);
}
@Override
public BufferedReader getReader() throws IOException
{
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException
{
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream()
{
@Override
public int read() throws IOException
{
return bais.read();
}
@Override
public int available() throws IOException
{
return body.length;
}
@Override
public boolean isFinished()
{
return false;
}
@Override
public boolean isReady()
{
return false;
}
@Override
public void setReadListener(ReadListener readListener)
{
}
};
}
}
getBodyString 方法
将二进制的输入流数据转换为易于处理的字符串形式,方便后续对请求体内容进行解析和处理
public static String getBodyString(ServletRequest request)
{
StringBuilder sb = new StringBuilder();
BufferedReader reader = null;
try (InputStream inputStream = request.getInputStream())
{
reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
String line = "";
while ((line = reader.readLine()) != null)
{
sb.append(line);
}
}
catch (IOException e)
{
LOGGER.warn("getBodyString出现问题!");
}
finally
{
if (reader != null)
{
try
{
reader.close();
}
catch (IOException e)
{
LOGGER.error(ExceptionUtils.getMessage(e));
}
}
}
return sb.toString();
}
来源:juejin.cn/post/7460129833931849737
🍉🍉🍉快来吃瓜,原来国外开发者也会因为哪个框架好吵的不可开交
前言
事情是这样的,今天浏览到了一篇dev上Ryan Carniato大佬(SolidJS
库作者)的文章,在文章中,作者阐述了自己对2025年的前段框架的展望,并且提到React、Svelte都在以增加编译的复杂性为代价简化开发。
And interestingly both choices come at the expense of increased complexity in tooling compared to their existing approaches. The verdict is still out on whether these will ultimately be good moves for these projects. The common ground is the foundation we build upon continues to get more complicated as we attempt to create solutions to make development easier.
有趣的是,与现有方法相比,这两种选择都以增加工具复杂性为代价。这些举措最终是否对这些项目有利尚无定论。共同点是,随着我们尝试创建解决方案以简化开发,我们所依赖的基础变得越来越复杂。
回顾下前端框架的区别
为了方便清楚的了解下面的内容,我先给大家简单回顾下前段框架的现状。
欢迎关注我的公众号:萌萌哒草头将军
以前的文章中提到,根据运行时和编译时,前端框架主要分为三类:重编译时(Svelte
、SolidJS
)、重运行时(React
)、介于两者之间(Angular
、Vue
)。
但是随着Svelte 5
引入了符文(runes),使得Svelte
不再是完美的进入运行时即可执行的javascript
片段,因为需要动态解析runes
的内容。这也是Ryan Carniato
觉得Svelte
变复杂的原因。
根据不同的更新粗细粒度,他们又被分为:
粒度 | 成员 |
---|---|
粗粒度 | React |
中粒度 | Vue |
细粒度 | SolidJS ,Svelte |
Vue
为了细粒度更新,正在努力的研发vapor
,React
就目前的趋势来看,打算一条道走到黑了。
而Angular
起初的处境和Vue
相似。但是在发布于 2023 年 5月
的Angular 16
,已经借助Signals
实现了翻身,提前完成了细粒度更新的框架。
Signals
是一种类似vue3
的proxy
、Reactivity
响应式的发布订阅模式,有响应式更新方面粒度更细,性能更好
好了有了上面的铺垫,相信大家都可以看明白下面的评论。
结果评论区出现了一批弃坑并谴责React
的老哥。
好戏开场了
下面是第一位老哥的发言:React
已死,Svelte
、SolidJS
当立!
这种回答,立马招惹了Angular
粉丝的不满,原因是Angular
使用了Signals
技术,性能提高了30%
!
可以看到作为粉丝是很自豪的,甚至提到了vue vopar
。说明这位开发者对前端框架的有一定深入的了解。
不过由于Angular
自身的笨重,即使有了Signals
的加持,还是取得了倒数第二的好成绩!
接着,伊桑先表达了自己不喜欢 React
的原因:
- useEffect 破坏代码可读性,难以预测的执行时机
- 状态管理库混乱,这里是在批评
Redux
- 不关系CSR。
CSR(Client-Side Rendering
指客户端渲染, - 虽然入门门槛低,但是存在步兵生成问题,这是指
React
不能很好的控制最小任务单元,需要借助外部工具,比如状态管理库。
然后表达了喜欢 Angular
的原因:
Angular
的就业市场更好(国外)Angular
的特性:事件重放、路由级渲染模式、Signals
等优秀的功能- 依赖注入很棒!
- 稳定性强,功能丰富
再来看看其他比较温和的评论
尹桑又单独评论了一次,这次的评论让我觉得他的水平有点低了,JavaScript
本身就是个脚本语言,不需要构建,边解释边执行。无可厚非的事!
这位评论者说的也很中肯,文章中,作者的确提出了Signals
的缺点: Signal
的生态尚未完全成熟、需要面对一些新的学习曲线和社区支持不足的问题
这位评论者的赞扬得到了原作者的肯定,并且推荐阅读更多文章,我也进主页看了下,
我打算抽空好好研究下,抽空在分享给大家!
最后
一个和谐友爱的社区会帮助开发者们更好的成长,希望大家有理有据的讨论不要踩一捧一。
这是原文连接:dev.to/this-is-lea…
好了,如果你觉得这篇文章对你有帮助,请记得给我点赞,或者关注我的公众号:萌萌哒草头将军
来源:juejin.cn/post/7460506415460745231
13个Cursor神功能,比VSCode强大100倍
"又是一个编辑器?VSCode不是挺好的吗?"
"装了GitHub Copilot已经够用了,还需要换吗?"
"迁移成本太高了吧,值得吗?"
相信这些问题困扰着很多程序员,一年前的我,也是Github Copilot的忠实粉丝,甚至是最早为它付费的那群人。在2023年,我写的《花了大半个月,我终于逆向分析了Github Copilot》火爆全网。但自从我用了Cursor之后,果断停止了Github Copilot的付费,我必须承认:迈出这一步是值得的。
我在使用Cursor后的第一天,就完成了两个组件的重构,而且比预期快了整整一倍。到第三天,我发现自己已经很少打开VSCode了。
这不是因为 Cursor 有多么惊艳的界面设计,而是它确实解决了我在日常开发中的诸多痛点。
接下来,我想分享13个让我彻底转投Cursor的实用功能。这些功能不仅提升了我的开发效率,更改变了我对AI辅助编程的认知。
AI代码补全质量远超预期
当我第一次用Cursor写代码时,说实话是被震撼到的。作为一个付费的GitHub Copilot用户,我原本以为自己对AI代码补全已经见怪不怪了。但Cursor的表现却刷新了我的认知。
多行批量编辑
想象一下这个场景:你需要修改一系列相似的代码块。在VSCode中,你可能需要:
- 使用多光标(Cmd/Ctrl + D)逐个选择
- 或者使用正则表达式查找替换
而在Cursor中:
// 原代码
const Orange = new Block("Orange").id(5006);
const Blue = new Block("Blue").id(5007);
const Red = new Block("Red").id(5008);
const White = new Block("White").id(5009);
// 只需要在第一行添加 "Concrete",Cursor就能理解你的意图
// 自动为所有相似结构添加相同的修改
const Orange = new Block("Orange Concrete").id(5006);
const Blue = new Block("Blue Concrete").id(5007);
const Red = new Block("Red Concrete").id(5008);
const White = new Block("White Concrete").id(5009);
智能代码重写
最让我惊艳的是Cursor的"Smart Rewrites"功能。它不仅能纠正拼写错误,还能自动规范化你的代码格式:
// 随意输入的CSS(故意写错的)
dataStyles: {
top 10px
left 10px
position fixed
zindex 1000
}
// Cursor自动修正为标准格式
dataStyles: {
top: '10px',
left: '10px',
position: 'fixed',
zIndex: '1000' // 注意这里自动修正了 zindex 为 zIndex
}
智能光标预测
这是我最喜欢的功能之一。Cursor会预测你下一步最可能要编辑的位置,通过Tab键就能快速跳转:
// 当你输入完一个函数参数时
function handleSubmit(event) {
// Tab键直接跳转到函数体内的最佳位置
▌ // 光标自动定位在这里
}
// 在条件语句中
if (condition) {
// Tab键智能跳转到下一个需要编辑的位置
▌
} else {
▌
}
强大的Chat能力
和Copilot类似,Cursor提供了更为强大的Chat能力,具体体现在以下6个方面:
智能对话(Chat)
核心特点:
- 实时感知当前文件和光标位置
- 支持自然语言交互
- 可以直接询问代码相关问题
- 快捷键
Cmd/Ctrl + Enter
激活全局对话
使用场景:
// 可以直接询问:
"这段代码有bug吗?"
"如何优化这个函数的性能?"
"这个组件的状态管理是否合理?"
5. ## 即时应用(Instant Apply)
特点:
- 一键应用AI建议的代码修改
- 实时预览修改效果
- 支持批量代码更新
- 智能合并冲突处理
代码库智能查询(Codebase Answers)
功能亮点:
- 使用
@Codebase
触发 - 深度理解项目结构
- 智能代码搜索
- 上下文相关的答案
常见用法:
@Codebase 如何实现用户认证?
@Codebase 这个API在哪里被调用?
@Codebase 查找所有使用Redux的组件
7. ## 代码引用(Reference your Code)
特性:
- 使用
@
符号快速引用代码 - 自动补全文件和符号名
- 智能上下文关联
- 支持跨文件引用
比如:
# 引用特定文件
@utils/auth.ts
# 引用特定函数
@validateUser
# 引用特定组件
@components/Button
图片识别(Use Images)
创新功能:
- 支持拖放图片
- 智能识别UI组件
- 代码实现建议
- 视觉参考转代码
Web集成(Ask the Web)
实用特性:
- 使用
@Web
触发网络搜索 - 实时获取最新信息
- 智能整合答案
- 自动引用文档
惊艳的Agent能力
Cursor的Agent模式代表了AI辅助开发的一个重要突破,它能够自主完成端到端的开发任务,同时保持开发者对整个过程的掌控。让我们深入了解这个强大的功能。
智能上下文理解
利用Agent,可以做到更智能的上下文能力:
- 使用自定义检索模型
- 自动分析项目结构
- 减少手动添加上下文的需求
- 精准定位相关代码
自动化命令执行
利用Agent,可以做到:
- 智能生成终端命令
- 提供命令确认机制
- 支持批量操作
- 自动处理依赖关系
在composer的Agent模式下,我们可以给Agent一些简单的指令,它会自动拆解任务,执行终端命令,我们还可以借助YOLO模式的开启(Cursor Settings打开开关即可)来让终端命令自动执行:
错误循环处理
Agent模式下,Cursor会自动检测出lint问题,并且重复循环进行修复,直到问题被解决为止:
- 自动检测代码问题
- 智能修复建议
- 持续优化循环
- 减少手动调试需求
自动提交Git Commit
使用Agent,我们直接不用操纵Git,让它帮忙快速写好commit message并且提交:
总结:重新定义AI辅助开发体验
在经历了从VSCode到Cursor的转变后,我深刻体会到AI辅助开发工具已经不仅仅是一个代码补全助手,而是evolving成为一个真正的开发伙伴。
让我们深入对比一下Cursor和Copilot的特性:
功能特性 | Cursor | GitHub Copilot | 对比说明 |
---|---|---|---|
基础功能 | |||
代码补全 | ✅ 更智能的上下文理解 | ✅ 基于上下文补全 | Cursor的补全更准确,理解更深入 |
多行编辑 | ✅ 智能批量编辑 | ⚠️ 仅支持基础多光标 | Cursor支持更智能的批量修改 |
代码解释 | ✅ 实时、详细 | ✅ 基础解释 | Cursor的解释更加详细和准确 |
AI 特性 | |||
对话功能 | ✅ 内置Chat功能 | ⚠️ 需要Copilot Chat | Cursor原生支持,无需额外订阅 |
代码重构 | ✅ 智能重构建议 | ⚠️ 有限支持 | Cursor提供更完整的重构方案 |
错误修复 | ✅ 自动循环修复 | ✅ 基础修复建议 | Cursor支持自动化修复循环 |
高级功能 | |||
Agent模式 | ✅ 完整支持 | ❌ 不支持 | Cursor独有功能 |
终端集成 | ✅ 智能命令生成 | ❌ 不支持 | Cursor支持终端智能操作 |
图片识别 | ✅ 支持 | ❌ 不支持 | Cursor可直接识别UI转代码 |
项目理解 | |||
代码库理解 | ✅ 深度理解 | ✅ 基础理解 | Cursor对项目结构理解更深入 |
跨文件引用 | ✅ 智能关联 | ⚠️ 有限支持 | Cursor提供更智能的代码关联 |
Web集成 | ✅ 支持 | ❌ 不支持 | Cursor可直接搜索网络资源 |
开发工具集成 | |||
Git集成 | ✅ 智能commit | ❌ 不支持 | Cursor支持自动化Git操作 |
自动化工作流 | ✅ 完整支持 | ❌ 不支持 | Cursor支持端到端自动化 |
LSP支持 | ✅ 完整支持 | ✅ 完整支持 | 两者都有良好的语言服务支持 |
性能与体验 | |||
响应速度 | ⚡️ 快速 | ⚡️ 快速 | 两者性能相当 |
资源占用 | 📊 中等 | 📊 较低 | Copilot资源占用较少 |
使用门槛 | 📈 中等 | 📉 较低 | Cursor功能更丰富,需要一定学习 |
如果你也在寻找一个能真正提升开发效率的工具,不妨给Cursor一个机会。也许和我一样,你会发现:这个选择,值得。
让我们一起在AI浪潮中成长
在写这篇文章的过程中,我收到了很多读者的私信,都在问: "如何才能更好地掌握这些 AI 工具?" 这个问题让我深思。作为一个持续深耕AI领域的从业者,我深知在这个快速发展的领域,仅仅依靠个人摸索是远远不够的。
为什么需要一个学习社群?
在AI技术日新月异的今天:
- 工具更新迭代快速
- 使用技巧需要实践
- 经验交流尤为重要
- 个人学习效率有限
我的AI学习社群能为你提供:
- 第一手实践经验
- 不是道听途说的二手资料
- 来自实战的经验总结
- 具体的应用案例分析
- 深度技术交流
- 定期的技术答疑
- 问题解决指导
- 实践中的难点突破
- 最新工具解析
- AI新工具第一时间评测
- 实用技巧分享
- 应用场景分析
- 社群价值
- 志同道合的伙伴交流
- 共同学习和进步
- 经验分享与互助
社群详情
为了保证每位成员都能得到充分的交流机会和良好的学习体验,目前社群仅剩20个名额,价格不及一顿午餐,但能带给你持续一年的学习和成长。
如果你:
✅ 对AI技术充满热情
✅ 渴望深度技术交流
✅ 愿意投入时间学习
✅ 想在AI浪潮中抓住机会
欢迎添加我的wx(备注:加入付费陪伴群)进一步交流,公众号同名。
来源:juejin.cn/post/7460322919504805951
几个自学项目的通病,别因为它们浪费了时间!
大家好,我是程序员鱼皮。就在昨天,我又带大家做完了一个新项目 《智能协同云图库平台》,已经带大家做了十多个项目了,自然也发现了很多大家在学项目过程中的问题。
最了解学生的,莫过于老师和学生自己。而我经历了自学阶段,从学生成长为了老师,所以也很清楚怎么自学项目,效率才能更快一些。这篇文章,就分享一下我发现的大家自学项目时的通病。
注意,本篇文章中我写的所有内容,目的都是为了帮你节约时间,提高自学效率。
如果你正好有下面这些情况,请务必及时调整!
自学项目的通病
1、重复工作
任何业务类项目基本都是从项目初始化、编写增删改查开始的,在你做第一个项目的时候,自己手动编写这些代码没有任何问题,主要是熟悉自己搭建项目的方法和过程。但当你做第 2 个、第 3 个项目的时候,如果还在从 0 开始写基础代码(比如全局异常处理器、一些工具类),那就属于是浪费时间了,完全可以通过复用自己之前的项目代码、使用工具自动生成、或者搭建一个自己的项目模板来提高开发效率。
像我工作的时候遵循一个原则 —— 只要有重复劳动,都会尝试能否通过自动化的方式来提高效率。大家学项目时也是如此,避免在重复工作上耽误时间,不要满足于 “自己重复代码写得有多快”,而是要多把时间花在学习新的技术知识上。
2、死守教程
大家都知道,每个教程中作者都会选择特定的版本、技术和工具来教学,没有人能保证这些技术和工具不更新,所以每个教程一定有自己的 “保质期”。
像我在几年前最开始带大家做第一个项目 —— 用户中心项目时,就吃过技术更新的亏,由于前端框架的更新,导致前端部分的开发跟教程有一些不一致。所以后续我在带大家做项目时,会倾向于选择稳定的框架和版本。
当然,这是对于项目作者来说的。那对于学习项目的同学来说,可能就会产生很多问题:
- 为什么我使用的版本跟教程不一致了?
- 为什么教程中用的工具有这个按钮,但我用的工具没有这个按钮?
- 为什么我在官方文档中找不到教程中写的内容了?
- 为什么我跟教程中操作一模一样,但是运行结果不同?
很多初学者会因为这些问题,纠结很久,甚至不敢接着往下做项目,其实大可不必。
任何教程都有保质期,但解决问题的方法是灵活的。
如果使用的版本或环境跟教程不一致,那么不妨安装跟教程相同的版本(比如前端可以用 NVM 管理 Node.js 版本),或者查阅下如何使用新版本;如果用了比教程更新的工具,那么就在网上搜一下新版本的工具有没有教程中要执行的功能;如果官方文档跟教程中的内容不一致,那么就仔细阅读一下官方文档中最新的使用方法;如果跟教程中操作一模一样但结果不同,那么不妨自己 Debug 一下来解决问题,有可能就是教程本身有错误呢?
总之,在出现跟教程不一致的地方时,可以先记录一下问题,并且自己查阅资料和文档,不必完全死守教程。
3、滥用技术
之前有个同学问我:鱼皮,我们公司想做个发券功能,现在的想法是用 Redis 分布式锁 + 消息队列 + blablabla。。。
我反问他:你们发券功能的 QPS 是多少?同时要发多少张券?
他跟我说:我们是管理员给用户发券,每批 1000 张。
我接着反问他:既然是管理员控制发券,数量也只有 1000 张,那你不妨思考一下,真的有必要用到这些技术么?
类似的情况我之前也分享过,可以看 这篇文章 。
在企业中,业务 > 技术,技术是为业务服务的,要根据业务选择合适的技术实现。
一般来说,我们在思考业务实现方案时,能少用一个技术就少用一个技术,减少开发和维护成本。
但我发现有些同学可能是学的技术多了、也可能是八股文背多了,在做项目功能时,反而是先搬出一大堆的技术,完全不去考虑有没有必要用这些技术,有点儿 “为了学习而学习” 了。其实从学习的角度来说,多用点技术倒也没什么问题,但是如果你把这些写到简历上,面试官就会问你:“为什么要用这个技术?不用它行不行?”,这时你又该如何回答呢?
所以大家即使是自学项目,也建议找到合适的业务场景,合理运用技术。就像我昨天刚给大家讲完 DDD 领域驱动设计,有些同学就表示 “以后就用 DDD 架构来做项目了”,但其实大家自己做的项目,90% 以上是没必要用 DDD 的。
当然,多学新技术肯定是好的,相当于填充了我们的弹药库;但使用弹药时,肯定优先选择成本低的、最合适的。
4、不够灵活
我在讲 DDD 领域驱动设计时,先问了大家一个问题:如果必须要 2 选 1,你觉得开发项目时理论和实践哪个更重要?
结果大家一致选择了 “实践”。
没错,理论再完美,不能落地也无法创造价值;理论再丰富,也不一定能满足所有的实践需要。做项目时,理论的指导固然重要,但一定要结合实际情况按需运用和调整。
举个例子,大家学数据库理论的时候,老师可能会讲 “我们可以通过外键来保证数据完整性,要遵循第三范式,要遵循 ACID 原则”。但实际开发中,我们可能会用逻辑外键(不添加外键约束)的方式来实现表之间的关联,可能会违反数据一致性,但是能提高写入性能。
我在带大家做项目的过程中,发现很多同学就会特别执着于 “理论和规范”,比如:
- 你的目录命名怎么是 utils 而不是 util?为什么数据库对象用 Entity 而不是 PO?
- 为什么你的数据库字段用驼峰而不是下划线?
- 为什么你只创建一个对象,却不使用单例模式?
- 为什么你的接口不遵循 Restful 的规范,删除资源时还是使用 Post 请求?
这些都是我经常收到的问题,但其实都是无足轻重的问题。大家不要把时间浪费在纠结理论或规范上,毕竟这些都是人定的,在不违背原则或产生 Bug 的情况下,我们保证团队内部、或者自己开发时的规范保持一致即可。养成统一的编码风格和开发习惯,也能帮我们提高开发效率,没必要完全和教程保持一致。
除了上面几点外,我们也要时刻把握自己的学习重点,比如后端方向的同学,就尽量不要花时间在调试前端的样式上。像我大学的时候就是学的有点太杂了,有一段时间沉迷于抠前端的像素无法自拔,现在回过头来想想,确实浪费了太多时间。
来源:juejin.cn/post/7459798158336458792
什么?Flutter 又要凉了? Flock 是什么东西?
今天突然看到这个消息,突然又有一种熟悉的味道,看来这个月 Flutter “又要凉一次了”:
起因 flutter foundation 决定 fork Flutter 并推出 Flock 分支用于自建维护,理由是:
foundation 推测 Flutter 团队的劳动力短缺,因为 Flutter 需要维护 Android、iOS、Mac、Window、Linux、Web 等平台,但是 Flutter团队的规模仅略有增加。
在 foundation 看来,保守估计全球至少有 100 万 Flutter 相关开发者,而 Flutter 团队的规模大概就只有 50+ 人,这个比例并不健康。
问题在于这个数据推测就很迷,没有数据来源的推测貌似全靠“我认为”。。。。
另外 foundation 做这个决定,还因为 Flutter 官方团队对其 6 个支持的平台中,有 3 个处于维护模式(Window、Mac、Linux),所以他们无法接受桌面端的现场,因为他们认为桌面端很可能是 Flutter 最大的未开发价值。
关于这点目前 PC 端支持确实缓慢,但也并没有完全停止,如果关注 PC issue 的应该看到, Mac 的 PlatformView 和 WebView 支持近期才初步落地。
而让 foundation 最无法忍受的是,issue 的处理还有 pr 的 merge 有时候甚至可能会积累数年之久。
事实上这点确实成立,因为 Flutter 在很多功能上都十分保守,同时 issue 量大且各平台需求多等原因,很多能力的支持时间跨度多比较长,例如 「Row/Column 即将支持 Flex.spacing」 、「宏编程支持」 、「支持 P3 色域」 等这些都是持续了很久才被 merge 的 feature 。
所以 Flutter 的另外一个支持途径是来自社区 PR,但是 foundation 表示 Flutter 的代码 Review 和审计工作缓慢,并且沟通困难,想法很难被认可等,让 foundation 无法和 Flutter 官方有效沟通。
总结起来,在 foundation 的角度是,Flutter 官方团队维护 Flutter 不够尽心尽力。
所以他们决定,创建 Flutter 分支,并称为 Flock:意寓位 “Flutter+”。
不过 foundation 表示,他们其实并不想也不打算分叉 Flutter 社区,Flock 将始终与 Flutter 保持同步。
Flock 的重点是添加重要的错误修复和全新的社区功能支持,例如 Flutter 团队不做的,或者短期不会实现:
并且 Flock 的目的是招募一个比 Flutter 团队大得多的 PR 审查团队,从而加快 PR 的审计和推进。
所以看起来貌似这是一件好事,那么为什么解读会是“崩盘”和“内斗”?大概还是 Flutter 到时间凉了,毕竟刚刚过完 Flutter 是十周年生日 ,凉一凉也挺好的。
来源:juejin.cn/post/7431032490284236839
一次关键接口设计和优化带来的思考
实习时负责实现一个任务新增的接口,本来以为应该可以轻松拿捏,结果在实现过程中发现还有点小复杂,优化了很多版,并且其中涉及到了很多之前学过的知识点,故记录一下。
接口基本信息
在无人机管理系统中,对无人机执行任务时的监控是非常重要的模块,系统的用户可以为无人机创建新的飞行任务,除了任务的基本信息外,用户还需要为飞行任务分配负责人,设备,飞手(操作无人机的人),航线,栅栏(任务区域)等信息,而后端实现时需要做好各种校验,对用户数据进行整理转换并插入不同的数据库表中,考虑与系统其他模块的关系(例如航线稽查模块),在系统内通知相关用户,发送邮件给相关用户,另外还要考虑接口幂等性,数据库事务问题,接口的进一步优化。
接口实现
- 参数校验
- 参数非空校验,格式校验,业务上的校验。
- 其中业务上的校验比较复杂:要保证设备,飞手,航线都存在,且是一 一对应关系;要确保任务的负责人有权限调动相关设备和人员(认证鉴权模块);确保设备,飞手都是可用状态;要检查设备所在位置与任务区域;要检查设备在指定时间内是否已被占用。
- 幂等性校验
- 新增或编辑接口都可能会产生幂等性问题,尤其这种关键的新增接口一般都要保证幂等性。
- 这里我使用的方案是创建任务时生成一个token保存在redis中,并返回给前端,前端提交任务时在请求中携带token,后端检查到redis中有token证明是第一次访问,删除token并执行后续逻辑(去redis中查并删除token用lua脚本保证原子性),如果请求重复提交则后端查不到token直接返回。
- 也顺便研究了一下其他幂等性方案,包括前端防重复提交,唯一id限制数据库插入,防重表,全局唯一请求id等,发现还是目前使用redis的这种方案更简单高效。
- 生成任务对象,设置任务基本信息,并将下列得到信息赋予任务对象
- 从线程上下文获取到当前用户信息设为负责人
- 用设备id,用户id去对应表批量查找对应数据(注意一个任务中设备,飞手,航线是一 一对应,为一个组合,一个任务中可能有多个这种组合)
- 将航线转化为多个地理点,保存到列表用于后续批量插入任务航线表
- 为每条航线创建稽查事务对象,保存到列表用于后续批量插入稽查表
- 将任务区域转化为多个地理点,保存到列表用于后续批量插入任务区域表
- 批量插入数据
- 将任务对象插入任务表,将之前保存的列表分别批量插入到航线表,区域表,稽查表。
- 任务创建成功
- 更新任务状态
- 通过Kafka异步发送邮件通知飞手和负责人
private final String LUA_SCRIPT =
"if redis.call('EXISTS', KEYS[1]) == 1 then\n" +
" redis.call('DEL', KEYS[1])\n" +
" return true\n" +
"else\n" +
" return false\n" +
"end";
DefaultRedisScript<Boolean> script = new DefaultRedisScript<>(LUA_SCRIPT, Boolean.class);
Boolean success = redisTemplate.execute(script, Collections.singletonList(token));
if (success == null || !success) {
throw new Exception(GlobalErrorCodeEnum.FAIL);
}
// 后续业务逻辑
接口优化
费尽九牛二虎之力写完接口,de完bug后,真正的挑战才开始,此时测试了一下接口的性能,好家伙,平均响应时间1000多ms,肯定是需要优化的,故开始思考优化方案以及测试方案。
压测方案
- 先屏蔽幂等性校验,设置好接口参数(多个设备,航线长度设置为较长,区域正常设置)
- 在三种场景下进行测试(弱压力场景:1分钟内100个用户访问。高并发场景:1秒内100个用户访问。高频率场景:2个用户以10QPS持续访问10秒)。以下图片是相关设置
- 主要关注接口的平均响应时间,吞吐量和错误率。同时CPU使用率,磁盘IO,网络IO也要关注。
优化方案1
首先是把接口中一些不必要的操作删除;并且需要多次查询和插入的数据库操作都改为了批量操作;调整好索引,确保查询能正常走索引。代码与压测结果如下:
注意本文提供的代码仅用于展示,只展示关键步骤,不包含完整实现,若代码中有错误请忽略,理解思路即可。
弱压力和高频率下接口的平均响应时间降低为200ms左右,高并发情况下仍然需要500ms以上,没有出现错误情况,吞吐量也正常。看来数据库操作还是主要耗时的地方。
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = EcpException.class)
public boolean insertTask(TaskInfoVO taskInfoVO) {
TaskInfo taskInfo = new TaskInfo();
// 基本信息查询与填充,分配负责人
// ...
// 查询并分配设备
// ...
List<Devices> devices = deviceService.selectList(new QueryWrapper<dxhpDevices>().in("identity_auth_id", identityAuthIds));
taskInfo.setDevice(getIdentityAuthId());
// 查询并分配飞手
// ...
List<User> devicePerson = userService.selectBatchIds(devicePersonIds);
taskInfo.setDevicePerson(getDevicePerson());
// 处理并分配航线
// ...
List<TaskTrajectory> trajectoryList = getTrajectoryList(taskInfoVO.getTrajectoryList(), taskId);
taskInfo.setTaskTrajectoryId(trajectorysId);
// 对每条航线创建初始稽查记录
// ...
List<Check> checkList = getCheckList(taskInfoVO, taskId, trajectorysId);
taskInfo.setCheckEventId(checkEventsId);
// 分配区域
// ...
List<Range> taskRangeList = getTaskRange(range, taskId);
taskInfo.setTaskRangeId(taskRangeId);
// 插入任务表
this.dao.insert(taskInfo);
// 批量插入任务航线表
trajectoryService.insertBatch(trajectoryList);
// 批量插入任务区域表
...
// 批量插入稽查表
...
}
优化方案2
这里发现数据库的主键使用了uuid,根据之前的学习,uuid是无序的,在插入数据库时会造成页分裂导致效率降低,故考虑把uuid改为数据库自增主键。压测结果如下:
三种情况下的接口平均响应时间都略有降低,但是我重复测试后又发现有时几乎与之前一样,效果不稳定,所以实际使用uuid插入是否真的比自增id插入效率低还不好说,要看具体业务场景。
后来问了导师为什么用uuid做主键,原因是使用uuid方便分库分表,因为不会重复,而自增id在分库分表时可能还要考虑每个表划分id起始点,比较麻烦。
另外,在分布式系统中分布式id的生成是个很重要的基础服务,除了uuid还有雪花算法,数据库唯一主键,redis递增主键,号段模式。
优化方案3
串行改为并行,开启多个线程去并行查询不同模块的数据并做数据库的插入操作,主要使用CompletableFuture类。代码和压测结果如下:
三种场景平均响应时间分别为:82ms,397ms,185ms。弱压力和高频率下性能有所提升,高并发下提升不明显,原因是高并发情况本身CPU就拉满了,再使用多线程去并行就没什么用了。
另外这里使用了自定义的线程池,实际业务中如果需要使用线程池,需要合理设置线程池的相关参数,例如核心线程池,最大线程数,线程池类型,阻塞队列,拒绝策略等,还要考虑线程池隔离。并且需要谨慎分析业务逻辑是否适合使用多线程,有时候加了多线程反而效果更差。
// 开启异步线程执行任务,指定线程池
CompletableFuture.runAsync(() -> {
// 处理航线数据
// ...
List<TaskTrajectory> trajectoryList = getTrajectoryList(taskInfoVO.getTrajectoryList(), taskId);
taskInfo.setTaskTrajectoryId(trajectorysId);
// 批量插入数据库
trajectoryService.insertBatch(trajectoryList)
}, executor);
// 其他模块的操作同理
优化方案4
开启Kafka,将插入操作都变为异步的,即任务表的数据插入后发消息到Kafka中,其他相关表的插入都通过去Kafka中读取消息后再慢慢执行。代码和压测结果如下:
弱压力和高频率下的性能差异不大,但是高并发情况下接口的响应时间又飙到了近1000ms。
经过排查,在高并发时CPU和网络IO都拉满了,应该是瞬时向Kafka发送大量消息导致网卡压力比较大,接口的消息发送不出去导致响应时间飙升。如果是正常生产环境下肯定有多台机器分散请求,同时发数据到Kafka,并且有Kafka集群分担接收压力,但是目前只能在我自己机器上测,故高并发场景下将1秒100个请求降为1秒20个请求,并且前面的优化重新测试,比较性能。结果如下
批量插入:接口平均响应时间354ms
uuid改为自增id:接口平均响应时间323ms
串行改并行:接口平均响应时间331ms
用kafka做异步插入:接口平均响应时间191ms
可以看出使用了异步插入后效果还是十分明显的,且CPU和网络IO也都处于合理的范围内。至此优化基本结束,从一开始的近1000ms的响应速度优化到200ms左右,还是有一定提升的。
// 生产者代码
List<TaskTrajectory> trajectoryList = getTrajectoryList(taskInfoVO.getTrajectoryList(), taskId);
String key = IdUtils.uuid(); // 标识不同数据,方便后续Kafka消息防重
MessageVO messageVO = new MessageVO();
messageVO.setMsgID("trajectoryService"); // 告知要操作的类
messageVO.setMsgBody(JSON.toJSONString(trajectoryList)); // 要操作的数据
// 发送消息并指定主题和分区
kafkaTemplate.send("taskTopic", "Partition 1", JSON.toJSONString(messageVO));
// 消费者代码
// 使用 @KafkaListener监听并指定对应的主题和分区
@KafkaListener(id = "listener", topics = "taskTopic", topicPartitions = @TopicPartition(topic = "taskTopic", partitions = "0"))
public void recvTaskMessage(String message, Acknowledgment acknowledgment) {
// 接收消息
MessageVO messageVo = JSON.parseObject(message, MessageVO.class);
// 根据消息的唯一ID,配合redis判断消息是否重复
...
// 消费消息
List<TaskTrajectory> list = JSON.parseArray(messageVo.getMsgBody(), TaskTrajectory.class);
trajectoryService.insertBatch(list);
//手动确认消费完成,通知 Kafka 服务器该消息已经被处理。
acknowledgment.acknowledge();
}
其他问题
问题1
引入了Kafka后需要考虑的问题:消息重复消费,消息丢失,消息堆积,消息有序。
消息重复消费:生产者生成消息时带上唯一id,保存到redis中,消费者消费过后就把id从redis中删除,若有重复的消息到来,消费者去redis中找不到对应id则不处理。(与前面的接口幂等性方案类似)
消息丢失:生产者发送完消息后会回调判断消息是否发送成功,Kafka的Broker收到消息后要回复ACK给生产者,若没有发送成功要重试。Kafka自身则通过副本机制保证消息不丢失。消费者接收并处理完消息后才回复ACK,即设置手动提交offset。
消息堆积:加机器,提高配置,生产者限流。
消息有序:一个消费者消费一个partition,partition中的消息有序,消费者按顺序处理即可。若消费者开启多线程,则要考虑在内存中为每个线程开启队列,相同key的消息按顺序入队处理。
问题2
长事务问题:像新增任务这类接口肯定是需要加事务的,一开始我直接使用了spring的声明式事务,即@Transactional,并且我看其他业务接口好像也都是这样用的,后来思考了一下新增任务这个接口要先查好几个表,再批量插入好几个表,如果用@Transactional全锁住了那肯定会出问题,故后来使用TransactionTemplate编排式事务只对插入的操作加事务。
另外,远程调用的方法也不用加事务,因为无法回滚远程的数据库操作,除非加分布式事务(效率低),一般关键业务远程调用成功但是后续调用失败的话需要设计兜底方案,对远程调用操作的数据进行补偿,保证最终一致性。
// 避免长事务,不使用@Transactional,使用事务编排
transactionTemplate.execute(transactionStatus -> {
try {
this.dao.insert(taskInfo);
trajectoryService.insertBatch(trajectoryList);
...
} catch (Exception e) {
transactionStatus.setRollbackOnly(); // 异常手动设置回滚
}
return true;
});
问题3
线程池隔离:一些关键的接口使用的线程池要与普通接口使用的线程池隔离,否则一旦普通接口把线程池打满,关键接口也会不可用。例如我上面的优化有使用了多线程,可能需要单独开一个线程池或者使用与其他普通接口不同的线程池。
第三方接口异常重试:如果说需要调用第三方接口或者远程服务,需要做好调用失败的兜底方案,根据业务考虑是重试还是直接失败,重试的时间和次数限制等。
接口的权限:黑白名单设置,可用Bloom过滤器实现
日志:关键的业务代码注意打日志进行监测,方便后续排查异常。
以上是我在设计实现一个重要接口,并对其进行优化时所思考的一些问题,当然上面提到的内容不一定完全正确,可能有很多还没考虑到的地方,有些问题也可能有更成熟的解决方案,但是整个思考过程还是很有收获的,期待能够继续成长。
来源:juejin.cn/post/7410601536126795811
2024年年终总结-就业-失业-就业
身为一个在长沙的iOSer(loser),经历了原生开发,失业,再回原公司Flutter,再失业,再原生的曲折道路。
东家1开始是原生开发,搞海外工具。小日子也算惬意,没人盯着干活,进度都是自己说了算。到了五一左右就通知可能要裁员,提前一个月告知,让我去找工作,说是要招Android了,iOS这边没收益了。想着反正要找工作了,就学下Flutter,学了三天逐渐可以上手了。。。老板突然问我要去找工作没,就如实告知在学Flutter,问能写Android么,得到我肯定的答复之后就说让我继续干,写Android。本以为又能继续苟着,苟到了国庆前夕,临近放假。又通知我要裁员,提前一个月通知,让我去找工作。。。于是这次就真出去了
说来也巧,出来面试就找到了东家2,就在东家1对面;这边大小周,但是其他福利比上家好。上家约定10.30离职。这家约定11.4入职,11.29开始跟公司旅游;周五固定下午茶。但这边也是海外工具,可能也是这家喊我来的原因吧。
2024,苟着的一年。iOS悔不当初,2025继续苟着吧
各种O(PO,BO,DTO,VO等) 是不是人为增加系统复杂度?
在Java和其他编程语言的开发过程中,经常会用到几个以"O"结尾的缩写,比如PO,BO,DTO,VO等等,O在这里是Object的缩写,不同的O代表了不同的数据类型,很多时候这些O看起来都是差不多的,干的事情好像也只是一个简单的封装,那么搞出这么多O出来是不是人为增加了系统的复杂度呢?
各种O都是干什么的?
想要搞清楚标题中的问题,我们首先得了解这些O都是什么东西?这里给大家介绍几种常见的O:
- PO (Persistent Object) - 持久化对象。 持久化对象通常对应数据库中的一个表,主要用于表示数据库中存储的数据。PO中的属性通常和数据表的列一一对应,用于ORM(对象关系映射)框架中,如Hibernate,JPA等。
- BO (Business Object) - 业务对象。 业务对象主要封装了业务逻辑。它可以包含多个PO,或者是一个PO的扩展,增加了业务处理的逻辑。BO通常在业务层被使用,用于实现业务操作,比如计算、决策等。
- VO (Value Object) - 值对象。 值对象是一种用于传输数据的简单对象,它通常不包含业务逻辑,只包含数据属性和get/set方法。值对象主要用于业务层与表示层之间的数据传递,它的数据可能是由多个PO组合而成。
- DTO (Data Transfer Object) - 数据传输对象。 数据传输对象类似于VO,它也是用于层与层之间的数据传递。DTO通常用于远程通信,比如Web服务之间的数据传递。DTO通常不包含任何业务逻辑,只是用于在不同层次或不同系统之间传输数据。
有时候我们还会看到DO、POJO等概念,它们又是什么呢?
- DO (Domain Object) - 领域对象。 领域对象是指在问题领域内被定义的对象,它可以包含数据和行为,并且通常代表现实世界中的实体。在DDD(领域驱动设计)中,领域对象是核心概念,用于封装业务逻辑和规则。这里需要注意DO和BO的区别,虽然都是搞业务逻辑,DO通常是业务领域中单一实体的抽象,它关注于单个业务实体的属性和行为;而BO则通常涉及到业务流程的实现,可能会协调多个DO来完成一个业务操作。
- POJO (Plain Old Java Object) - 简单老式Java对象。 POJO是指没有遵循特定Java对象模型、约定或框架(如EJB)的简单Java对象。POJO通常用于表示数据结构,它们的实例化和使用不依赖于特定的容器或框架。
为什么要划分各种O?
在软件开发中划分不同的O主要是为了实现关注点分离(Separation of Concerns,SoC),提高代码的可维护性、可读性和可扩展性。
关注点分离的典型案例:MVC模式。
下面展开列举了一些划分这些对象的原因:
- 明确职责:通过将不同的职责分配给不同的对象,可以使每个对象都有明确的职责,这样代码更容易理解和维护。
- 减少耦合:不同层次之间通过定义清晰的接口(如特定的对象)交互,减少了直接的依赖关系,降低了耦合度。
- 抽象层次:通过定义不同的对象,可以在不同的抽象层次上操作,比如在数据层处理PO,在业务层处理BO,这样可以在合适的层次上做出决策。
- 灵活性:当系统需要变更时,由于职责和层次的清晰划分,更容易做出局部的修改而不影响到整个系统。不同的对象可能针对性能有不同的优化,例如PO可能被优化以提高数据库操作的性能。
- 安全性:通过使用不同的对象,可以控制敏感数据的暴露。例如,可以在DTO中排除一些不应该传输到前端的敏感信息。
- 测试性:分离的对象使得单元测试变得更加容易,因为可以针对每个对象进行独立的测试。
- 交互清晰:在不同的系统组件或层次之间传递数据时,清晰的对象定义可以让数据交互更加清晰,减少数据传递中的错误。
总之,通过划分各种“O”对象,开发者可以更好地组织代码,将复杂系统分解为更小、更易于管理的部分,同时也有助于团队成员之间的沟通和协作。这种划分在设计模式和软件工程实践中是一种常见且有效的方法。
OO不分的惨痛经历
说个实际的惨痛经验。
很多时候我会感觉这些O之间存在很多重复的代码,比如重复的属性定义、简单的方法封装,DRY(Don't Repeat Yourself)原则不是说让大家避免重复嘛,所以我也曾经尝试在程序中统一它们。
但是总有一些O之间存在或多或少的差异,比如:
- 这个O需要一个A属性,仅用于内部状态管理,不会暴露到外部,其它O都不需要。
- 还有这个接口需要返回一个B属性,其它接口都不需要。
这时候,你怎么办?如果使用同一个类型,那就得加上这些属性,尽管它们在某些时候用不到。根据你的选择,你可能在所有的地方都给这个属性赋值,也可能仅在业务需要的时候给他们赋值。
看个实际的例子:在一个复杂的电商系统中,商品的管理可能涉及到库存管理、价格策略、促销信息等多个方面。
// 商品类
public class Product {
private Long id; // 来自商品表
private String name; // 来自商品表
private double price; // 来自商品表,传输时需要特殊格式
private int stock; // 来自库存表,仅在下单判断中需要,展示层不需要
private String promotionInfo; // 来自促销表,展示层需要
// 构造器、getter和setter方法省略
}
但是这却带来了很大的危害:
- 调用接口的同学会问,这个属性什么时候会有值,什么时候会没值?
- 优化的同学会问,计算这个属性的值会影响性能,能删掉吗?
- 交接的同学会问,这个属性是干什么用的,为什么不给他赋值?
总之会增加了大量的沟通成本与维护难度。一旦这样做了,后边就会特别别扭,改不完,根本改不完。
在软件工程化的今天,各类O的设计看似增加了复杂度,但是实际上是对系统模块化、职责划分以及实际应用场景的合理抽象和封装,有助于提高软件质量和团队协作效率。
老老实实写吧,不同的O就是不同的东西,它们不是重复的,只是在代码上看着像,就像人有四肢,动物也有四肢,但是它们不能共用,否则出来的就是四不像。
图片来源:ozhanozturk.com/2018/01/28/…
当然如果只是一个很简单的程序或者一次性的程序,我们确实没必要划分这么多的O出来,直接在接口方法中访问数据库也不是不可以的。
前端中O的使用
虽然各种O一般活跃在各种后端程序中,但是前端也不乏O的身影,只是没有后端那么形式化。
以下是一些可能在前端开发中遇到的以“O”结尾的数据对象:
- VO (View Object) - 视图对象。在前端框架中,VO可以代表专门为视图层定制的数据对象。这些对象通常是从后端接口获取的数据经过加工或格式化后,用于在界面上显示的对象。
- DTO (Data Transfer Object) - 数据传输对象。虽然DTO通常用于后端服务间的数据传输,但在前端中也可以用来表示从后端接口获取的数据结构。前端的DTO通常是指通过Ajax或Fetch API从服务器获取的原始数据结构。
- VMO (ViewModel Object) - 视图模型对象。在MVVM(Model-View-ViewModel)架构中,VMO可以代表视图模型对象,它是模型和视图之间的连接器。在Vue.js中,Vue实例本身就可以被看作是一个VMO,因为它包含了数据和行为,同时也是视图的反映。
- SO (State Object) - 状态对象 尽管不是标准的术语,但在使用如Vuex这样的状态管理库时,SO可以用来指代代表应用状态的对象。这些状态对象通常包含了应用的核心数据,如用户信息、应用设置等。
在实际的Vue开发过程中,开发者可能不会严格区分这些概念,而是更多地关注于组件的状态、属性(props)、事件和生命周期。组件内部的数据通常以数据属性(data)的形式存在,而组件间的数据传递通常使用属性(props)和事件(emits)。在处理与后端的数据交互时,开发者可能会定义一些专门的对象来适应后端的接口,但是这些都不是Vue框架强制的概念或规则。
简单地说,这些“O”其实就是帮我们把代码写得更清晰、更有条理,虽然一开始看着很麻烦,但时间一长,你会发现这样做能省下不少力气。就像我们的衣柜,虽然分类放好衣服需要点时间,但每天早上起来挑衣服的时候,不就轻松多了吗?
记住,合适的工具用在合适的地方,能让你事半功倍!
关注萤火架构,加速技术提升!
来源:juejin.cn/post/7336020150867230757
2024年终总结--在悲催中寻找希望的光
大家好,我是 V 哥。时光荏苒,岁月如梭,转眼间2024年已接近尾声。回首这一年,心中感慨万千。2024年是悲催的一年、心酸的一年、纠结的一年,在这充满挑战的大环境下,IT行业受到了巨大的冲击,就业市场的不景气让工作变得愈发难找。“外卖、快递、跑滴滴”成了人们口中的“铁人三项”,据说截止2024年,有8400万的从业者投身于外卖和快递行业,其中不乏大学生的身影。而造成这一现象的原因,除了大环境中中美之间的博弈,还有社会产业结构升级带来的变革。顺势而为,方能行稳致远。
书
在这样艰难的形势下,我也曾感到迷茫和无助。然而,我始终坚信,只要坚持不懈地努力,总会找到属于自己的方向。幸运的是,2024年我做对了一件事情,那就是坚持技术写作和写书。这一年里,我致力于鸿蒙开发的研究和创作,完成了鸿蒙开发三部曲:《ArkTS》、《HarmonyOS NEXT5应用开发》和《项目实战》。虽然这些书还未出版,但我相信它们将会为鸿蒙开发者们提供宝贵的参考资料,期待年后能与大家见面,在这也感谢清华大学出版社对我细致入微的帮助。
技术文章创作
在技术写作方面,我也取得了一些成果。我原创了282篇技术文章,总字数超过了100万字。当我意识到这个数字时,自己都吓了一跳。这相当于一本厚度5.6厘米的长篇小说呀,等同于一本曹雪芹的前80 回加上高鹗的后40回的《红楼梦》,也相当于100万字的《史记》。当然只是数字匹配而已,可不敢跟这些大家相提并论,也就自己瞎乐呵乐呵,还好这些文章在各大技术社区得到了广泛的传播,记住 V 哥的账号都是威哥爱编程哦,也也欢迎兄弟们来关注哈,V 哥掐指一算,拢共收获了150 万+网友们的阅读。功夫不负有心人呀,同时,我的努力也得到了平台的认可,2024年获得了CSDN博客专家称号、阿里云开发者社区专家博主称号、华为云云享专家称号以及掘金优秀创作者称号。又多了好些,没事儿,证不压身,做的事儿都是一样的,这些荣誉不仅是对我过去一年努力的肯定,更是激励我继续前行的动力。
关注【威哥爱编程】一起打造个人 IP
视频
除了写作,我还积极参与了技术视频的录制。2024年,录制了2024版《趣味Java》。全套课程由V哥,Mask、强哥、索尔四位老师共同打造,这是一套幽默风趣风格的技术视频,为的就是帮助更多初学者、大学生更好的入门编程语言,选择 Java,当然是大学里几乎计算机专业都会开设这门课,然而,不得不承认,Java领域的竞争实在是太激烈了,我感觉这套视频有些生不逢时。课程的设计、录制以及剪辑制作耗费了我大量的精力,但最终的效果却没有达到我的预期。尽管如此,我并不后悔投入的时间和精力,因为通过这次经历,我也学到了很多宝贵的经验,这将对我未来的工作产生积极的影响。短视频创作同样没有达到预期,期待自己2025年有更多突破。
技术大会与讲座
今年,V哥还参加了各种技术大会,与同行们交流分享经验,收集的嘉宾牌都有10几个,多数是华为的话动啦,保留起来,作为自己的成长见证吧,自我激励一下。同时,我还去过39所大学进行技术讲座,大概有50多场吧,有一半以上是鸿蒙专场,因为鸿蒙的势头正猛,也是 IT 赛道里为数不多比较火热的方向,相信2025年从15000个应用到10万,50万的APP 体量,要知道 iOS 的体量是180万应用,Android的体量是380万应用,鸿蒙的发展潜力是巨大的,还会有更多的兄弟会入场,V 哥坚信这一点,对灯发誓吧,不信咱看着瞧。通过这些讲座,我不仅能够将自己的知识和经验传授给学生们,还能够感受到他们对知识的渴望和对未来的憧憬,这让我更加坚定了自己的信念,要为推动技术的发展和人才的培养贡献自己的一份力量。
算算这一年,回来奔波了34个城市之间,即忙碌又充实。虽然过程中充满了疲惫和艰辛,但我也收获了许多宝贵的经验和回忆。在这个过程中,我学会了如何在压力下保持冷静,如何在困难面前坚持不懈,如何在不断变化的环境中调整自己的方向。
心态的成长
这一年,是磨心态的一年,当你遇到无能为力的事情时,焦虑、彷徨、五味杂陈,会带来很多思想层面的压力需要释放,你有更好的方法吗,也许每个人解压的方法不一样,V 哥的方式是看书,当你迷茫的时候,书是解药,今年非技术的书看了30本,自己的成长只能自己来鞭策。
回顾2024年,我感慨万千。这一年,我经历了许多挫折和困难,你是知道的,咱是报喜不报忧的人,不管吐槽也好,抱怨也罢,一笑而过,咱还得积极面对生活,去努力实现自己想要做到的事情,这一年,也离不开家人的支持和帮助。因为他们的包容和理解,让我有了坚实的后盾,在此,我想向他们表示衷心的感谢。
最后
最后,我想说的是,2024 年虽然充满了挑战和困难,但它也是我人生中宝贵的经历。它让我更加深刻地认识到了自己的不足和努力的方向,也让我更加坚定了自己的信念和目标。我相信,在未来的日子里,我一定能够取得更加优异的成绩,为自己的人生创造更加美好的未来!
展望2025年,V 哥一定会继续努力,不断提升自己的技术水平和综合素质。继续坚持技术创作和帮助更多想要学习技术的人,分享使我快乐,助人让我的人生更有意义。
2025,不用怀疑,坚定的抱着华为的大腿,向鸿蒙出发!有同行的兄弟么,加个好友一起出发,欢迎关注威哥爱编程。顺势而为,方能行稳致远。
来源:juejin.cn/post/7455167756105154597
程序员的出路
最近几天渐渐悟出来一个道理,做技术没有出路。
我在想如果哪天我失业了,没有公司要我,以我现在的经验和能力,我该怎么办?
结果是我什么都干不了,我的经验和能力必须依靠公司才有价值,我在社会上独立生存,跟普通人比,没有任何优势。
所以我觉得做技术,尤其是走技术专家路线,根本没有出路,路越走越窄,最后把自己绑死在公司。
这段时间买了不少课,学了很多思路,打开了视野,也非常认真的思考,找身边厉害的人聊,写出这篇文章,希望可以供你参考。
培养赚钱的能力
赚钱的思维:发现需求,满足需求,推广流量。
普通人能赚到大钱的地方,都是很 low 的。我说几个自己身边的和听过的案例。
有的爸妈上班孩子放学没人接,有人开个自习室把孩子从学校接过来,每个孩子 700 块,一个月赚几万块。
前几年搞视频号,有一哥们做了个租房合同的带货视频爆了,赚了 20 多万。
老婆表姐搞外贸,老早没上班自己开了公司,具体不知道赚了多少钱,但有倒腾鞋子袜子的案例,一年 100 个没有问题。
还有各种卖包装盒、打包盒、塑料袋、花生瓜子、酱菜、小板凳的案例,包括我今天下楼看了下小区周边,我们这条街在金融港正对面,不可否认有些店没过多久倒闭了,但还有些店,招牌已经破的不行还开着在,包括各种餐饮店、收废品、五金、中老年活动中心、中医推拿针灸按摩、宠物店。他们肯定是赚到了钱,而且事情本身没什么门槛。
这些事给我刺激够大的,世界上赚钱的方式不只打工,从大学出来是第一次进入社会,跳出工作是第二次进入社会。
把他们抽象一下,都是发现了某个需求,然后找到了解决方案,再把方案推广让更多人知道。
解决方案各种各样,可以是做个产品(美团、饿了么各种 App 都是),也可以是简单的对接下资源,找个服装厂、鞋厂,甚至直接在 PDD 上代发货,这也是为什么我觉得搞技术没出路原因,解决方案面太窄,资产太重,一个人干不起来。
对我未来有什么启发呢?我分别说说。
发现需求
这个真的难,没抽象出方法论,因为这东西是别人赚钱的底裤,没人会分享,我要有正在赚钱的路子我也不可能说出来,上面说出来的都是过时的。
可行的方案是保持好奇,保持观察,多了解案例,自己多领悟。接地气,多关注衣食住行、吃喝拉撒方面的。
有机会我准备去广州、福建那里有各种代工厂、工业园,找人聊聊积累一些下游资源,多和身边的人聊天换取信息,多认识这个世界,积累各种各样的资源。
满足需求
在职场上,可以往产品经理方向发展。能锻炼自己产品思维,对自己未来创业是有帮助的。
另外搞产品也需要一定的沟通能力,这也能对接上创业时的销售能力。不管搞什么,这两项能力都是必要的。
我在做开发前做过几年销售,而且后期写的一些项目,我也承担了部分产品的职责,往这个方向转,对我来说没有问题,尤其是偏技术的产品,更是信手拈来。
推广流量
往投流方向转,也是可行的。要想赚钱,就得离钱近,投流是离钱最近的。
我了解投流的大致逻辑,ROI 怎么跑正,每个环节数据怎么排查,做增量的大致方向。
这个方面我缺少具体可能遇到的细节经验,或者行业的一些潜规则,平台的规则等。
但其实问题不大,流量方法不是公开的,如果公开的大家都一窝蜂上去投,就会把价格越拉越高,直到超过成本。
重点是如何用更低的价格买到流量,这里面可能有很多野路子、钻空子、薅羊毛等。
而且我之前做视频号的时候,也做过各个赛道,累计有上千万播放和几万粉丝,也算有点经验。
所以往投放方向转,我也有信心做好。
技术本行
再说到技术老本行,我不会去钻研高精尖的东西,就各种大厂面试题里的情况,实话实说工作了 7 年,面试题里面的场景一个都没有遇到,就像知乎上问:什么时候用 ArrayList 什么时候用 LinkList?答曰:工作中用 ArrayList,面试时用 LinkList。
技术上精通基本数据类型、Java 核心、MySQL、分库分表、SpringBoot、Spring Cloud相关、Redis、Kafka、Idea 熟练度等,就差不多了。
重要的还有项目经验、架构设计、业务场景处理那些。我会把这些做成视频发到 B 站上,一方面可以提升自己的技能熟练度,另一方面也能为自己攒点影响力。
然后把精力放在横向扩展上,比如关注 GitHub 各种搭建完就能用的开源项目,思考他们可以用在哪些场景,满足哪些需求。
总之,不管是未来做什么,我的目标是提升自己的赚钱能力,也留意观察、抓住生活中机会。
加油,共勉。
来源:juejin.cn/post/7456417337676595212