mysql到底是join性能好,还是in一下更快呢?
大家好呀,我是楼仔。
今天发现一篇很有意思的文章,使用 mysql 查询时,是使用 join 好,还是直接 in 更好,这个大家工作时经常遇到。
为了方便大家查看,文章我重新进行了排版。
我没有直接用作者的结论,感觉可能会误导读者,而是根据实验结果,给出我自己的建议。
不 BB,上目录:
01 背景
事情是这样的,去年入职的新公司,之后在代码 review 的时候被提出说,不要写 join,join 耗性能还是慢来着,当时也是真的没有多想,那就写 in 好了。
最近发现 in 的数据量过大的时候会导致 sql 慢,甚至 sql 太长,直接报错了。
这次来浅究一下,到底是 in 好还是 join 好,仅目前认知探寻,有不对之处欢迎指正。
以下实验仅在本机电脑试验。
02 表结构
2.1 用户表
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '姓名',
`gender` smallint DEFAULT NULL COMMENT '性别',
`mobile` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `mobile` (`mobile`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1005 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
2.2 订单表
CREATE TABLE `order` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`price` decimal(18,2) NOT NULL,
`user_id` int NOT NULL,
`product_id` int NOT NULL,
`status` smallint NOT NULL DEFAULT '0' COMMENT '订单状态',
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `product_id` (`product_id`)
) ENGINE=InnoDB AUTO_INCREMENT=202 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
03 千条数据情况
数据量:用户表插一千条随机生成的数据,订单表插一百条随机数据
要求:查下所有的订单以及订单对应的用户
耗时衡量指标:多表连接查询成本 = 一次驱动表成本 + 从驱动表查出的记录数 * 一次被驱动表的成本
3.1 join
select order.id, price, user.name from order join user on order.user_id = user.id;
3.2 in
select id,price,user_id from order;
select name from user where id in (8, 11, 20, 32, 49, 58, 64, 67, 97, 105, 113, 118, 129, 173, 179, 181, 210, 213, 215, 216, 224, 243, 244, 251, 280, 309, 319, 321, 336, 342, 344, 349, 353, 358, 363, 367, 374, 377, 380, 417, 418, 420, 435, 447, 449, 452, 454, 459, 461, 472, 480, 487, 498, 499, 515, 525, 525, 531, 564, 566, 580, 584, 586, 592, 595, 610, 633, 635, 640, 652, 658, 668, 674, 685, 687, 701, 718, 720, 733, 739, 745, 751, 758, 770, 771, 780, 806, 834, 841, 856, 856, 857, 858, 882, 934, 942, 983, 989, 994, 995);
其中 in 的是order查出来的所有用户 id。
如此看来,分开查和 join 查的成本并没有相差许多。
3.3 并发场景
主要用php原生写了脚本,用ab进行10个同时的请求,看下时间,进行比较。
> ab -n 100 -c 10 // 执行脚本
下面是 join 查询的执行脚本:
$mysqli = new mysqli('127.0.0.1', 'root', 'root', 'test');
if ($mysqli->connect_error) {
die('Connect Error (' . $mysqli->connect_errno . ') ' . $mysqli->connect_error);
}
$result = $mysqli->query('select order.id, price, user.`name` from `order` join user on order.user_id = user.id;');
$orders = $result->fetch_all(MYSQLI_ASSOC);
var_dump($orders);
$mysqli->close();
下面是 in 查询的执行脚本:
$mysqli = new mysqli('127.0.0.1', 'root', 'root', 'test');
if ($mysqli->connect_error) {
die('Connect Error (' . $mysqli->connect_errno . ') ' . $mysqli->connect_error);
}
$result = $mysqli->query('select `id`,price,user_id from `order`');
$orders = $result->fetch_all(MYSQLI_ASSOC);
$userIds = implode(',', array_column($orders, 'user_id')); // 获取订单中的用户id
$result = $mysqli->query("select `id`,`name` from `user` where id in ({$userIds})");
$users = $result->fetch_all(MYSQLI_ASSOC);// 获取这些用户的姓名
// 将id做数组键
$userRes = [];
foreach ($users as $user) {
$userRes[$user['id']] = $user['name'];
}
$res = [];
// 整合数据
foreach ($orders as $order) {
$current = [];
$current['id'] = $order['id'];
$current['price'] = $order['price'];
$current['name'] = $userRes[$order['user_id']] ?: '';
$res[] = $current;
}
var_dump($res);
// 关闭mysql连接
$mysqli->close();
看时间的话,明显 join 更快一些。
04 万条数据情况
user表现在10000条数据,order表10000条试下。
4.1 join
4.2 in
order 耗时:
user 耗时:
4.3 并发场景
join 耗时:
in 耗时:
数据量达到万级别,非并发场景,in 更快,并发场景 join 更快。
05 十万条数据情况
随机插入后user表十万条数据,order表一百万条试下。
5.1 join
5.2 in
order 耗时:
user 耗时:
order查出来的结果过长了...
5.3 并发场景
join 耗时:
in 耗时:
数据量达到十万/百万级别,非并发场景,in 过长,并发场景 join 更快。
06 总结
实验结论:
- 数据量不到万级别,join 和 in 差不多;
- 数据量达到万级别,非并发场景,in 更快,并发场景 join 更快;
- 数据量达到十万/百万级别,非并发场景,in 过长,并发场景 join 更快。
下面是楼仔给出的一些建议。
当数据量比较小时,建议用 in,虽然两者的性能差不多,但是 join 会增加 sql 的复杂度,后续再变更,会非常麻烦。
当数据量比较大时,建议用 join,主要还是出于查询性能的考虑。
不过使用 join 时,小表驱动大表,一定要建立索引,join 的表最好不要超过 3 个,否则性能会非常差,还会大大增加 sql 的复杂度,非常不利于后续功能扩展。
最后,把楼仔的座右铭送给你:我从清晨走过,也拥抱夜晚的星辰,人生没有捷径,你我皆平凡,你好,陌生人,一起共勉。
原创好文:
来源:juejin.cn/post/7306322677039218724
自研一套带双向认证的Android通用网络库
当前,许多网络库基于Retrofit或OkHttp开发,但实际项目中常需要定制化,并且需要添加类似双向认证等安全功能。这意味着每个项目都可能需要二次开发。那么,有没有一种通用的封装方式,可以满足大多数项目需求?本文将介绍一种通用化的封装方法,帮助你用最少的代码量开发出自己的网络库。
源码及涉及思路参考:如何开发一款安全高效的Android网络库(详细教程)
框架简介
FlexNet 网络库是基于 Square 公司开源的 Retrofit 网络框架进行封装的。Retrofit 底层采用 OkHttp 实现,但相比于 OkHttp,Retrofit 更加便捷易用,尤其适合用于 RESTful API 格式的请求。
在网络库内部,我们实现了双向认证功能。在初始化时,您可以选择是否开启双向认证,框架会自动切换相应的 URL,而业务方无需关注与服务端认证的具体细节。
接入方式
1. 本地aar依赖
下载aar到本地(下载地址见文末),copy到app的libs目录下,如图:
implementation(files("libs/flex-net.aar"))
然后sync只会即可
2. 通过Maven远程依赖
FlexNet目前已上传Maven,可通过Maven的方式引入,在app的build.gradle中加入以下依赖:
implementation("com.max.android:flex-net:3.0.0")
sync之后即可拉到Flex-Net
快速上手
网络库中默认打开了双向认证,并根据双向认证开关配置了相应的 baseUrl,大多数场景下只需要控制双向认证开关,其余配置走默认即可。
初始化
在发起网络请求之前(建议在Application
的onCreate()
中),调用:
fun initialize(
app: Application,
logEnable: Boolean = BuildConfig.LOG_DEBUG,
sslParams: SSLParams? = null,
)
- application: Application类型,传入当前App的Application实例;
- logEnable: Boolean类型,网络日志开关,会发打印Http的Request和Resonpse信息,可能涉及敏感数据,release包慎用;(仅限网络请求日志,和双向认证的日志不同)
- sslParams: 双向认证相关参数,可选,为空则关闭双向认证。具体描述见下文。
当App需要双向认证功能时,需要在initialize()
方法中传递sslParams参数,所有双向认证相关的参数都放在sslParams当中,传此参数默认打开双向认证。
SSLParams的定义如下:
data class SSLParams(
/** App 是否在白名单之中。默认不在 */
val inWhiteList: Boolean = false,
/** 双向认证日志开关,可能涉及隐私,release版本慎开。默认关 */
val logSwitch: Boolean = true,
/** 是否开启双向认证。默认开 */
val enable: Boolean = true,
/** 双向认证回调。默认null */
val callback: MutualAuthCallback = null,
)
- inWhiteList: App是否在白名单中,默认不在
- logSwitch: 双向认证日志开关,可能涉及隐私,release版本慎开。默认关,注意这里仅针对双向认证日志,与
initialize()
方法中的logEnable
不同 - callback : 监听初始化结果回调,true表示成功,反之失败。可选参数,默认为null,仅
enableMutualAuth
为true时有效
在调用了initialize
之后就完成了初始化工作,内部包含了双向认证、网络状态、本地网络缓存等等功能,所有的网络请求都需要在初始化之后发起。
初始化示例代码:
FlexNetManger.initialize(this,
logEnable = true,
SSLParams {
Timber.i("Mutual auth result : $it")
})
PS *: *部分App在启动的时候获取不到证书,所以这里会失败。如果失败了后续可以在合适的时机通过MutualAuthenticate.isSSLReady()
来检查是否认证成功,然后通过MutualAuthenticate.suspendBuildSSL()
来主动触发双向认证,成功之后方可开始网络请求。具体可参见文档“配置项”的内容。
双向认证失败及其相关问题,可参考双向认证文档 : [双向认证])
定义数据 Model
在请求之前需要根据接口协议的字段定义对应的数据Model,用来做Request或者Response的body。
比如我们需要通过UserId获取对应用户的UserName
定义 Request 数据 Model
后端请求接口参数如下:
{
"userId" : "123456"
}
那么根据参数定义一个UserNameReq类:
data class UserNameReq(
/** 用户id */
var userId: String
)
定义 Response 数据 Model
后端返回数据如下:
{
"userName" : "MC"
}
对应定义一个UserNameRsp:
data class UserNameRsp(
/** 用户id */
var userId: String
)
编写 Http 接口
接口类必须继承自IServerAPI:
interface UserApi: IServerApi
然后在IServerApi的实现类中,每个接口需要用注解的形式标注 Http 方法,通过参数传入 http 请求的 url:
interface UserApi: IServerApi {
/** 获取用户ID */
@POST("api/cloudxcar/atmos/v1/getName")
suspend fun getUserName(@Body request: UserNameReq): ResponseEntity
}
这里需要注意的是,我们的UserNameRsp需要用ResponseEntity封装一层,看一下ResponseEntity的内容:
sealed class ResponseEntity<T>(val body: T?, val code: Int, val msg: String)
有3个参数:
- body: 消息体,即UserNameReq。仅成功时有效
- code : 返回码,这里要分多种情况描述。
- Http错误:此时code为Http错误码
- 其他异常:code对应错误原因,后面会附上映射表
- 请求成功:区分网络数据和缓存数据
- msg : 错误信息
可调用ResponseEntity.isSuccessful()
来判断是否请求成功,然后通过ResponseEntity.body
获取数据,返回的是一个根据服务端返回的 Json 解析而来的UserNameRsp实体类。
如果请求失败,则从ResponseEntity.msg
和ResponseEntity.code
中获取失败ma失败码和失败提示
创建网络请求Repo
继承自BaseRepo,泛型参数为步骤3中创建的IserverApi实现类:
class VersionRepo : BaseRepo<VersionAPI>
- 其中需要有1个必覆写的变量:
- baseUrl: 网络接口的baseUrl
- 两个可选项:
- mutualAuthSwitch: 双向认证开关,此开关仅针对当前 baseUrl 生效。默认开
- interceptorList: 需要设置的拦截器列表
- 一个必覆写的方法:
- createRepository(): 创建当前网络仓库
完整的Repo类内容如下:
class UserRepo: BaseRepo<UserApi>() {
// 必填
override val baseUrl = "https://juejin.cn/editor/drafts/7379502040140218422"
// 必填
override fun createRepository(): VersionAPI =
MutualAuthenticate.getServerApi(baseUrl, mutualAuthSwitch, interceptorList)
// 可选:双向认证开关,仅针对当前repo生效
override val mutualAuthSwitch = true
// 可选:Http拦截器
override val interceptorList: List? = listOf(HeaderInterceptor())
// 请求接口
suspend fun getUserName(): ResponseEntity{
return mRepo.upgradeVersion(UserNameReq("123456"))
}
}
注: 其中拦截器的设置interceptorList,如果声明的时候提示错误,可以尝试加上完整的类型声明:
interceptorList: List?
5 发起网络请求
最后就可以在业务代码中通过Repo类完成网络请求的调用了:
lifecycleScope.launch {
val entity= UserRepo().getUserName()
Timber.i("Get responseEntity: $entity")
if (entity.isSuccessful()) {
val result = entity.body
Timber.i("Get user name result: $result")
} else {
val code = entity.code
val msg = entity.msg
Timber.i("Get user name failed: code->$code; msg->$msg")
}
}
到这里,就可以发起一次基础的网络请求接口了。
依赖项
双向认证
目前引入的双向认证版本为1.6.0,如果需要切换版本,或者编译出现依赖冲突,可以尝试使用exclude的方式自行依赖。当然也请自行确保功能正常。
日志库
implementation("com.jakewharton.timber:timber:4.7.0")
组件库中的日志库。FlexNet推荐宿主使用Timber进行日志输出,但是需要宿主App在初始化FlexNet之前对Timber做plant操作。
网络请求内核
// Net
implementation ("com.squareup.retrofit2:retrofit:2.9.0")
implementation ("com.squareup.retrofit2:converter-gson:2.9.0")
底层网络请求目前依赖OkHttp完成。
本地持久化
implementation("com.tencent:mmkv:1.2.14")
网络库中的本地存储,主要用于保存网络缓存,目前采用MMKV-1.2.14版本,同样如果有冲突,或者需要另换版本,可通过exclude实现。
Gson
api(core.network.retrofit.gson) {
exclude(module = "okio")
exclude(module = "okhttp")
}
依赖Gson,用于做数据结构和Json的相互转化
错误码对照表
CODE_SUCCESS | 10000 | 请求成功,数据来源网络 |
---|---|---|
CODE_SUCCESS_CACHE | 10001 | 返回成功,数据来源于本地缓存 |
CODE_SUCCESS_BODY_NULL | 10002 | 请求成功,但消息体为空 |
CODE_ERROR_UNKNOWN | -200 | 未知错误 |
CODE_ERROR_UNKNOWN_HOST | -201 | host解析失败,无网络也属于其中 |
CODE_ERROR_NO_NETWORK | -202 | 无网络 |
日志管理
从FlexNet 2.0.5开始,对接入方使用的日志库不再限制(2.0.5以下必须用Timber,否则无日志输出)。可以通过以下接口来设置日志监视器:
setLogMonitor(log: ILog)
设置之后所有的网络日志都会回调给ILog,即可由接入方自行决定如何处理日志数据。
如果没有设置LogMonitor
,则会使用Timber
或者Android
原生Log
来进行日志输出。当宿主App的Timber挂载优先于FlexNet的初始化,则会采用Timber做日志输出,反之使用Android Log。
文件下载
网络库内置了下载功能,可配置下载链接和下载目录。注意外部存储地址需要自行申请系统权限。
1 构建下载器
使用Downloader.builder()
来构建你的下载器,Builder需要传入以下参数:
- url:待下载文件的url
- filePath:下载文件路径
- listener:下载状态回调。可选参数,空则无回调
示例代码如下:
Downloader.Builder("https://juejin.cn/editor/drafts/7379502040140218422.zip",
File(requireContext().filesDir, "MC").absolutePath)
2 回调监听
builder()
最后一个参数,可传入下载监听器接口DownloadListener
,内部有3个方法需要实现:
- onFinish(file: File): 下载完成,返回下载完成的文件对象
- onProgress( progress : Int, downloadedLengthKb: Long, totalLengthKb: Long): 下载进度回调,回传进度百分比、已下载的大小、总大小
- onFailed(errMsg: String?): 下载失败,回调失败信息
示例代码如下:
val downloader = Downloader.Builder("https://juejin.cn/editor/drafts/7379502040140218422.zip",
File(Environment.getExternalStorageDirectory(), "MC").absolutePath,
object : DownloadListener {
override fun onFinish(file: File) {
Timber.e("下载的文件地址为:${file.absolutePath}".trimIndent())
}
override fun onProgress(
progress: Int,
downloadedLengthKb: Long,
totalLengthKb: Long,
) {
runOnUiThread {
textView.text =
"文件文件下载进度:${progress}% \n\n已下载:%${downloadedLengthKb}KB | 总长:${totalLengthKb}KB"
}
}
override fun onFailed(errMsg: String?) {
Timber.e("Download Failed: $errMsg")
}
}).build()
PS : 这里要注意,FlexNet会在业务方调用下载的线程返回下载回调,所以绝大部分时候回调是发生在子线程,此时如果有线程敏感的功能(比如刷新UI),需要自行处理线程切换。
3 触发下载
通过Builder.build()
创建 Downloader 下载器,最后调用Downloader.download()
方法即可开始下载。
和Http Request一样,download()
是一个suspend方法,需要在协程中使用:
lifecycleScope.launch(Dispatchers.IO) {
downloader.download()
}
整体架构
设置配置项
1. 设置双向认证开关
在初始化的时候控制双向认证开关:
fun init(context: Application, needMutualAuth: Boolean = true)
方法内部会根据开关值来切换不同的后端服务器,但是有些App不能过早的获取证书,这样会有双向认证失败的风险,FlexNet同时支持懒汉式的主动双向认证
2. 主动双向认证接口
在确定拿到证书,或者确定可以双向认证的时机,可随时发起双向认证请求:
MutualAuthenticate.suspendBuildSSL()
可通过
MutualAuthenticate.isSSLReady()
接口来检查当前双向认证是否成功。
主动触发示例代码如下:
MutualAuthenticate.suspendBuildSSL {
if (it) {
Toast.makeText(context, "双向认证成功,可以开始访问加密资源", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "双向认证失败", Toast.LENGTH_SHORT).show()
}
}
3. 数据缓存
在前面发起请求调用httpRequest
顶层函数的时候,可以传入一个可选参数cacheKey
,这个key不为空则网络库会在本地保存当前请求的返回数据。Key作为缓存的唯一标识,在无网络或请求失败的时候,会通知调用方错误,并返回缓存的数据。
缓存部分流程如下:
4. 错误及异常处理
在发起请求的顶层函数 httpRequest
中,有两个参数用来提供给调用方处理错误和异常。
首先区分一下错误和异常:
错误通常是发起了网络请求,且网络请求有响应,只是由于接口地址或者参数等等原因导致服务端解析失败,最终返回错误码及错误信息。
而异常是指在发起网络请求的过程中出现了 Exception,导致整个网络请求流程被中断,所以当异常发生的时候,网络库是不会返回错误码和错误信息的,只能返回异常信息供调用方定位问题。
回调的使用方式很简单,只需要在httpRequest
中传入两个回调:fail
和error
,下面分别看看二者的处理方式:
1. 错误处理
fai的定义如下:
fail: (response: ResponseEntity) -> Unit = {
onFail(it)
}
传入的回调有一个 ResponseEntity 参数,这是网络请求返回的响应实体,内部包含errorCode
和errorMessage
,不传则默认打印这两个字段,可以在 Logcat 中通过Tag:Http Request
**过滤出来。
2. 异常处理
error的定义如下:
error: (e: Exception) -> Unit = {
onError(it)
} ,
回调函数只有一个 Exeption 对象,和前面的定义相符,在异常的时候将异常返回供调用方定位问题。不传网络库默认打印异常,可以在 Logcat 中通过Tag:Http Request
**过滤出来。
扩展接口:发起请求并处理返回结果
网络库定义了一个顶层函数用来发起请求并接收返回结果或处理异常:
fun
- block: 实际请求体,必填。可以传入步骤 4 中实现的接口
- fail: 请求错误回调,非必填。用来处理服务端返回的请求错误,会携带错误码及错误信息
- error: 请求异常回调,非必填。用来处理请求中发生的异常,此时没有response返回
- cacheKey: 数据缓存唯一标识,非必填
httpRequest 中的泛型 T 就是接入步骤2定义的 Response 实体,正常返回会在方法内部自动解析出 UserNameRsp
,到此就完成了一次网络请求。
以上是基本的使用方式,涵盖了安全、数据请求、缓存、异常处理等功能,可以适应于多种项目场景。应大家的建议,后续会完善几篇文章拆解具体的原理及开发思路,从源码的角度教你如何从0开发一套完善的网络库
大家如果想了解设计思路及框架原理,可以参考:源码及涉及思路参考:如何开发一款安全高效的Android网络库(详细教程)
需要体验的同学可以在评论区留下联系方式,我给你发送aar以及源码。有问题欢迎随时探讨
来源:juejin.cn/post/7379521155286941708
sleep 和 wait深度对比!
在计算机编程中,特别是在多线程或并发编程中,sleep
和 wait
是两个非常常见的函数,但它们有不同的用途和工作机制,这篇文章我们将详细地讨论 sleep
和 wait
的区别,包括它们的内部工作原理、应用场景以及详细的示例代码,以帮助更全面地理解它们。
sleep
工作机制
- 暂停当前线程:
sleep
方法暂停当前执行的线程一段指定的时间,时间结束后线程再恢复执行。 - 不会释放锁: 即使线程在
sleep
状态下持有锁,它也不会释放。它依然占用着该锁,其他线程无法获得该锁。 - 线程状态转换:
sleep
方法会使线程从运行(RUNNING)状态转换为计时等待(TIMED_WAITING)状态。 - 静态方法: 它是
Thread
类的静态方法,调用时通过Thread.sleep
访问。
应用场景
- 限流: 控制任务执行的频率,防止线程过度占用CPU资源。
- 定时任务: 在某个循环中,定时执行某些任务。
示例代码
public class SleepExample extends Thread {
public void run() {
try {
System.out.println("Thread going to sleep for 2 seconds.");
Thread.sleep(2000); // 睡眠 2 秒
System.out.println("Thread woke up after sleeping.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SleepExample thread = new SleepExample();
thread.start();
}
}
wait
工作机制
- 释放锁并等待通知:
wait
方法使当前线程等待,直到其他线程调用当前对象的notify
或notifyAll
方法。调用wait
时,线程会释放它持有的锁。 - 必须在同步块或同步方法中使用:
wait
方法必须在同步块或同步方法中调用,否则会抛出IllegalMonitorStateException
。 - 线程状态转换:
wait
方法会使线程从运行(RUNNING)状态转换为等待(WAITING)状态。 - 对象方法: 它是
Object
类的方法,所以任何对象都可以调用。
应用场景
- 线程间通信: 多个线程协同工作时,一个线程等待某个条件满足后,再被其他线程通知继续执行。
- 生产者-消费者模型: 经常用于实现生产者-消费者模式中的同步。
示例代码
public class WaitNotifyExample {
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
// 等待线程
Thread waitingThread = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("Thread waiting for the lock to be released.");
lock.wait(); // 进入等待状态并释放锁
System.out.println("Thread resumed after lock released.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 通知线程
Thread notifyingThread = new Thread(() -> {
synchronized (lock) {
System.out.println("Notifying other threads.");
lock.notify(); // 通知其他等待该锁的线程
System.out.println("Notified waiting thread.");
}
});
waitingThread.start();
Thread.sleep(1000); // 确保 waitingThread 先持有锁并进入等待状态
notifyingThread.start();
}
}
sleep 和 wait的对比
特性 | sleep | wait |
---|---|---|
释放锁 | 否 | 是 |
需要在同步块或方法中 | 否 | 是 |
属于 | Thread 类 | Object 类 |
引发异常 | InterruptedException | InterruptedException 引发机制相同 |
作用范围 | 当前调用的线程 | 当前拥有锁的线程 |
线程状态改变 | 变为计时等待(TIMED_WAITING) | 变为等待(WAITING) |
典型应用场景 | 暂停线程的一段时间,用于控制节奏或定时操作 | 线程间通信,生产者-消费者模型等 |
总结
本文,我们分析了sleep
和 wait
,sleep
用于暂停当前线程一段指定时间,但仍保持锁,这常用来控制执行节奏或定时操作。wait
使线程释放锁并进入等待状态,直到通过 notify/notifyAll 被唤醒,需在同步块中使用,适用于线程间通信如生产者-消费者模型。
来源:juejin.cn/post/7420718386953355279
springboot + minio + kkfile实现文件预览(不暴露minio地址)
前言
之前我写过一片文章【springboot + minio + kkfile实现文件预览】,该文章介绍了如何使用kkfile预览文件,但是文章中介绍的方案,会暴露minio的地址,实际的预览地址如下:
http://kkfile-server/onlinePreview?url=base64UrlEncode(minio生成的文件预览地址)
但是大多数情况下,minio服务的地址是不允许暴露的,所有我们对其进行优化,依然使用kkfile预览文件,但是我们使用文件流的方式,并且在下载接口上校验用户认证的有效性,在保证不暴露minio地址的前提下,还加入了token认证,提高了安全性,话不多说,直接上代码。
一、文件上传
上传服务
public void uploadFile(MultipartFile file) throws Exception {
String fileName = System.currentTimeMillis() + "-" + file.getOriginalFilename();
PutObjectArgs args = PutObjectArgs.builder().bucket(minioConfig.getBucketName()).object(fileName).stream(file.getInputStream(), file.getSize(), -1).contentType(file.getContentType()).build();
client.putObject(args);
}
封装接口
@PostMapping("upload")
public RestResult upload(MultipartFile file) {
try {
sysFileService.uploadFile(file);
} catch (Exception e) {
log.error("上传文件失败", e);
return RestResult.fail(e.getMessage());
}
}
二、文件下载
下载服务
public void download(String filename, HttpServletResponse response) throws ServiceException {
try {
InputStream inputStream = client.getObject(GetObjectArgs.builder().bucket(minioConfig.getBucketName()).object(filename).build());
// 设置响应头信息,告诉前端浏览器下载文件
response.setContentType("application/octet-stream;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
// 获取输出流进行写入数据
OutputStream outputStream = response.getOutputStream();
// 将输入流复制到输出流
byte[] buffer = new byte[4096];
int bytesRead = -1;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
// 关闭流资源
inputStream.close();
outputStream.close();
} catch (Exception e) {
log.error("文件下载失败:" + e.getMessage());
throw new ServiceException("文件下载失败");
}
}
封装接口
@ApiOperation("文件下载")
@GetMapping("/download/{token}/{filename}")
public void getDownload(@PathVariable("token") String token, @PathVariable("filename") String filename, HttpServletResponse response) {
tokenUtils.validateToken(token);
sysFileService.download(filename, response);
}
上面的接口有两个地方需要注意
- @GetMapping("/download/{token}/{filename}")中filename参数必须放在最后
- tokenUtils.validateToken(token);
该接口要在拦截器中放行,验证token在代码逻辑中,这里根据项目中实际场景去实现。该地址为kkfile请求获取文件流的地址,所以需要放开鉴权
三、文件预览地址获取
文件预览地址生成服务(该服务只是获取token并拼接到文件下载地址中,不对token做验证,因为该服务的接口在请求进入前要做校验)
public String getPreviewUrl(String filename) throws UnsupportedEncodingException {
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = sra.getRequest();
if (request ==null || StringUtils.isBlank(request.getHeader(TokenConstants.AUTHENTICATION))) {
throw new ServiceException("未获取到有效token");
}
String previewUrl = filePreviewUrl + FileUploadUtils.base64UrlEncode(fileDownloadUrl + "/" + token + "/" + filename);
return previewUrl + "&fullfilename=" + URLEncoder.encode(filename, "UTF-8");
}
FileUploadUtils中的base64UrlEncode方法
public static String base64UrlEncode(String url) throws UnsupportedEncodingException {
String base64Url = Base64.getEncoder().encodeToString(url.getBytes(StandardCharsets.UTF_8));
return URLEncoder.encode(base64Url, "UTF-8");
}
封装接口,获取文件预览地址
@GetMapping("/getPreviewUrl")
public RestResult<String> getPreviewUrl(String filename) throws UnsupportedEncodingException {
return RestResult.ok(sysFileService.getPreviewUrl(filename));
}
测试
假设
- 文件服务地址为:http://file-server
- kkfile服务地址为:http://kkfile-server
- 文件名称为:xxxx.docx
最后生成的文件预览地址为:
http://kkfile-server/onlinePreview?url=aHR0cDovLzE3Mi4xNi41MC4y....&fullfilename=xxxx.docx
其中aHR0cDovLzE3Mi4xNi41MC4y....为:
FileUploadUtils.base64UrlEncode("http://file-server" + "/" + token + "/" + filename);
截图为证
来源:juejin.cn/post/7424338056918761498
Java已死,大模型才是未来?
引言
在数字技术的浪潮中,编程语言始终扮演着至关重要的角色。Java,自1995年诞生以来,便以其跨平台的特性和丰富的生态系统,成为了全球范围内开发者们最为青睐的编程语言之一
然而,随着技术的不断进步和新兴语言的崛起,近年来,“Java已死”的论调开始不绝于耳。尤其是在大模型技术迅猛发展的今天,Java的地位似乎更加岌岌可危。然而,事实真的如此吗?Java的春天,真的已经渐行渐远了吗?本文将从多个维度深入探讨Java的现状、大模型技术的影响,以及Java与大模型融合的可能性,为读者提供一个更为全面和深入的视角。
Java的辉煌历史与稳健地位
Java,作为Sun Microsystems在1995年推出的编程语言,一经问世便凭借其独特的跨平台特性和丰富的生态系统,迅速在全球范围内赢得了广泛的认可和应用。从最初的Java Applet,到后来的Java Web开发、Java EE企业级应用,再到如今的Android应用开发、大数据处理等领域,Java都展现出了其强大的生命力和广泛的应用前景。
在最新的TIOBE编程语言排行榜上,Java长期位居前列,这足以证明其在开发界的重要地位。而在中国这个拥有庞大IT市场的国家中,Java更是受到了广泛的关注和追捧。无论是大型企业还是初创公司,Java都成为了其首选的开发语言之一。这背后,是Java的跨平台特性、丰富的库和框架、强大的社区支持等多方面的优势所共同铸就的。
然而,随着技术的不断进步和新兴语言的崛起,Java也面临着一些挑战和质疑。
一些人认为,Java的语法过于繁琐、性能不够优越、新兴语言如Python、Go等更加轻便灵活。这些观点在一定程度上反映了Java在某些方面的不足和局限性。
但是,我们也不能忽视Java在企业级应用、Web开发、大数据处理等领域的深厚积累和广泛应用。这些领域对Java的稳定性和可靠性有着极高的要求,而Java正是凭借其在这方面的优势,赢得了众多企业和开发者的青睐。
大模型技术的崛起与影响
近年来,随着人工智能和机器学习技术的飞速发展,大模型技术逐渐成为了人工智能领域的一大热点,可谓是百家争鸣。大模型技术通过构建庞大的神经网络模型,实现对海量数据的深度学习和处理,从而在各种应用场景中取得了令人瞩目的成果。
在自然语言处理领域,大模型技术通过训练庞大的语言模型,实现了对自然语言的深入理解和生成。这使得机器能够更加智能地处理人类的语言信息,从而实现更加自然和流畅的人机交互。在图像处理领域,大模型技术也展现出了强大的能力。通过训练庞大的卷积神经网络模型,机器能够实现对图像的精准识别和分析,从而在各种应用场景中发挥出巨大的作用。
大模型技术的崛起对软件开发产生了深远的影响。
首先,大模型技术为开发者提供了更加高级别的抽象和智能化解决方案。这使得开发者能够更加专注于核心业务逻辑的实现,而无需过多关注底层技术的细节。其次,大模型技术降低了AI应用的开发门槛。传统的AI应用开发需要深厚的数学和编程基础,而大模型技术则通过提供易于使用的工具和框架,使得开发者能够更加方便地构建和部署AI应用。最后,大模型技术推动了软件开发的智能化升级。从需求分析、设计到开发、测试和维护等各个环节都在经历着智能化的变革,这使得软件开发过程更加高效和智能。
Java与大模型的融合与变革
在大模型技术崛起的背景下,Java作为一种成熟且广泛应用的编程语言,自然也在探索与大模型技术的融合之路。事实上,Java与大模型的融合已经取得了不少进展和成果。
首先,Java社区对于大模型技术的支持和探索已经初见成效。一些开源项目和框架在Java环境中实现了深度学习和大模型技术的支持,如Deeplearning4j、ND4J等。这些项目和框架为Java开发者提供了丰富的工具和资源,使得他们能够更加方便地构建和部署基于大模型的应用。
其次,Java自身的特性和优势也为其与大模型的融合提供了有力的支持。Java作为一种面向对象的语言,具有强大的抽象能力和封装性,这使得它能够更好地处理大模型中的复杂数据结构和算法。同时,Java的跨平台特性也使得基于Java的大模型应用能够在不同的操作系统和硬件平台上运行,从而提高了应用的兼容性和可移植性。
最后,Java与大模型的融合也推动了软件开发的智能化升级。在需求分析阶段,大模型技术可以通过对海量数据的学习和分析,帮助开发者更加准确地把握用户需求和市场趋势。在设计阶段,大模型技术可以通过对已有设计的分析和优化,提高设计的合理性和效率。在开发阶段,大模型技术可以为开发者提供智能化的编程辅助和错误检查功能,从而提高开发效率和代码质量。在测试和维护阶段,大模型技术可以通过对应用的持续监控和分析,及时发现和修复潜在的问题和缺陷。
未来趋势与展望
随着AI和机器学习技术的不断发展,大模型技术将在未来继续发挥重要的作用。而Java作为一种成熟且广泛应用的编程语言,也将继续在大模型时代发挥其独特的优势和作用。
首先,Java将继续优化其性能和语法,提高开发者的开发效率和代码质量。同时,Java还将加强对大模型技术的支持和整合,为开发者提供更加全面和强大的工具和框架。
其次,Java将与更多新兴技术进行融合和创新。例如,随着云计算和边缘计算的兴起,Java将加强与这些技术的融合,推动云计算和边缘计算应用的发展。此外,Java还将与物联网、区块链等新兴技术进行深度融合,开拓新的应用领域和市场空间。
最后,Java将继续发挥其在企业级应用、Web开发、大数据处理等领域的优势,为各行各业提供更加稳定、可靠、安全的解决方案。同时,Java也将积极拥抱开源文化和社区文化,与全球开发者共同推动Java生态系统的繁荣和发展。
总之,Java作为一种历久弥新的编程语言巨头,将在大模型时代继续发挥其独特的优势和作用。通过与大模型技术的深度融合与创新,Java将引领编程世界的潮流,为各行各业带来更加智能化和自动化的解决方案。让我们共同期待Java在未来的辉煌!
写在最后
我不禁要感慨Java这一编程语言的深厚底蕴和持久魅力。它不仅是一段技术史,更是无数开发者智慧与汗水的结晶。在大模型时代,Java也会以其独特的稳定性和可靠性,持续为各行各业提供着坚实的支撑。正如历史的河流永不停息,Java也在不断地进化与创新,与新兴技术深度融合,共同推动着科技发展的浪潮。让我们携手前行,继续书写Java的辉煌篇章,为构建更加智能、更加美好的未来贡献力量。
来源:juejin.cn/post/7419967609451675700
Android串口开发入门
最近的开发项目有涉及到Android串口开发,所以比较好奇安卓项目中是如何读取串口数据的,官方给了一个代码示例,但是发现官方的示例比较久了,没有使用CMake,下载后运行老是报错,踩坑,最后使用CMake的方式终于运行成功,在此记录一下,源码:gitee.com/hluck/hello…
目录结构
1.创建一个HelloWord项目
2.引入jni和so库
将jni文件夹和jniLibs文件夹复制到main目录下:
3.修改gradle
由于此时Android studio编译时,不会去编译加载CMakeLists.txt,所以要告诉他在哪加载:
android {
...
externalNativeBuild {
cmake {
path "src/main/jni/CMakeLists.txt" // 指定 CMakeLists.txt 文件路径
// 其他 CMake 选项
}
}
}
4.加载动态库,编译native方法
官方示例中有两个类是关于打开和关闭串口api的:
1.SeriaPort
其中加载动态库,打开和关闭串口的native方法在SerialPort类中:
这两个native方法对应的是jni文件下的SerialPort.c文件中,如果你的SerialPort类所在包名和我的不一样,记得修改一下这个文件,值得一提的是,open方法中的第一个参数是串口地址,第二个参数是波特率,第三个参数是打开串口时的操作模式,0表示默认,当调用读写操作时,如果串口没有准备好数据,程序会阻塞等待,直到有数据可以读取或写入。
2.FileDescriptor
上面的open方法会返回一个FileDescriptor实例,通过这个实例获取写入和读取串口数据的流。
5.读取或写入串口数据
在Application类中保存一个SerialPort实例,这样就能通过获取SerialPort实例来读写串口数据了。
参考文章
来源:juejin.cn/post/7381347654743326746
作为前端开发,这些年跟设计师的斗智斗勇
我无意中在知乎上看到这样一个话题, 让我不得不有感而发。
因为曾经的我也是被设计师给虐惨了。
我是工作了 8 年的老前端了, 如果算上实习那就有 9 年了。
我做过 C 端应用, 做过 B 端应用, 做过 SaaS 应用, 我经历的所有设计师, 都不接受 0.5px 的像素偏差。 可以说是对像素偏差 0 容忍。
所以想作为前端工程师, 来来给大家聊聊我的日常工作中是怎么跟设计师斗智斗勇的。
1. 给设计师设置门槛
这个设置门槛意思很简单, 你不能拿着电脑, 指着我屏幕就说, 这这这不行, 那那那不行的。
你得走系统工单, 一个样式一个工单。 得注明, 哪儿样式不对齐, 差多少像素, 预期重新验收的时间, 走查报告, 样式走查进度等等。如果没有系统工单的流程, 搞一个复杂的文档丢给设计同学也行, 让设计师同学把每一个问题都尽可能的详细记录下来。
还要明确设计走查验收时间, 定稿的设计稿件, 非特殊原因不允许修改等方式, 增加设计师自身的成本。
和设计师合作完成一个项目, 完成之后大家都分蛋糕, 自己改样式是有成本投入, 设计师别人是零成本投入, 哪儿那行? 所以这个就是门槛的来源。
2. 告诉设计师我修改某一些样式的成本, 这个成本超过了预期, 需要设计师额外承担成本付出
举一个简单的例子哈。 设计是要求做 移动端 和 PC 端兼容, 我预估做移动端和PC 端兼容, 需要 5 天时间。 已经拍板定下来了。 做到了一半, 设计说, 我的移动端, 要适配 小屏幕手机和大屏幕手机以及 平板 拥有独立的样式展现。 这个成本是预估之外的额外成本, 可能需要多加 5 天时间。 那么这个 5 天时间, 是需要设计师去向项目经理申请的, 项目经理如果同意, 多给我加五天我就做。(其实相当于转移矛盾)
3. 给出设计师无法拒绝的理由
就说到了静态和动态的问题了。 比如设计师给了一个版本的设计, 是没有数据情况下的设计, 但是前端数据加载出来了, 渲染出来的结果, 跟设计师预期的不一样。 而且设计师自己也没有给设计稿。 这个就直接专业碾压就行了, 黑话直接就来,比如:我的架构能力已经做好了, 实在是改不了了, 否则的推翻了重新做; 你这个 1 PX 的像素偏差, 真的有必要吗,你如何论证你这部分必要性;它是一个共性问题,以前的需求都是这样子的, 如果你这次改了,那之前的那些样式也都改吗,否则是不一致的;..........
这个方向, 就是主要针对一些可有可无的样式调整。如果修改成本较大, 而且设计稿本身就模棱两可的, 就可以使用专业知识碾压。
4. 设计稿变动要周知
其实很多时候在开发过程中是, 会出现一个情况, 设计师改了设计稿(在没有跟前端同学商量的情况下改了设计稿)。这个我相信是很多前端同学最头疼的事情之一。
我之前合作过一个设计师, 很喜欢临时改设计稿,但是改了又不周知,让我跟着后面反复改, 最后项目导致了一定时间的延期。 最后项目复盘的时候, 我就直接跳出来喷这个情况,有理有据,让 leader 们去核算这部分成本了。
其实一定要达成一个一致意见, 甚至可以在做项目之前就商量好, 如果遇到设计稿变更, 导致开发工作要返工的情况, 总计返工超过 0.5 日的情况, 要提出需求变更,不通过需求变更的设计稿变更, 一律不接受。
5. 提升自己专业能力
最后这个没啥好说的, 吃这个饭, 就的接受这个设定;尽量想办法还原设计师的设计稿即可。
曾经我也常被设计师折磨得体无完肤, 甚至想过要转行算了。 想想, 后端同学还不是一样被数据、稳定性、服务器运维等问题折磨得体无完肤。 各行都有各行的难处, 吃这个饭就得接受这个设定。
提升自己专业能力, 只会有利无害, 就比如我现在也能算是半个像素眼。
来源:juejin.cn/post/7429981053039312934
回顾我转前端的这一年。。
✊不积跬步,无以至千里;不积小流,无以成江海
从去年年尾意外发生,突然“找工作”,23年10月24号注册掘金写下第一篇博客,到今天刚好一年。这一年经历了写毕业论文、从0做项目找实习,答辩,刷算法,准备秋招,最终拿到四个大厂offer。也算是天糊开局,完美收官。
说实话最开始并不是以进入大厂作为target来规划今年的秋招的,因此也还算走了一些“弯路”。但也许offer真的和运气是相关的,索性殊途同归了吧。当然9本给我了折腾的底气,但我相信就算是非92,一年时间(其实是用不了的)好好规划,是一定能够在秋招季收获一个好的结果的。
回顾这一年,做的最正确的决定之一,就是坚持写博客。虽然也是写到差不多第十个月才感受到什么是“写博客输出”,但没有前面十个月的坚持,也不会有后几个月的蜕变。所以!没有一点努力是白费的!就像我每一篇文章的quota:’✊不积跬步,无以至千里;不积小流,无以成江海‘。种下一棵树最好的时间是十年前,其次是现在。所以看到这篇文章的你,不妨从现在开始尝试写下自己的第一个博客文章~~
这一年来,除去整理其他公司的前端笔试题这种博客,应该写了差不多70篇左右的博客。先介绍一下博客,也大概是我的学习过程的缩影。
前三十篇左右的博客更像是知识点的罗列复习。其实我并不是第一次接触前端,但曾经系统学过的知识点已经不够熟悉了,我坚信好记性不如烂笔头,所以连查带写带输出,完成了前端知识点的第一轮复习。
之后十篇左右的博客记录了三个月跟着网上视频学项目的时候遇到的问题,比如场景,或者问题解法等等。一方面是觉得这些点比较有趣,另一方面也是为了面试的时候和面试官沟通的话不用干巴巴讲,有博客+动图能够尽量完整的将信息传递。(毕竟考虑别人的【用户体验】也是前端工程师要在意的事)
再然后到了找实习之前,为了面试写了一系列的面试复习笔记,大概十多篇。都是实习时候会被问到的各种前端知识点,应该是比较全和具有代表性的。这个时候开始试图自己归纳、总结、整理输出成自己的想法。“自己的感悟”开始有一点雏形。也是这个时候明显觉得自己的前端知识上了一个台阶,能够理解知识点之间的关系并可以串联,能初步构建自己的前端知识网络。
之后就是实习的感悟。其实实习的时候因为太忙(实习任务、新城市适应、生活变动、学校杂事、回学校答辩),并没有写什么博客。反而是要准备离开实习公司,开始罗列自己的收获的时候,把自己日常的工作记录摘抄一些“精品”而有趣的点,写了几篇博客。也是这个时候我自我反思觉得不坚持写博客真的时间会“溜走”,还是记下来更有安全感,坚定了要坚持写下去的信念。
后面就是到了秋招,其实这个阶段大框架上的知识点已经很少有哪里需要查漏补缺了。但和一些有经验的大佬沟通,觉得似乎“沉淀和思考”还可以做的更好。所以花了相当一部分精力修正或者说串联之前的博客,以及酝酿了几篇非常好的博客。一篇博客光写就要写两天的那种。虽然数据一般,但我相信是金子总会发光哈哈。基本这个阶段走完,自己前端知识点网络已经构建的比较全面了,很多问题都已经有了自己的见解,不止仅仅是那种看几个博客看几个视频的见解,而是真的自己也知道怎么回事,什么原理,要怎么样去用这种。
综上所述,自己也完成了从罗列博客,到写博客,再到输出博客的蜕变。还是对自己的这一年很满意的。
对于看到这篇文章的朋友,并且想自己找工作能有好结果的,我能给出的一些建议:
- 坚持做一件事,写博客只是其中一件。每个工作日github push代码也可以是一件事。量变到质变的积累,要先有量才可以,没有人是天才。
- 实习经历(有效,不是混日子那种)大于项目经历。实际工作场景中能够获得的成长是个人项目没法获得的。
- 如果因为某些原因没办法有实习,那么只能通过疯狂背“八股”来展示自己的’优秀‘基础。但别气馁,没实习的人很多的。
- leetcode代码是不能逃掉的一个待办项。比起我那时极度痛苦疯狂恶补,不如给自己三个月,每天都刷一点点。leetcode对每个人都很公平,只能靠量,几乎不可能速成。
- 如果从大一就开始为前端做准备,那不同大厂间的不同业务线实习是最优解。这能帮助你更好的了解不同大厂/不同业务的风格,能帮助你找到自己想要的到底是什么。当然,如果你找到的第一个就发现是自己所爱,那恭喜你,持续在这里实习吧!
- 如果再给我一个月,也许我会静下心来好好研究一下react源码/fiber架构这类的。尽管关于这些网上有长视频讲的很好,但想真的有自己的理解,唯有自己沉下心来好好研究,所有视频资料都不过是辅助工具。当然,更好的理解方式是在工作中慢慢感悟。
- 找工作是一场马拉松,没到最后,万物皆有可能,心态要稳。我一周收获了三个offer,我相信如果我肯多约几个面试,也许能创造更多的不可能。
很高兴你能看到最后,回首这一年,虽然很苦,但是真的很开心。希望你回顾自己的一年,也会觉得很开心~
有些什么问题也可以在评论区留言,打破零回复哈哈
来源:juejin.cn/post/7429321661491462155
WebGL实现soul星球效果
WebGL实现soul星球效果
最近在研究webGL,觉得soul app的星球挺有意思的,于是就实现了一下,中间涉及的细节和知识点挺多的,写篇博客分享一下
soul原版

WebGL实现的

主要技术要点
1.使用黄金分割数螺旋分配
使小球在球表面均匀分布
使用不同的goldenRatio可以得到非常多分布效果,采用黄金分割数在视觉上最匀称、舒服
const goldenRatio = (1 + Math.sqrt(5)) / 2
const y = 1 - (i / (numPoints - 1)) * 2
const radiusAtY = Math.sqrt(1 - y * y)
const theta = (2 * Math.PI * i) / goldenRatio
const x = Math.cos(theta) * radiusAtY
const z = Math.sin(theta) * radiusAtY
2.自由转动
因为要解决万向锁的问题,所以不能使用rotateX
、rotateY
、rotateZ
来旋转,应当使用四元数THREE.Quaternion
3.背面小球变暗
这里通过内部放置了一个半透明的黑色小球来实现
// 创建半透明球体
const sphereGeometry = new THREE.SphereGeometry(4.85, 16, 16)
为了使小球从正面转动的背面的过程中可以平滑的变暗,这里还需要把半透明小球的边沿处理成高斯模糊
,具体实现就是使用GLSL的插值函数smoothstep
fragmentShader: `
uniform vec3 color;
uniform float opacity;
varying vec3 vNormal;
void main() {
float alpha = opacity * smoothstep(0.5, 1.0, vNormal.z);
gl_FragColor = vec4(color, alpha);
}
但是需要注意的是需要关闭小球的深度测试
,否则会遮挡小球
side: THREE.FrontSide,
depthWrite: false,
4.使用THREE.Sprite
创建小球标签
5.标签位置计算
for (let i = 0; i < numPoints; i++) {
const y = 1 - (i / (numPoints - 1)) * 2
const radiusAtY = Math.sqrt(1 - y * y)
const theta = (2 * Math.PI * i) / goldenRatio
const x = Math.cos(theta) * radiusAtY
const z = Math.sin(theta) * radiusAtY
const smallBallMaterial = new THREE.MeshBasicMaterial({
color: getRandomBrightColor(),
depthWrite: true,
depthTest: true,
side: THREE.FrontSide,
})
const smallBall = new THREE.Mesh(smallBallGeometry, smallBallMaterial)
smallBall.position.set(x * radius, y * radius, z * radius)
6.超出长度的标签采用贴图采样位移
来实现跑马灯效果
7.滚动阻尼,鼠标转动球体之后速度能衰减到转动旋转的速率
8.自动旋转需要保持上一次滚动的方向
9.使用射线拾取
来实现点击交互
完整代码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>3D 半透明球体与可交互小球</title>
<style>
body {
margin: 0;
background-color: black;
touch-action: none;
}
canvas {
display: block;
}
</style>
</head>
<body>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.169.0/build/three.module.js'
// 创建场景
const scene = new THREE.Scene()
// 创建相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
)
camera.position.set(0, 0, 14)
camera.lookAt(0, 0, 0)
// 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setClearColor(0x000000, 0)
document.body.appendChild(renderer.domElement)
// 创建半透明球体
const sphereGeometry = new THREE.SphereGeometry(4.85, 16, 16)
const sphereMaterial = new THREE.ShaderMaterial({
uniforms: {
color: { value: new THREE.Color(0x000000) },
opacity: { value: 0.8 },
},
vertexShader: `
varying vec3 vNormal;
void main() {
vNormal = normalize(normalMatrix * normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
}
`,
fragmentShader: `
uniform vec3 color;
uniform float opacity;
varying vec3 vNormal;
void main() {
float alpha = opacity * smoothstep(0.5, 1.0, vNormal.z);
gl_FragColor = vec4(color, alpha);
}
`,
transparent: true,
side: THREE.FrontSide,
depthWrite: false,
})
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial)
scene.add(sphere)
// 创建小球体和标签数组
const smallBallGeometry = new THREE.SphereGeometry(0.15, 16, 16)
const smallBalls = []
const labelSprites = []
const radius = 5
const numPoints = 88
const goldenRatio = (1 + Math.sqrt(5)) / 2
const maxWidth = 160
const textSpeed = 0.002
// 创建射线投射器
const raycaster = new THREE.Raycaster()
const mouse = new THREE.Vector2()
function createTextTexture(text, parameters = {}) {
const {
fontSize = 24,
fontFace = 'PingFang SC, Microsoft YaHei, Noto Sans, Arial, sans-serif',
textColor = 'white',
backgroundColor = 'rgba(0,0,0,0)',
maxWidth = 160,
} = parameters
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
context.font = `${fontSize}px ${fontFace}`
const textMetrics = context.measureText(text)
const textWidth = Math.ceil(textMetrics.width)
const textHeight = fontSize * 1.2
const needMarquee = textWidth > maxWidth
let canvasWidth = maxWidth
if (needMarquee) {
canvasWidth = textWidth + 60
}
canvas.width = canvasWidth
canvas.height = textHeight
context.font = `${fontSize}px ${fontFace}`
context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = backgroundColor
context.fillRect(0, 0, canvas.width, canvas.height)
context.fillStyle = textColor
context.textAlign = needMarquee ? 'left' : 'center'
context.textBaseline = 'middle'
if (needMarquee) {
context.fillText(text, 0, canvas.height / 2)
} else {
context.fillText(text, maxWidth / 2, canvas.height / 2)
}
const texture = new THREE.CanvasTexture(canvas)
texture.needsUpdate = true
if (needMarquee) {
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.ClampToEdgeWrapping
texture.repeat.x = maxWidth / canvas.width
} else {
texture.wrapS = THREE.ClampToEdgeWrapping
texture.wrapT = THREE.ClampToEdgeWrapping
}
texture.minFilter = THREE.LinearFilter
texture.magFilter = THREE.LinearFilter
texture.generateMipmaps = false
return { texture, needMarquee, HWRate: textHeight / maxWidth }
}
for (let i = 0; i < numPoints; i++) {
const y = 1 - (i / (numPoints - 1)) * 2
const radiusAtY = Math.sqrt(1 - y * y)
const theta = (2 * Math.PI * i) / goldenRatio
const x = Math.cos(theta) * radiusAtY
const z = Math.sin(theta) * radiusAtY
const smallBallMaterial = new THREE.MeshBasicMaterial({
color: getRandomBrightColor(),
depthWrite: true,
depthTest: true,
side: THREE.FrontSide,
})
const smallBall = new THREE.Mesh(smallBallGeometry, smallBallMaterial)
smallBall.position.set(x * radius, y * radius, z * radius)
sphere.add(smallBall)
smallBalls.push(smallBall)
const labelText = getRandomNickname()
const { texture, needMarquee, HWRate } = createTextTexture(labelText, {
fontSize: 28,
fontFace: 'PingFang SC, Microsoft YaHei, Noto Sans, Arial, sans-serif',
textColor: '#bbbbbb',
maxWidth: maxWidth,
})
const spriteMaterial = new THREE.SpriteMaterial({
map: texture,
transparent: true,
depthWrite: true,
depthTest: true,
blending: THREE.NormalBlending,
})
const sprite = new THREE.Sprite(spriteMaterial)
sprite.scale.set(1, HWRate, 1)
labelSprites.push({ sprite, smallBall, texture, needMarquee, labelText })
scene.add(sprite)
}
// 添加灯光
const light = new THREE.AmbientLight(0xffffff, 0.5)
scene.add(light)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1)
directionalLight.position.set(5, 5, 5)
scene.add(directionalLight)
// 定义自动旋转速度和轴
const autoRotationSpeed = 0.0005
let autoRotationAxis = new THREE.Vector3(0, 1, 0).normalize()
let currentAngularVelocity = autoRotationAxis.clone().multiplyScalar(autoRotationSpeed)
let isDragging = false
let previousMousePosition = { x: 0, y: 0 }
let lastDragDelta = { x: 0, y: 0 }
const decayRate = 0.92
const increaseRate = 1.02
// 鼠标事件处理
const onMouseDown = (event) => {
isDragging = true
previousMousePosition = {
x: event.clientX,
y: event.clientY,
}
}
const onMouseMove = (event) => {
if (isDragging) {
const deltaX = event.clientX - previousMousePosition.x
const deltaY = event.clientY - previousMousePosition.y
lastDragDelta = { x: deltaX, y: deltaY }
const rotationFactor = 0.005
const angleY = deltaX * rotationFactor
const angleX = deltaY * rotationFactor
const quaternionY = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 1, 0),
angleY
)
const quaternionX = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(1, 0, 0),
angleX
)
const deltaQuat = new THREE.Quaternion().multiplyQuaternions(quaternionY, quaternionX)
sphere.quaternion.multiplyQuaternions(deltaQuat, sphere.quaternion)
const dragRotationAxis = new THREE.Vector3(deltaY, deltaX, 0).normalize()
const dragRotationSpeed = Math.sqrt(deltaX * deltaX + deltaY * deltaY) * rotationFactor
if (dragRotationAxis.length() > 0) {
currentAngularVelocity.copy(dragRotationAxis).multiplyScalar(dragRotationSpeed)
}
previousMousePosition = {
x: event.clientX,
y: event.clientY,
}
}
}
const onMouseUp = () => {
if (isDragging) {
isDragging = false
const deltaX = lastDragDelta.x
const deltaY = lastDragDelta.y
if (deltaX !== 0 || deltaY !== 0) {
const newAxis = new THREE.Vector3(deltaY, deltaX, 0).normalize()
if (newAxis.length() > 0) {
autoRotationAxis.copy(newAxis)
}
const dragSpeed = currentAngularVelocity.length()
if (dragSpeed > autoRotationSpeed) {
// 维持当前旋转速度
} else {
currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
}
}
}
}
// 触摸事件处理
const onTouchStart = (event) => {
isDragging = true
const touch = event.touches[0]
previousMousePosition = {
x: touch.clientX,
y: touch.clientY,
}
}
const onTouchMove = (event) => {
event.preventDefault()
if (isDragging) {
const touch = event.touches[0]
const deltaX = touch.clientX - previousMousePosition.x
const deltaY = touch.clientY - previousMousePosition.y
lastDragDelta = { x: deltaX, y: deltaY }
const rotationFactor = 0.002
const angleY = deltaX * rotationFactor
const angleX = deltaY * rotationFactor
const quaternionY = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 1, 0),
angleY
)
const quaternionX = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(1, 0, 0),
angleX
)
const deltaQuat = new THREE.Quaternion().multiplyQuaternions(quaternionY, quaternionX)
sphere.quaternion.multiplyQuaternions(deltaQuat, sphere.quaternion)
const dragRotationAxis = new THREE.Vector3(deltaY, deltaX, 0).normalize()
const dragRotationSpeed = Math.sqrt(deltaX * deltaX + deltaY * deltaY) * rotationFactor
if (dragRotationAxis.length() > 0) {
currentAngularVelocity.copy(dragRotationAxis).multiplyScalar(dragRotationSpeed)
}
previousMousePosition = {
x: touch.clientX,
y: touch.clientY,
}
}
}
const onTouchEnd = (event) => {
if (isDragging) {
isDragging = false
const deltaX = lastDragDelta.x
const deltaY = lastDragDelta.y
if (deltaX !== 0 || deltaY !== 0) {
const newAxis = new THREE.Vector3(deltaY, deltaX, 0).normalize()
if (newAxis.length() > 0) {
autoRotationAxis.copy(newAxis)
}
const dragSpeed = currentAngularVelocity.length()
if (dragSpeed > autoRotationSpeed) {
// 维持当前旋转速度
} else {
currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
}
}
}
// 检查点击事件
if (event.changedTouches.length > 0) {
const touch = event.changedTouches[0]
mouse.x = (touch.clientX / window.innerWidth) * 2 - 1
mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1
checkIntersection()
}
}
// 事件监听
window.addEventListener('mousedown', onMouseDown)
window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp)
window.addEventListener('touchstart', onTouchStart)
window.addEventListener('touchmove', onTouchMove)
window.addEventListener('touchend', onTouchEnd)
document.addEventListener('gesturestart', function (e) {
e.preventDefault()
})
// 添加点击事件监听
window.addEventListener('click', onMouseClick)
// 处理窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
})
function onMouseClick(event) {
event.preventDefault()
mouse.x = (event.clientX / window.innerWidth) * 2 - 1
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
console.log(event.clientX, mouse.x, mouse.y)
checkIntersection()
}
function checkIntersection() {
raycaster.setFromCamera(mouse, camera)
const intersects = raycaster.intersectObjects(smallBalls)
if (intersects.length > 0) {
const intersectedBall = intersects[0].object
const index = smallBalls.indexOf(intersectedBall)
if (index !== -1) {
const labelInfo = labelSprites[index]
showLabelInfo(labelInfo)
}
}
}
function showLabelInfo(labelInfo) {
alert(`点击的小球标签:${labelInfo.labelText}`)
}
// 动画循环
function animate() {
requestAnimationFrame(animate)
if (!isDragging) {
const deltaQuat = new THREE.Quaternion().setFromEuler(
new THREE.Euler(
currentAngularVelocity.x,
currentAngularVelocity.y,
currentAngularVelocity.z,
'XYZ'
)
)
sphere.quaternion.multiplyQuaternions(deltaQuat, sphere.quaternion)
const currentSpeed = currentAngularVelocity.length()
if (currentSpeed > autoRotationSpeed) {
currentAngularVelocity.multiplyScalar(decayRate)
if (currentAngularVelocity.length() < autoRotationSpeed) {
currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
}
} else if (currentSpeed < autoRotationSpeed) {
currentAngularVelocity.multiplyScalar(increaseRate)
if (currentAngularVelocity.length() > autoRotationSpeed) {
currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
}
} else {
currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
}
}
// 更新标签的位置和跑马灯效果
labelSprites.forEach(({ sprite, smallBall, texture, needMarquee }) => {
smallBall.updateMatrixWorld()
const smallBallWorldPos = new THREE.Vector3()
smallBall.getWorldPosition(smallBallWorldPos)
const upOffset = new THREE.Vector3(0, 0.3, 0)
sprite.position.copy(smallBallWorldPos).add(upOffset)
if (needMarquee) {
texture.offset.x += textSpeed
if (texture.offset.x > 1) {
texture.offset.x = 0
}
}
})
renderer.render(scene, camera)
}
animate()
function getRandomBrightColor() {
const hue = Math.floor(Math.random() * 360)
const saturation = Math.floor(Math.random() * 40 + 10)
const lightness = Math.floor(Math.random() * 40 + 40)
const rgb = hslToRgb(hue, saturation, lightness)
return (rgb.r << 16) | (rgb.g << 8) | rgb.b
}
function hslToRgb(h, s, l) {
s /= 100
l /= 100
const c = (1 - Math.abs(2 * l - 1)) * s
const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
const m = l - c / 2
let r, g, b
if (h >= 0 && h < 60) {
r = c
g = x
b = 0
} else if (h >= 60 && h < 120) {
r = x
g = c
b = 0
} else if (h >= 120 && h < 180) {
r = 0
g = c
b = x
} else if (h >= 180 && h < 240) {
r = 0
g = x
b = c
} else if (h >= 240 && h < 300) {
r = x
g = 0
b = c
} else {
r = c
g = 0
b = x
}
return {
r: Math.round((r + m) * 255),
g: Math.round((g + m) * 255),
b: Math.round((b + m) * 255),
}
}
function getRandomNickname() {
const adjectives = [
'Cool',
'Crazy',
'Mysterious',
'Happy',
'Silly',
'Brave',
'Smart',
'Swift',
'Fierce',
'Gentle',
]
const nouns = [
'Tiger',
'Lion',
'Dragon',
'Wizard',
'Ninja',
'Pirate',
'Hero',
'Ghost',
'Phantom',
'Knight',
]
const randomAdjective = adjectives[Math.floor(Math.random() * adjectives.length)]
const randomNoun = nouns[Math.floor(Math.random() * nouns.length)]
const nickname = `${randomAdjective} ${randomNoun}`
if (nickname.length < 2) {
return getRandomNickname()
} else if (nickname.length > 22) {
return nickname.slice(0, 22)
}
return nickname
}
</script>
</body>
</html>
来源:juejin.cn/post/7425249244850913280
当前端遇到了自动驾驶
这是一篇用ThreeJS开发自动驾驶点云标注系统的实战记录,也是《THREEJS无师自通》的第一篇。通常情况,一个系列文章开篇应该是Quick Start或者Guide之类的简单口水文,而我选择以此开篇,最主要的原因还是因为这次经历足够有趣。
公众号|沐洒(ID:musama2018)
前端开发,大家熟啊,有很多亲(bi)切(shi)的称谓,诸如“切图仔”,“Bug路由器”。自动驾驶,大家更熟了吧,最近几年但凡新能源汽车,谁要是不说自己搭配点自动驾驶(或辅助驾驶)功能,都不好意思拿出来卖。那么,当前端和自动驾驶碰到了一起,会发生什么有意思的事呢?
有点云标注相关背景的可以跳过背景普及,直接看方案。
背景
去年9月,我们业务因为某些原因(商业机密)开始接触自动驾驶领域的数据处理,经过仔细一系列调研和盘算,我们最终决定从零开始,独立自研一套自动驾驶点云数据标注系统。你可能要说了,自动驾驶我知道啊,但是“点云”是个啥?呐,就是这玩意儿:
点云的学术定义比较复杂,大家可以自行搜索学习,这里我简单贴一个引述:
点云是指目标表面特性的海量点集合。
根据激光测量原理得到的点云,包括三维坐标(XYZ)和激光反射强度(Intensity)。
根据摄影测量原理得到的点云,包括三维坐标(XYZ)和颜色信息(RGB)。
结合激光测量和摄影测量原理得到点云,包括三维坐标(XYZ)、激光反射强度(Intensity)和颜色信息(RGB)。
在获取物体表面每个采样点的空间坐标后,得到的是一个点的集合,称之为“点云”(Point Cloud)。
看不懂?没事,不重要,你只需要知道,我们周围的世界,都是点构成的,而点云只不过是用一些仪器(比如激光雷达),对真实世界进行了采样(且只对部分属性进行采样)。
好了,假设你已经知道“点云”是啥了,但你心里肯定还有十万个为什么:
你不是说自动驾驶么?前端呢?这仨有啥关联么?这东西自研成本很高么?
别急,容我慢慢解释,先快速普及一下啥叫“数据标注”:
人工智能数据标注是对文本、视频、图像等元数据进行标注的过程,标记好的数据将用于训练机器学习的模型。常见的数据标注类型有文本标注、语义分割和图像视频标注。
这些经标注的训练数据集可用于训练自动驾驶、聊天机器人、翻译系统、智能客服和搜索引擎等人工智能应用场景之中
假设你懒得看,或者看不懂,我再给你翻译翻译,什么叫数据标注:
一个婴儿来到这个世界,你在它面前放两张卡片,一张红色,一张绿色,你问它,这是什么颜色,它必然是不知道的(我们假设它能听懂并理解你的话)。只有当你一遍又一遍的,不断的告诉它,这是红色,这是绿色,它才会记住。等下次你带它过马路时,它就能准确地识别出红绿灯,并在你面前大声喊出来 “红色(的灯)!”没错,你应该猜到了,那两张卡片本身没有标签(元数据),是你给它们“打上了标”(分别标注了红色和绿色),然后把这个“结构化的数据”,“喂养”给你的宝宝,久而久之,这个宝宝就学会了分辨世间万物,成为一个“智人”。
(图片来源于网络)
你的“喂养”,就是人工;宝宝的成长,就是智能。人工智能(AI,Artificial Intelligence),就是数据喂养的成果,没有数据标注,就没有人工智能。
从这个意义上聊,你和我,都是别人(父母,老师,朋友…)用成千上万的标注数据喂养出来的AI。
扯远了,收!我们说回自动驾驶。
大家都知道现在自动驾驶很火啊,那自动驾驶的“智能”是怎么训练的呢?当然是算法工程师用模型训练出来的啦,而自动驾驶模型需要喂养的数据,就是点云。仪器扫描回来的点云数据里,仅仅只是包含了所有点的基本信息(位置,颜色,激光强度等),模型怎么知道这个点是人身上采的,还是出租车上采的呢?!
(图片来源于网络)
于是这些点就需要被加工(标注),被我们用一系列手段(包括人工和机器)给点赋予更多的信息,区分出每一个点的含义(语义分割)。在自动驾驶领域的点云标注里,我们需要通过2D+3D工具,把物体识别出来。本文重点讲3D的部分。可以先看下3D框的效果:
(图中黄色高亮的点,就是被3D框圈中的点云)
挑战
以往我们较为常见的数据标注,主要集中在文本,图片,视频等类型,例如文本翻译,音频转写,图片分类等等,涉及的工具基本上都是传统web开发知识可以搞定的,而点云标注则完全不同,点云需要作为3D数据渲染到立体空间内,这就需要使用到3D渲染引擎。我们使用的是ThreeJS,这是一个基于WebGL封装的3D引擎。
写了10年的web前端代码,能有机会把玩一下3D技术,还真是挺令人兴奋的。于是我们吭哧吭哧把基本的3D拉框功能做出来了,效果是这样的:
(3D拉框 - 人工调整边缘:2倍速录制)
动图是我加了2倍速的效果,真实情况是,要标出图上这辆小汽车,我需要先拉出一个大概的2D矩形区域,然后在三视图上不断的人工调整边缘细节,确保把应该纳入的点都框进去(未框入的点呈白色,框体垂直方向未框入则呈现蓝色,框入的呈现黄色)
看起来好像也还行?
no,no,no!你知道一份完整的点云标注任务需要标多少个框么?也不吓唬大家,保守点,一般情况一份连续帧平均20帧左右,每帧里要标注的框体保守点,取100个吧,而这一份连续帧标注,必须同一个标注员完成,那么20帧至少有2000个框体需要标注!
按照上面实现的这种人工调节边缘的方式来拉框,一个框需要22秒(GIF共11秒,2倍速),熟练工可能能在10秒内调整完成。那么2000个框体,单纯只是拉框这一件小事,不包括其他工序(打标等),就需要耗费20000秒,约等于5.5小时!
这是什么概念?通常情况标注员都是坐班制,平均一天有效工作时长不超过6小时,也就是说,一个标注员,在工位上一动不动,大气都不敢喘一下的工作一天,就只能把一条点云数据标完,哦不对,仅仅只是拉完框!没错,只是拉框而已。
这种低效的重复性工作,哪个组织受得了?怎么办呢?
方法比较容易想,不就是引入自动化能力么,实现自动边缘检测,嗯,想想倒是挺简单的,问题是怎么实现呢?
以下进入干货区,友情提示:货很干,注意补水。
方案
点云分类
基本思路就是进行边缘探测:
找出三个坐标轴(XYZ)方向上的框体边缘点,计算出边缘点之间的距离,调整框体的长宽高,进而将框体贴合到边缘点。
边缘的定义:
某方向上的同值坐标点数大于某个设定值(可配置,默认3,三者为众)
找出边缘点的核心算法:
遍历框体内的点,分别将XYZ方向的坐标值存入数组,加权,排序,取第一个满足边缘定义的点,作为该方向极限值。
进行边缘判定之前,我们得先找出存在于框体内的点,这就涉及到第一个核心问题:点云和3D框的相对位置判断。
为了更好的管理与框体“强相关”的点云,我们先对点云进行一个基本分类:
从俯视图看,把3D图降维成2D图,立方体则看作矩形,如下图:
则点与框的相对位置可以降维等效为:
第一类(点在立方体内)
点在矩形内,且点的Z值在立方体[Zmin, Zmax]范围内
第二类(点在立方体垂直方向)
点在矩形内,且Z值在立方体[Zmin, Zmax]范围外
第三类(点在立方体周围)
点在延展矩形(向外延展N个距离)内,且不属于第二类。
我们先按这个思路实现一版代码:
// 判断点是否位于框体XY平面区域内
function isPointInXYPlane(gap: IGap, distance = 0) {
const { gapL, gapR, gapB, gapU } = gap;
// 在框体XY平面区域内
return gapL > - distance && gapR < distance && gapU < distance && gapB > - distance;
}
// 在框体垂直方向上下边界内
function isPointInVerticalBoundry(up: number, bottom: number, z: number) {
return z >= bottom && z <= up;
}
// 位于框体XY平面向外延伸NEAR_DISTANCE距离的区域内
if (isPointInXYPlane(posInfo.gap, NEAR_DISTANCE)) {
const isInVerticalBoundry = isPointInVerticalBoundry(posInfo.up, posInfo.bottom, posInfo.z);
// 位于框体XY平面区域内
if (isPointInXYPlane(posInfo.gap)) {
// 在框体内
if (isInVerticalBoundry) {
isInside = true;
} else {
// 在框体外的垂直方向上
isVertical = true;
}
}
// 在框体上下边界内
if (isInVerticalBoundry) {
isNearBy = true;
}
}
通过以上逻辑,我们就拿到了与框体“相关”的点云(正确与否先按下不表,后面会说),我们先存起来,后面做极值寻找(即边缘检测)时候使用。
第一版效果
看起来好像还行,基本实现了贴合,但是……我们旋转一下看看:
好家伙,旋转后框体边界没更新!所以点云高亮也没变化。
这个问题其实也好理解,我们在处理边界的时候,只采用position和scale计算,并没有使用rotation属性,所以当框体的旋转分量发生变化,我们计算边界时没有及时调整,程序就会认为框体此时仍然留在原地未动呢。
我们来优化一下。我先尝试用三角函数来计算旋转后的新坐标点,类似这样
折腾了很久的三角函数,有点变化了,但是效果却成了这样:
已经接近真相了,只需要把待判定点放到三角函数判定公式里,就可以知道该点是否在旋转后的框体内了,不过到这里我突然意识到问题被我搞复杂了,是不是可以有更简单的方法来判定矩形内部点呢?
我们回到最初的问题:判断一个点,与一个立方体的相对位置
对这个原始问题进行逻辑拆解,可以拆为3个子问题:
- 如何判断一个点位于立方体内部?
- 如何判断一个点位于立方体的垂直方向(排除体内点)?
- 如何判断一个点位于立方体的周围(排除垂直方向点)?
关于问题1,第一反应还是立体几何,而且我笃定这是个非常成熟的几何问题,没必要自己硬憋。于是我就上网搜索:How to determine a point is inside or outside a cube? 结果如下:
上面是stackoverflow上大神给的两种数学方法,一看就知道能解,奈何我看图是看懂了,公式没有完全吸收透,于是最终没有采纳(尽量不干不求甚解的事,写成代码就要求自己得是真的懂)
于是我进一步思考:
几种数学方法确实都很虎,但我是不是把问题搞复杂了?能不能没事踩踩别人的肩膀呢?
看看ThreeJS 是否有相应的API……果然有:
这不正好就是我想要的效果么?踏破铁鞋无觅处,得来全不费功夫啊!
直接拿来用,搞定!
但问题来了,人家是怎么做到的呢?带着这个疑问,我开始翻相关源码。
首先看到containsPoint,其实就和我们用的方法是一样的:
// https://github.com/mrdoob/three.js/blob/4503ef10b81a00f5c6c64fe9a856881ee31fe6a3/src/math/Box3.js#L243
containsPoint( point ) {
return point.x < this.min.x || point.x > this.max.x ||
point.y < this.min.y || point.y > this.max.y ||
point.z < this.min.z || point.z > this.max.z ? false : true;
}
而核心问题还是得想办法计算出box.min和box.max,那ThreeJS是怎么计算的呢?继续看:
// https://github.com/mrdoob/three.js/blob/4503ef10b81a00f5c6c64fe9a856881ee31fe6a3/src/core/BufferGeometry.js#L290
computeBoundingBox() {
// ..... 省略部分代码 ....
const position = this.attributes.position;
if ( position !== undefined ) {
this.boundingBox.setFromBufferAttribute(position);
}
// ..... 省略部分代码 ....
}
看起来boundingBox的属性来自于attributes.position,这个position就是box在世界坐标里的具体位置,是我们在创建box时候设定的。再继续深挖下setFromBufferAttribute:
// https://github.com/mrdoob/three.js/blob/4503ef10b81a00f5c6c64fe9a856881ee31fe6a3/src/math/Box3.js#L56
setFromBufferAttribute( attribute ) {
// ..... 省略部分代码 ....
for ( let i = 0, l = attribute.count; i < l; i ++ ) {
const x = attribute.getX( i );
const y = attribute.getY( i );
const z = attribute.getZ( i );
if ( x < minX ) minX = x;
if ( y < minY ) minY = y;
if ( z < minZ ) minZ = z;
if ( x > maxX ) maxX = x;
if ( y > maxY ) maxY = y;
if ( z > maxZ ) maxZ = z;
}
this.min.set( minX, minY, minZ );
this.max.set( maxX, maxY, maxZ );
return this;
}
平平无奇啊这代码,几乎和我们自己写的边界判定代码一模一样啊,也没引入rotation变量,那到底怎么是在哪处理的旋转分量呢?
关键点在这里:
我尝试给你解释下:
在调用containsPoint之前,我们使用box的转换矩阵,对point使用了一次矩阵逆变换,从而把point的坐标系转换到了box的坐标系,而这个转换矩阵,是一个Matrix4(四维矩阵),而point是一个Vector3(三维向量)。
使用四维矩阵对三维向量进行转换的时候,会逐一提取出矩阵的position(位置),scale(缩放)和rotation(旋转)分量,分别对三维向量做矩阵乘法。
也就是这么一个操作,使得该point在经过矩阵变换之后,其position已经是一个附加了rotation分量的新的坐标值了,然后就可以直接拿来和box的8个顶点的position做简单的边界比对了。
这里涉及大量的数学知识和ThreeJS底层知识,就不展开讲了,后面找机会单独写一篇关于转换矩阵的。
我们接着看点与框体相对位置判断的第二个问题:如何判断一个点位于立方体的垂直方向(排除体内点)?
首先,我们置换下概念:
垂直方向上的点 = Z轴方向上的点 = 从俯视图看,在XY平面上投射的点 - 框内点
那么,如何判断一个点在一个矩形内,这个问题就进一步转化为:
(AB X AE ) * (CD X CE) >= 0 && (DA X DE ) * (BC X BE) >= 0
这里涉及到的数学知识是向量点乘和叉乘的几何意义,也不展开了,感兴趣的朋友可以自行搜索学习下。
还剩最后一个问题:如何判断一个点位于立方体的周围(排除垂直方向点)?
这个问题我们先放一放,周围点判断主要用来扩展框体的,并不影响本次的边界探测结果,以后再找机会展开讲,这里先跳过了。
到此为止,我们就至少拿到了两类点(框内点,和框体垂直方向的点),接下来就可以开始探测边缘了。
边缘探测
边缘探测的核心逻辑其实也不复杂,就是:
遍历框体内的点,分别将X,Y,Z方向的坐标值存入数组,加权,排序,取第一个满足边缘定义的点,作为该方向极限值。
这里我们可以拆分位两个Step。
Step 1:点位排序
基本思路如下:
选择一个方向,遍历点云,取到该方向上点云的坐标值,放入一个map中,key为坐标值,value为出现次数。同时对该坐标进行排序,并返回有序数组。**
那么问题来了,点云的坐标值多半精确到小数点七八位,如果直接以原值作为key,那么这个map很难命中重复坐标,那map的意义就不大了,难以聚合坐标。
于是这里对原坐标取2个精度后作为key来聚合点云,效果如下:
可以明显看到已经有聚合了。这是源码实现:
Step 2:夹逼探测
拿到了点云坐标的聚合map,和排序数组,那么现在要检测边缘就很简单了,基本思路就是:
从排序数组的两头开始检查,只要该点的聚合度大于DENSE_COUNT(根据需要设置,默认为3),我们就认为这个点是一个相对可信的边缘点。
从这个算法描述来看,这不就是个夹逼算法么,可以一次遍历就拿到两个极值。
到这里,某方向的两个极值(low 和 high)就拿到手了,那么剩下的工作无非就是分别计算XYZ三个方向的极值就好了。
我们来看下效果,真的是“啪”一下,就贴上去了:
上面的案例录制的比较早,有点模糊,再来看个高清带色彩的版本:
这个体验是不是很丝滑?就这效率,拉框速度提升了10倍有吧?(22秒 -> 2秒)
读到这里,不知道大家还记不记得前面,我们刻意跳过了一个环节的介绍,就是“框体周围点位”这一部分,这里简单补充两句吧。
在实际的场景里,有很多物体是靠得很近的,还有很多物体的点云并没有那么整齐,会有一些离散点在物体周围。那么这些点就可能会影响到你的边缘极限值的判断。
因此我在这里引入了两个常量:
附近点判定距离 NEAR_DISTANCE(框体紧凑的场景,NEAR_DISTANCE就小一点,否则就大一点)!
密集点数 DENSE_COUNT(点云稀少的场景,就可以把DENSE_COUNT设置小一点,而点云密集厚重的场景,DENSE_COUNT就适当增加。)
通过在不同的场景下,调整这两个常量的取值,就可以使得边缘探测更加的准确。
遗留问题
其实在3D的世界里,多一个维度之后,很多问题都会变得更加的麻烦起来。上面的方案,在处理大部分场景的时候都能work,但实际上依然有一些小众场景下存在问题,比如:
平时多半都是物体都是围绕Z轴旋转,但如果有上下坡路,物体围绕XY轴旋转,那垂直方向就需要进行矫正。
再比如:
用户移动了镜头方位,在屏幕上拉2D框的时候,就需要对2D框采集到的坐标进行3D投射,拿到真实的世界坐标,才能创建合适的立方体。
当然,这些问题在后面的版本都已经完善修复了,之所以放在遗留问题,是想说明,仅仅依照正文部分的方法去实现的话,还会有这些个遗留的问题需要单独处理。
如果大家感兴趣的话可以留言告诉我,我再决定要不要接着写。
来源:juejin.cn/post/7422338076528181258
已老实!公司的代码再也不敢乱改了!
开篇
大家好,我是聪。想必对于很多初入职场,心中怀着无限激情的兄弟们,对于接手老代码都会有很多愤慨,碰到同事的代码十分丑陋应不应该改!我也是这样,我相信有很多人同样有跟我一样的经历。满打满算实习 + 正式工作,我也敲了两年多代码,我今天来说说我自己的看法吧。
亲身经历
我第一次接手老代码的时候,映入我眼帘的就是侧边栏满页的黄色提示以及代码下面的众多黄色波浪线,以及提交代码时的提示,如下图:
我内心 OS:
1)大干一场,把黄色波浪线全干掉!
2)同事这写的也太不优雅了吧,改成我这样!
3)这代码怎么也没格式化,我来 Ctrl + Alt + L 格式化一波!
已老实,求放过
干掉黄色波浪线,将代码改 ”优雅“ 结局如下:
1)不声不吭动了同事代码,换来同事怒骂,毕竟人家逻辑写好,然后你按你想法来搞,也没有跟人家商量。
2)后续领导找你加需求,你发现原来之前的代码有妙用,你悔不当初,被扣绩效。
3)格式化后,在项目修改记录上面是你的修改,这代码出问题,负责人先来找你。
说说我的看法
代码能跑不要动
前几日我要在老项目中,新增一点小功能,在新增完功能后,我扫了一眼代码,发现有几处逻辑根本不会执行,比如:抛异常后,执行删除操作类似,我也不会去义愤填膺的去干掉这块代码,毕竟我想到一点!项目都跑七八年没出问题了,能跑就别动它。
代码强迫症不要强加于别人
前几日在某金看见了这样一个沸点:

这样的事情其实在小公司经常发生,你觉得它写的不优雅,封装少,可能是别人也有别人的难处,至少不能将自己想法强加于别人,比如领导突然来一个需求,跟你说今天你得完成,然后第二天这个需求,你要这样改、再给我加点新需求上去,你能想到的封装其实只是你冷静下来,而且没有近乎疯狂的迭代需求得到的想法,当你每天都要在原代码上面疯狂按照领导要求修改,可能你会有自己的看法。
新增代码,尽量不影响以前逻辑
新增代码的时候,尽量按照以前的规则逻辑来进行,比如我改的一个老项目,使用的公司自己写的一套 SQL 处理逻辑,我总不能说不行!我用不惯这个!我要用 MyBatis!!!!那真的直接被 T 出门口了。
尊重他人代码风格
每个人的代码风格都有所不同,这个很正常,不同厨师的老师教法不一样,做出的味道还不一样呢,没有最好的代码,只有更适合的代码,刚好我就有这样的例子:
我注入 Spring 依赖喜欢用构造注入、用 Lombook 的注解 @RequiredArgsConstructor 注入,我同事喜欢 @Autowired ,我能说他不准用这个吗,这个是人家的习惯,虽然 Spring 也不推荐使用这个,但改不改这个都不会影响公司收益,反而能少一件事情,促进同事友好关系,哈哈哈哈,我是这样认为的。
处理好同事之间的关系
哈哈哈哈这个真的就是人情事故了,你换位想象一下,如果你写的幸幸苦苦的代码,新来的同事或者实习生,来批评你的代码不规范,要 Diss 你,偷偷改你代码,就算他说的超级对,你心里都十分不好受,会想一万个理由去反驳。
我一般如果需求需要改动同事的代码,我会先虚心的向同事请求,xx哥,我这个需求要改动你这边的代码来配合一下,你来帮我一起看看,你这部分的代码这样改合理吗,或者你自己改下你自己的部分,然后我合并一下~ 谢谢 xx哥。
来源:juejin.cn/post/7383342927508799539
OpenHarmony首次亮相欧洲开源会议
10月22日至24日,由欧洲最大开源组织 Eclipse 基金会主办的Open Community Experience (OCX) 2024开源大会(后文简称“大会”)于德国·美因茨盛大召开。OpenHarmony项目群技术指导委员会(TSC)主席陈海波受邀参加大会主论坛分享,由OpenHarmony 与 Eclipse Oniro 联合筹划的“Think Global, Code Local”分论坛圆满举办,OpenHarmony最新技术与生态进展也成功亮相大会展区。
嘉宾大合影
陈海波主论坛演讲
10月22日下午,OpenHarmony项目群技术指导委员会(TSC)主席,华为Fellow陈海波在大会的主论坛上作了主题为《Empowering a Connected Intelligent World With OpenHarmony and Oniro》的技术报告。在报告中,陈海波强调了智能移动操作系统的重要性以及影响其发展的三大要素(硬件演进、APP模式开发与操作系统技术创新),阐述了万物智联时代操作系统的发展趋势、挑战与机遇,介绍了OpenHarmony的架构设计与关键特性,并分享了OpenHarmony开源项目的最新技术、生态、人才进展与行业实践。此外,他还提到,开放原子开源基金会与欧洲最大的开源组织 Eclipse 基金会合作,基于OpenHarmony底座发布的 Oniro OS 已经成为极具影响力的开源操作系统。
OpenHarmony项目群技术指导委员会(TSC)主席陈海波主论坛演讲
OpenHarmony & Eclipse基金会闭门研讨会
此外,OpenHarmony 项目群技术指导委员会(TSC)主席陈海波、秘书处主任许家喆、副主任刘果,OpenHarmony PMC主席任革林,OpenHarmony项目群工作委员会执行主席助理周顺淦,上海交通大学OpenHarmony技术俱乐部主任夏虞斌,华为德累斯顿研究所所长刘宇涛,华为开源专家王荣泽,华为欧洲开源专家Adrian OSullivan等与 Eclipse 基金会执行董事Mike Milinkovich、董事Bryan Che以及首席会员官Gaël Blondelle等开展了闭门研讨,就OpenHarmony与 Eclipse 基金会的进一步合作以及OpenHarmony技术生态在欧洲的未来发展进行深入交流。
OpenHarmony & Eclipse基金会闭门研讨会
“Think Global,Code Local”分论坛
10月23日下午,OpenHarmony 与 Eclipse Oniro 联合筹划的“Think Global,Code Local”分论坛如期举办。
分论坛嘉宾合影
分论坛现场
Eclipse 基金会首席会员官 Gaël Blondelle 与OpenHarmony项目群工作委员会执行主席助理周顺淦出席分论坛并作开场欢迎致辞。他们表示,全球开源生态的良好发展是技术创新的关键。未来,开放原子开源基金会与 Eclipse 基金会将基于OpenHarmony开展进一步的深度合作,持续推动OpenHarmony开源生态在欧洲的发展。
OpenHarmony项目群工作委员会执行主席助理周顺淦(左)与 Eclipse 基金会首席会员官Gaël Blondelle(右)欢迎致辞
Oniro指导委员会主席Suhail Khan出席分论坛并作《Oniro and OpenHarmony partnership. From code cooperation to a global ecosystem》技术报告。在报告中,Suhail Khan提到,基于OpenHarmony底座的强大能力使 Oniro 项目能够满足多样化的全球产品需求和标准。他介绍了当前 Oniro 项目的最新进展,并展望了未来OpenHarmony和 Oniro 通过进一步的深度合作从而推动全球开源生态的繁荣发展。
Oniro指导委员会主席Suhail Khan技术报告
华为德累斯顿研究所技术专家Hernan Luis Ponce de Leon出席分论坛并作《Introduction to OpenHarmony Concurrent and Collaborative VSync Open Source Project》技术报告。在报告中,Hernan Luis Ponce de Leon详细介绍了OpenHarmony并发与协同TSG孵化的开源项目“VSync”的最新进展。他提到,并行和异构是摩尔定律之后驱动性能的关键,但多核、VMM、NUMA以及异构等复杂现代硬件进一步加剧了软件的复杂性。VSync提供了Vsyncer工具,可在内存一致性较弱CPU上验证并发算法的准确性,并提供一系列使用Vsyncer工具验证过的并发算法。
华为德累斯顿研究所技术专家Hernan Luis Ponce de Leon技术报告
华为华沙研究所技术专家Jaroslaw Marek与华为米兰研究所资深技术专家Francesco Pham出席分论坛并作《An architecture designed to boost innovation,interoperability and opportunities》技术报告。在报告中,Jaroslaw Marek介绍了 Eclipse 基金会 Oniro 开源项目的最新进展,重点包括技术文档、社区交流、GitHub项目以及IP合规性等,并分享了 Oniro 如何基于OpenHarmony建立开源生态以及其对本地市场的适应性;Francesco Pham介绍了欧洲构建的开源移动平台“Oniro Developer Phone”,包括其四个关键的开发阶段、Volla X23内核的技术创新以及原生应用商店的集成,并讨论了该平台对开发者的重要意义以及后续的发展规划。
华为华沙研究所技术专家Jaroslaw Marek技术报告
华为米兰研究所技术专家Francesco Pham技术报告
华为德累斯顿研究所技术专家张越出席分论坛并作《Challenges and difficulties in terminal operating system technology》技术报告。在报告中,张越阐述了万物智联时代终端操作系统技术创新与发展将面临的10个主要挑战和难点,并基于OpenHarmony的技术架构优势给出了相关解决方案。
华为德累斯顿研究所技术专家张越技术报告
Array公司合伙人Alberto Pianon与NOI技术创新中心的开放数据和自由软件技术专家Luca Miotto出席分论坛并作《Collaborative Approaches towards license management》技术报告。在报告中,Alberto Pianon与Luca Miotto介绍了OpenHarmony社区的自动化开源审视工具“OAT (OSS Audit Tool)”,并分享了当前OpenHarmony在识别第三方组件许可证变体类型上可能面临的挑战和困难。此外,他们正在与 Eclipse Oniro 合规工具链项目合作,以进一步改进和完善OpenHarmony的许可合规流程。
NOI技术创新中心的开放数据和自由软件技术专家Luca Miotto技术报告
Array公司合伙人Alberto Pianon技术报告
OpenHarmony PMC主席任革林出席分论坛并作《OpenHarmony Design Philosophy and Latest Progress》技术报告。在报告中,任革林以两个典型的手机、平板、IOT等多设备协同交互的场景,介绍终端操作系统所面临的应用、数据、能力、交互“割裂”问题,并由此引出OpenHarmoy的核心设计理念:“超级终端”,即多物理设备融合成一个逻辑终端。此外,他提到,OpenHarmony已成为发展速度最快的智能终端操作系统根社区,四年内PMC共发布47个版本,其中7个为大版本,社区活跃度持续保持领先。
OpenHarmony PMC主席任革林技术报告
Futurewei开源技术战略总监Mats Lundgren出席论坛并做《A Web Engine For the Future》技术报告。在报告中,Mats Lundgren介绍了Web内核Servo的重要特性,并分享了Servo在OpenHarmony and Oniro项目中的集成进展。他提到,Servo由Rust编写,支持WebGL和WebGPU,适用于桌面、移动和嵌入式应用,能够给用户带来更高效以及更安全的体验。
Futurewei开源技术战略总监Mats Lundgren技术报告
上海交通大学OpenHarmony技术俱乐部主任、软件学院副院长夏虞斌出席论坛并做《Introduction to OpenHarmony Security Related Technologies》技术报告。在报告中,夏虞斌从总体架构设计、实现原理以及实际应用案例等方面详细介绍了基于RISC-V的蓬莱TEE,并介绍了分布式TEE的概念。他还提到,通过将蓬莱整合进OpenHarmony以形成统一的TEE架构,不仅提高了跨平台的兼容性,也简化了可信应用的移植过程。
上海交通大学OpenHarmony技术俱乐部主任、软件学院副院长夏虞斌技术报告
Software Mansion公司软件专家Przemysław Sosna出席论坛并做《Supporting developers, the role of React Native in OpenHarmony and Oniro》技术报告。在报告中,Przemysław Sosna介绍了 Oniro 开源操作系统在移动设备中的应用进展,并阐述了跨平台开发框架“React Native”以及ArkUI的技术架构、实现原理以及最新生态进展。目前,React Native已经逐步扩展到OpenHarmony以及 Onrio 上,为现有应用的移植和开发提供便利与跨平台开发优势。
Software Mansion公司软件专家Przemysław Sosna技术报告
OpenHarmony开源图形驱动SIG、游戏SIG组长,华为终端图形TMG主任黄然出席论坛并做《Introduction to OpenHarmony Graphics Technology》技术报告。在报告中,黄然强调了图形是OpenHarmony实现极致流畅体验的关键,并结合计算机图形发展的过去、现在与未来发展,重点阐述了OpenHarmony图形相关技术架构的演进思路,提出了万物智联时代计算机图形发展趋势的畅想与思考。
OpenHarmony开源图形驱动SIG、游戏SIG组长,华为终端图形TMG主任黄然技术报告
江苏润和软件股份有限公司副总裁刘洋出席论坛并作《Challenges and opportunities for companies with a global business》技术报告。刘洋提到,OpenHarmony-Oniro是一个真正开源的操作系统,支持各类设备的快速互联互通,并优化了性能功耗等系统能力。在中国,OpenHarmony在电力、煤炭和金融等行业得到了广泛应用,随着HarmonyOS 5.0的发布,标志鸿蒙生态在消费电子领域也取得了巨大成功。欧洲一直以来有着深厚技术底蕴和大量兴趣开发者,Oniro 在欧洲商业成功的首要目标是培养足够多的开发者,通过开发者在欧洲建立一个 Oniro 的技术生态。
江苏润和软件股份有限公司副总裁刘洋技术报告
OCX Tracks
Eclipse 基金会Oniro and Cloud Programs的高级经理Juan Rico出席OCX Tracks并做《Cooperation between two OS Foundations to build a global interoperable ecosystem》技术报告。在报告中,Juan Rico介绍了如何进一步推动开放原子开源基金会与 Eclipse 基金会间的合作和交流,促进OpenHarmony和 Oniro 开源项目的技术和生态发展,以打造更具影响力的智能终端操作系统底座,促进全球开源生态的繁荣发展。
Eclipse基金会Oniro and Cloud Programs的高级经理Juan Rico技术报告
Eclipse Oniro 合规工具链项目负责人Alberto Pianon以及OpenHarmony合规SIG负责人高亮出席OCX Tracks并做《Collaborative Approaches to License Compliance: OpenHarmony and Eclipse Oniro Compliance Toolchain》技术报告。在报告中,Alberto Pianon和高亮介绍了OpenHarmony基于OAT(OSS审核工具)的许可证合规验证的解决方案,并分享了当前识别第三方组件许可证变体类型所面临的困难。此外,他们还介绍了目前了目前OpenHarmonyOpenHarmony与 Eclipse Oniro 合规工具链所合作的项目进展,以改进OpenHarmony许可证许可证合规验证流程。
Eclipse Oniro合规工具链项目负责人Alberto Pianon(右)以及OpenHarmony合规SIG负责人高亮(左)技术报告
OpenHarmony展区
此外,OpenHarmony开源项目介绍、社区建设情况、技术与生态的最新进展以及仓颉、Vsync、OpenTrustee、ArkUI-X等孵化技术项目成果等也亮相大会展区,来自SAMSUNG、LG、Bosch、Canonical、S&P Global以及Harman等公司的与会嘉宾在OpenHarmony展区前就相关技术进展和未来的合作机会进行积极交流和思想碰撞。
OpenHarmony展区
OpenHarmony亮相OCX 2024开源大会是OpenHarmony开源技术生态在欧洲拓展的第一站,标志着OpenHarmony在智能终端领域全球开源生态迈出了重要一步。未来,OpenHarmony将与欧洲最大开源组织Eclipse基金会开展更进一步的深度合作,共享技术资源、提升开源项目的可持续性并拓宽开发者的视野,持续推动全球开源生态良好发展。
收起阅读 »接口不能对外暴露怎么办?
在业务开发的时候,经常会遇到某一个接口不能对外暴露,只能内网服务间调用的实际需求。
面对这样的情况,我们该如何实现呢?
1. 内外网接口微服务隔离
将对外暴露的接口和对内暴露的接口分别放到两个微服务上,一个服务里所有的接口均对外暴露,另一个服务的接口只能内网服务间调用。
该方案需要额外编写一个只对内部暴露接口的微服务,将所有只能对内暴露的业务接口聚合到这个微服务里,通过这个聚合的微服务,分别去各个业务侧获取资源。
该方案,新增一个微服务做请求转发,增加了系统的复杂性,增大了调用耗时以及后期的维护成本。
2. 网关 + redis 实现白名单机制
在 redis 里维护一套接口白名单列表,外部请求到达网关时,从 redis 获取接口白名单,在白名单内的接口放行,反之拒绝掉。
该方案的好处是,对业务代码零侵入,只需要维护好白名单列表即可;
不足之处在于,白名单的维护是一个持续性投入的工作,在很多公司,业务开发无法直接触及到 redis,只能提工单申请,增加了开发成本;
另外,每次请求进来,都需要判断白名单,增加了系统响应耗时,考虑到正常情况下外部进来的请求大部分都是在白名单内的,只有极少数恶意请求才会被白名单机制所拦截,所以该方案的性价比很低。
3. 方案三 网关 + AOP
相比于方案二对接口进行白名单判断而言,方案三是对请求来源进行判断,并将该判断下沉到业务侧。避免了网关侧的逻辑判断,从而提升系统响应速度。
我们知道,外部进来的请求一定会经过网关再被分发到具体的业务侧,内部服务间的调用是不用走外部网关的(走 k8s 的 service)。
根据这个特点,我们可以对所有经过网关的请求的header里添加一个字段,业务侧接口收到请求后,判断header里是否有该字段,如果有,则说明该请求来自外部,没有,则属于内部服务的调用,再根据该接口是否属于内部接口来决定是否放行该请求。
该方案将内外网访问权限的处理分布到各个业务侧进行,消除了由网关来处理的系统性瓶颈;
同时,开发者可以在业务侧直接确定接口的内外网访问权限,提升开发效率的同时,增加了代码的可读性。
当然该方案会对业务代码有一定的侵入性,不过可以通过注解的形式,最大限度的降低这种侵入性。
具体实操
下面就方案三,进行具体的代码演示。
首先在网关侧,需要对进来的请求header添加外网标识符: from=public
@Component
public class AuthFilter implements GlobalFilter, Ordered {
@Override
public Mono < Void > filter ( ServerWebExchange exchange, GatewayFilterChain chain ) {
return chain.filter(
exchange.mutate().request(
exchange.getRequest().mutate().header('id', '').header('from', 'public').build())
.build()
);
}
@Override
public int getOrder () {
return 0;
}
}
接着,编写内外网访问权限判断的AOP和注解
@Aspect
@Component
@Slf4j
public class OnlyIntranetAccessAspect {
@Pointcut ( '@within(org.openmmlab.platform.common.annotation.OnlyIntranetAccess)' )
public void onlyIntranetAccessOnClass () {}
@Pointcut ( '@annotation(org.openmmlab.platform.common.annotation.OnlyIntranetAccess)' )
public void onlyIntranetAccessOnMethed () {
}
@Before ( value = 'onlyIntranetAccessOnMethed() || onlyIntranetAccessOnClass()' )
public void before () {
HttpServletRequest hsr = (( ServletRequestAttributes ) RequestContextHolder.getRequestAttributes()) .getRequest ();
String from = hsr.getHeader ( 'from' );
if ( !StringUtils.isEmpty( from ) && 'public'.equals ( from )) {
log.error ( 'This api is only allowed invoked by intranet source' );
throw new MMException ( ReturnEnum.C_NETWORK_INTERNET_ACCESS_NOT_ALLOWED_ERROR);
}
}
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OnlyIntranetAccess {
}
最后,在只能内网访问的接口上加上@OnlyIntranetAccess注解即可
@GetMapping ( '/role/add' )
@OnlyIntranetAccess
public String onlyIntranetAccess() {
return '该接口只允许内部服务调用';
}
4. 网关路径匹配
在DailyMart项目中我采用的是第四种:即在网关中进行路径匹配。
该方案中我们将内网访问的接口全部以前缀/pv开头,然后在网关过滤器中根据路径找到具体校验器,如果是/pv访问的路径则直接提示禁止外部访问。
使用网关路径匹配方案不仅可以应对内网接口的问题,还可以扩展到其他校验机制上。
譬如,有的接口需要通过access_token进行校验,有的接口需要校验api_key 和 api_secret,为了应对这种不同的校验场景,只需要再实现一个校验类即可,由不同的子类实现不同的校验逻辑,扩展非常方便。
最后说一句(求关注!别白嫖!)
如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。
关注公众号:woniuxgg,在公众号中回复:笔记 就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利!
来源:juejin.cn/post/7389092138900717579
2024:写 TypeScript 必须改掉的 10 个坏习惯
大家好,我是CodeQi! 一位热衷于技术分享的码仔。
在过去的几年里,TypeScript 已经逐渐成为了前端开发的首选语言,尤其是那些追求更高代码质量和类型安全的开发者。不过,正如所有编程语言一样,随着时间的推移和技术的进步,我们的编程习惯也应该与时俱进。
👋 你有没有想过,自己在写 TypeScript 时是否养成了一些“坏习惯”?
随着 TypeScript 生态系统的进一步成熟,有些你以前觉得合理的做法,现在可能不太合理。接下来,我将分享10 个常见的 TypeScript 坏习惯,并告诉你如何改进它们,确保你的代码更健壮、性能更高、并且更加易于维护。
1. 不使用 strict 模式
当开发者为了减少“麻烦”而禁用 TypeScript 的 strict
模式时,往往是在给自己埋雷。💣
为什么不好?
strict
模式通过强制进行更严格的类型检查,帮助我们避免潜在的错误。如果你关掉它,TypeScript 就变得更像是 JavaScript,失去了静态类型带来的种种好处。短期内你可能会觉得更自由,但未来的重构和维护将变得更加棘手。
怎么改进?
在 tsconfig.json
中启用 strict
模式,这样你的代码在未来的迭代中会更加稳健:
{
"compilerOptions": {
"strict": true
}
}
2. 依赖 any 类型
any
可能是 TypeScript 中最具“争议”的类型之一,因为它违背了我们使用 TypeScript 的初衷:类型安全。
为什么不好?
any
让 TypeScript 失去意义。它让代码回归到“JavaScript 模式”,绕过了类型检查,最终可能导致各种运行时错误。
怎么改进?
使用 unknown
替代 any
,并在实际使用前对类型进行检查。unknown
更安全,因为它不会自动允许任何操作:
let data: unknown;
if (typeof data === "string") {
console.log(data.toUpperCase());
}
3. 过度使用类型断言
你是否经常用 as
关键字来“消除”编译错误?🙈 这种做法短期内看似有效,但可能会隐藏更多问题。
为什么不好?
类型断言会绕过 TypeScript 的安全机制,告诉编译器“别管了,我知道自己在做什么”。问题是,当你其实并不完全确定时,它会导致难以追踪的运行时错误。
怎么改进?
减少类型断言,使用类型保护函数代替:
function isString(value: unknown): value is string {
return typeof value === 'string';
}
if (isString(data)) {
console.log(data.toUpperCase());
}
4. 忽视联合类型和交叉类型
联合类型 (|
) 和交叉类型 (&
) 是 TypeScript 中极其强大的工具,但它们经常被忽视。🚫
为什么不好?
没有联合和交叉类型,代码容易变得冗长而难以维护。你可能会写大量的冗余代码,而这些类型可以帮你更简洁地表达逻辑。
怎么改进?
使用联合类型来处理不同情况,交叉类型来组合多个类型:
type Admin = { isAdmin: true; privileges: string[] };
type User = { isAdmin: false; email: string };
type Person = Admin | User;
function logUser(person: Person) {
if (person.isAdmin) {
console.log(person.privileges);
} else {
console.log(person.email);
}
}
5. 使用非特定的返回类型
不为函数指定精确的返回类型,可能会让使用者摸不着头脑。🤔
为什么不好?
模糊的返回类型增加了代码的不确定性,调试难度也会增加。你失去了静态类型的优势,最终使代码变得不可靠。
怎么改进?
始终为函数指定明确的返回类型,哪怕它是一个联合类型:
function fetchData(): Promise<{ id: number; name: string }> {
return fetch("/data").then(response => response.json());
}
6. 忽视 null 和 undefined
一些开发者在处理 null
和 undefined
时掉以轻心,结果导致一堆潜在的运行时错误。
为什么不好?
JavaScript 允许变量为 null
或 undefined
,TypeScript 也有相应的工具帮助处理这些值。如果忽视它们,代码可能会在运行时崩溃。
怎么改进?
使用可选链 (?.
) 和空值合并操作符 (??
) 处理 null
和 undefined
:
const name = user?.profile?.name ?? "Guest";
7. 过度使用 Enums
在 TypeScript 中,Enums
有时会被滥用。尽管它们有其应用场景,但并不总是必要。
为什么不好?
Enums
会增加复杂性,尤其是在简单常量足够的情况下。
怎么改进?
考虑用 const
或字面量类型来替代枚举:
type Role = "Admin" | "User" | "Guest";
let userRole: Role = "Admin";
8. 不使用 readonly
如果不使用 readonly
来防止对象或数组的意外修改,代码中的副作用将难以控制。
为什么不好?
可变性会导致对象在不经意间被修改,造成难以调试的问题。
怎么改进?
尽可能使用 readonly
来确保不变性:
const data: readonly number[] = [1, 2, 3];
9. 忽视自定义类型保护
依赖隐式类型检查而非明确的类型保护,可能导致你错过一些重要的类型问题。
为什么不好?
没有自定义类型保护,你可能会在运行时错过一些类型错误,最终导致不可预期的行为。
怎么改进?
编写明确的类型保护函数:
function isUser(user: any): user is User {
return typeof user.email === "string";
}
10. 没有充分利用 unknown 类型
许多开发者默认使用 any
来处理未知类型,其实 unknown
是一个更好的选择。
为什么不好?
any
禁用了类型检查,而这正是使用 TypeScript 的初衷。unknown
则要求你在使用前对类型进行明确的验证。
怎么改进?
用 unknown
代替 any
,并在使用前进行类型缩小:
let input: unknown;
if (typeof input === "string") {
console.log(input.toUpperCase());
}
总结
2024 年,是时候告别这些坏习惯了!通过启用 strict
模式、避免使用 any
、掌握联合和交叉类型等高级特性,你的 TypeScript 代码将变得更强大、更灵活、更具维护性。希望这些建议能够帮助你在 TypeScript 之路上走得更远,写出更加优雅的代码!✨
来源:juejin.cn/post/7426298029286916146
程序员节快乐!– 致所有 1024 码农的幽默与哲思
今天是 10 月 24 日,也就是传说中的 “1024 程序员节”,这一天是属于全世界程序员的“狂欢日”。为什么是 1024 呢?因为 1024 是 2 的 10 次方,是计算机世界里最常见的数字之一——特别是在存储和数据的单位里,如 1KB = 1024 字节。它象征着二进制与数字化生活的核心。
程序员们每天的生活总是充满了奇思妙想、反复调试和对代码的无限热爱。所以,今天我们不聊崩溃的服务器、也不谈工期紧张的项目,来一点轻松幽默,借助我们熟悉的 PHP,用代码带来一些欢乐!
1. 程序员的内心独白
作为一个程序员,最常见的恐惧是什么?是生产环境里的“意外”。让我们看看如何在 PHP 中模拟这种恐惧的场景:
<?php
// 在生产环境执行的一段不该存在的代码
if (getenv('APP_ENV') === 'production') {
echo "啊!为什么这段代码会在生产环境运行?!\n";
} else {
echo "你还在开发环境,放轻松。\n";
}
?>
输出:
啊!为什么这段代码会在生产环境运行?!
程序员的噩梦就是这种了:忘记屏蔽的调试代码在上线后引发了一场“灾难”,手忙脚乱地撤回。还好我们有版本控制和备份。
2. 程序员的平凡一天
程序员的一天通常由无数的 bug
和 debug
组成。我们来看一段有趣的 PHP 代码,模拟一位程序员一天的工作流程:
<?php
$bugs = 10;
$debugs = 0;
$coffee = 1;
while ($bugs > 0) {
$debugs++;
echo "调试第 $debugs 次,修复了1个bug。\n";
$bugs--;
if ($debugs % 3 === 0) {
echo "喝杯咖啡提提神吧!\n";
$coffee++;
}
}
echo "所有bug修复完毕!你喝了 $coffee 杯咖啡。\n";
?>
输出:
调试第 1 次,修复了1个bug。
调试第 2 次,修复了1个bug。
调试第 3 次,修复了1个bug。
喝杯咖啡提提神吧!
调试第 4 次,修复了1个bug。
调试第 5 次,修复了1个bug。
调试第 6 次,修复了1个bug。
喝杯咖啡提提神吧!
调试第 7 次,修复了1个bug。
调试第 8 次,修复了1个bug。
调试第 9 次,修复了1个bug。
喝杯咖啡提提神吧!
调试第 10 次,修复了1个bug。
所有bug修复完毕!你喝了 4 杯咖啡。
这就是程序员的一天,似乎修复一个 bug 的代价往往是多喝一杯咖啡。喝杯咖啡是充满仪式感的事情,只有这样,程序员才能在层出不穷的 bug 中找到自我。
3. 程序员的经典吐槽
编程界有很多经典的笑话,最常见的就是 "It works on my machine",意思是代码在我的电脑上运行没问题,那为什么在其他地方总是崩溃呢?让我们用 PHP 来重现这个场景:
<?php
$environment = "development"; // 在开发环境完美运行
function itWorksOnMyMachine($env) {
if ($env === "production") {
throw new Exception("在生产环境中崩溃了!");
} else {
echo "在开发环境中运行良好!\n";
}
}
try {
itWorksOnMyMachine($environment);
} catch (Exception $e) {
echo $e->getMessage();
}
?>
输出:
在开发环境中运行良好!
然而当我们切换到生产环境时:
$environment = "production"; // 在生产环境突然崩溃
输出:
在生产环境中崩溃了!
这就是所谓的“本机没问题,但上线必然出问题”的尴尬。
4. 程序员的哲学思考
程序员常常思考这样的问题:究竟是我在调试代码,还是代码在调试我?这是每个开发者都要面对的编程哲学问题。我们用 PHP 代码来表达这种深刻的哲学思考:
<?php
function whoIsDebuggingWho($iterations) {
$you = "程序员";
$code = "代码";
for ($i = 0; $i < $iterations; $i++) {
if ($i % 2 == 0) {
echo "$you 在调试 $code\n";
} else {
echo "$code 在调试 $you\n";
}
}
}
whoIsDebuggingWho(5);
?>
输出:
程序员 在调试 代码
代码 在调试 程序员
程序员 在调试 代码
代码 在调试 程序员
程序员 在调试 代码
在调试的过程中,有时候会让你感觉代码有它自己的意志,总是在你修复一个 bug 的时候带来另一个问题,就像代码在调试你一样。
总结
在程序员节这天,致敬所有在键盘前日夜奋斗的开发者们,你们的努力创造了现代数字世界的奇迹。程序员的工作可能常常伴随着压力和挑战,但每一次解决问题的瞬间,都是无价的成就感。1024,不只是一个数字,它是程序员精神的象征:精简、高效、充满逻辑的美感。
祝大家 1024 节快乐,愿每行代码都顺利运行,每次编译都能一键通过!
来源:juejin.cn/post/7429349035893784630
研发团队没有战斗力,怎么解?
研发团队没有战斗力,怎么解?
在现代企业中,研发团队的战斗力是企业竞争力的重要组成部分,尤其是在技术驱动型的公司。
一个高效、有战斗力的研发团队不仅能快速适应市场变化,还能通过技术创新为企业创造更多的价值。那么,如何才能打造一个有战斗力的研发团队?
我们先界定问题,拆解问题,然后再看怎么系统化的去解。
1 界定问题
我们需要明确什么是「有战斗力的研发团队」,并清楚当前团队与理想状态之间的差距。
用我和我们家闺女常说的,当有人和你说一些事情的时候,需要看一下他说的「是一个观点还是一个事实」。「研发团队没有战斗力」,这明显是一个观点。基于这个观点,接下来我们要做的,就是去拆解这个观点背后的事实,并找到支撑这个观点的具体原因。
那事实有哪些呢?
1.1 任务完成效率低
团队的任务完成效率可以通过数据来衡量。如果团队频繁出现项目延期、任务积压,或者在完成某些任务时总是比预期时间拖延很多,这通常会被认为是研发团队没有足够战斗力的重要表现之一。这里的事实包括:
- 项目计划与实际进度的差距有多大?
- 每个任务的平均完成时间是否过长?
- 团队在解决问题时是否常常遇到瓶颈?
这些数据可以通过项目管理工具(如 Jira、Trello 等)来进行追踪和量化。一旦明确了当前的情况,我们就能更好地了解团队效率低下的具体原因。
1.2 沟通不畅
沟通问题是研发团队中非常常见的困扰之一。它可以通过以下事实来体现:
- 团队成员之间是否常常因为沟通不足而产生误解?
- 在跨部门协作中,是否有任务交接不清、信息传递不准确的情况?
- 是否存在因为沟通问题导致的工作重复或返工?
通过团队内部的回顾会议、跨部门的反馈等方式,可以明确沟通问题的具体表现和影响。沟通不畅往往会拖慢整体效率,降低团队的战斗力。
1.3 团队士气低落
士气低落是另一个常见的观点化描述,但它背后有很多具体的事实可以支撑:
- 团队成员是否主动承担任务,还是常常出现推诿现象?
- 团队的离职率是否高于行业平均水平?
- 团队成员是否经常表现出疲惫、倦怠,缺乏对工作的积极性?
如果团队中缺乏成就感、归属感,激励机制不到位,这些都会导致士气低落,进而影响团队的整体战斗力。通过员工满意度调查、绩效考核结果等数据,我们可以准确捕捉到士气低落的事实。
1.4 技术债务积累
「技术债务」经常会被忽视,但它实际上是研发团队战斗力不足的重要原因之一。以下事实可以帮助我们判断团队是否面临技术债务问题:
- 系统是否频繁出现 BUG,导致大量时间用于修复问题而非开发新功能?
- 是否有大量遗留的代码或架构问题,导致团队在进行新功能开发时效率低下?
- 系统的可维护性和可扩展性是否在不断下降?
技术债务的积累不仅会拖慢整个团队的开发进度,还可能让团队陷入“救火”而非创新的状态,这无疑是战斗力下降的一个重要体现。
1.5 质量问题严重
质量问题也是影响研发团队战斗力的一个重要因素,并且算是一种非常关键的事实表现。质量问题不仅影响产品的稳定性和用户体验,还会对团队的效率、士气和创新能力造成负面影响。在「研发团队没有战斗力」这一观点下,质量问题可以归结为以下几个具体事实:
- 有频繁的产品缺陷和返工,可以使用缺陷率、线上故障数、SLA 等指标来衡量
- 项目交付质量不达标,如功能不完整,性能问题,用户反馈差等
- 缺乏严格的代码审查和质量控制流程
1.6 工程化和系统化问题
「工程化和系统化问题」是影响研发团队战斗力的重要因素之一,尤其是在团队规模扩大、项目复杂性增加的情况下。工程化和系统化不足通常会导致团队的开发流程混乱、效率低下、交付质量不稳定、可扩展性差,甚至会影响团队的整体协作能力和长期发展。其主要体现在如下几个方面:
- 缺乏标准化流程
- 自动化程度不足,缺乏自动化测试,手动操作的事项较多,重复劳动多
- 系统化不足,缺乏整体架构设计,模块耦合度高或者扩展性差
1.7 人才梯队问题
人才梯队是指团队中不同层级的人才储备和发展体系。如果团队中缺乏明确的人才梯队,意味着团队内部没有清晰的发展路径,成员的技能水平参差不齐,导致团队的整体战斗力不足。以下是一些具体的事实表现:
- 缺乏明确的晋升机制:团队中没有明确的晋升机制和路径,导致优秀的员工看不到职业发展前景,逐渐失去动力。
- 关键人员依赖严重:团队中的某些核心人员承担了过多的技术关键任务,一旦这些人离职或出问题,整个项目或团队都会陷入停滞。
- 缺乏接班人:当团队中的高层或资深技术人员调岗或离职时,缺乏能够快速接替其工作的接班人,导致项目推进或技术维护出现断档。
这些现象说明团队在人才梯队建设上存在严重不足,导致团队的持续作战能力和抗风险能力较差。
1.8 人才密度问题
人才密度指的是团队中高水平技术人才的比例。如果团队的人才密度不足,即高水平人才较少,团队整体的战斗力自然会大打折扣。以下是一些具体的事实表现:
- 技术水平不均衡:团队中技术能力强的人数较少,大多数成员的技术能力不足以支撑复杂的项目开发,导致高水平的成员承担了大部分工作,而低水平的成员拉低了整体效率。
- 问题解决能力差:团队整体在面对复杂问题时,解决问题的能力不足,往往需要依赖外部资源或高层决策,无法自主高效地解决技术难题。
- 技术创新动力不足:由于缺乏高水平人才的引领,团队内部的技术创新能力较弱,难以提出具有前瞻性的技术方案。
人才密度直接影响到团队的技术创新和问题解决能力,因此提升人才密度是打造高战斗力团队的关键。
2 分解问题
在明确了研发团队战斗力不足的主要表现后,我们需要进一步分解问题,以便逐步分析并找到解决方案。根据 MECE 的原则,可以将战斗力不足的问题分解为下列几个方面:
2.1 效率问题
效率是衡量研发团队战斗力的最直接指标之一。如果团队的任务完成效率低下,项目延期频繁,势必会影响整体战斗力。这一问题可以分为以下几个子问题:
- 流程不清晰:团队的开发流程、测试流程、发布流程是否标准化?是否有明确的职责划分和操作步骤?
- 工具使用不当:项目管理工具、代码管理工具、自动化工具是否充分使用?是否存在大量的手动操作和重复劳动?
- 不合理的资源分配:团队成员的任务分配是否合理?是否存在某些成员工作过载,而其他成员任务量不足的情况?
- 瓶颈无法突破:团队在某些技术领域或开发阶段是否经常遇到瓶颈,导致任务卡住?
2.2 沟通协作问题
沟通不畅往往是导致研发团队效率低下和战斗力不足的主要原因之一。沟通问题可以进一步分解为:
- 跨部门沟通障碍:研发团队和其他部门(如产品、运营、市场等)之间的沟通是否频繁出现误解或信息不对称?
- 内部沟通不畅:团队内部成员之间是否缺乏有效的沟通渠道?是否存在信息流动不畅或不透明的情况?
- 技术与业务脱节:研发团队是否充分理解业务需求?技术方案是否能够及时响应业务的变化?
2.3 士气和激励问题
研发团队的士气低落通常是由激励机制不合理、工作压力过大或缺乏成就感引起的。这个问题可以进一步分解为:
- 激励机制不健全:绩效考核、薪资、奖金等激励机制是否能够有效激励员工?团队中是否存在“吃大锅饭”的问题,导致优秀员工失去动力?
- 成就感缺失:团队成员是否能感受到工作的意义?是否有足够的成就感和归属感?
- 工作倦怠:团队成员是否长期处于高压、加班的状态,导致出现工作倦怠?
2.4 技术债务与质量问题
技术债务和质量问题会严重影响团队的战斗力,因为它们导致团队需要花费大量时间在修复错误和维护上,而不是开发新功能或创新。技术债务和质量问题的细分包括:
- 代码质量差:团队是否有严格的代码评审流程?代码是否有良好的可读性、可维护性?
- 技术债务积累:系统中是否存在大量的历史遗留问题(如未重构的老旧代码、架构问题等),导致维护成本高、开发效率低?
- 缺乏自动化测试:团队是否有足够的自动化测试覆盖?是否依赖大量的手工测试,增加了测试和发布的成本?
2.5 人才梯队建设不足
人才梯队建设不足意味着团队缺乏不同层次的人才储备,导致团队的整体战斗力和可持续发展能力受限。具体问题包括:
- 晋升机制不明确:是否有清晰的晋升机制和职业发展通道?员工是否知道如何通过努力获得晋升或更多的成长机会?
- 接班人缺失:是否有计划培养接班人,确保每个关键岗位都有后备力量?
- 关键依赖严重:团队是否过度依赖某些核心人员,一旦这些人离职或请假,项目进展是否会受到严重影响?
2.6 人才密度不够
人才密度不够会导致团队在面对复杂技术问题时缺乏足够的解决能力,团队的技术创新能力也会因此受到影响。这个问题可以进一步分解为:
- 招不到合适的人:招聘过程是否存在瓶颈,导致无法及时引入高水平的技术人才?
- 人才培养不足:是否有系统的内部培训机制,帮助团队成员提升技术水平?
- 技术水平参差不齐:团队成员的技术能力是否存在较大的差异,导致整体效率不高?
2.7 工程化和系统化不足
工程化和系统化不足会导致团队效率低下、交付质量不稳定,无法应对复杂的项目需求。具体问题包括:
- 开发流程不标准:是否有统一的开发、测试、发布流程?是否存在大量的手动操作?
- 自动化程度不够:系统的开发、测试、部署等环节是否充分利用了自动化工具?是否存在大量重复的手工劳动?
- 架构设计不合理:系统的架构设计是否能够支持业务的扩展和未来的发展需求?是否存在模块耦合度过高、扩展性差等问题?
3 体系化的解决问题
解决研发团队没有战斗力的问题,是一个多维度、跨职能的系统性工程。它涉及到组织文化、组织结构、技术架构、流程设计、工程系统和度量考核等多个方面。每个维度的优化和提升都能够为研发团队带来战斗力的增强,但这些维度并非孤立存在,而是相互关联、彼此支撑的。
我们需要明确的是,研发团队战斗力的提升不仅仅是为了提高「速度」,更是为了提高「质量」和「价值」,即更高效地交付更优质的产品,满足业务需求,并为公司创造长期的价值。
3.1 组织文化和沟通机制构建
组织文化是企业的灵魂,它直接影响员工的行为和思维方式。一个以创新和协作为核心的组织文化能激发员工的创造力,鼓励他们尝试新方法和新技术,并在失败中学习和改进。文化的塑造对研发效能提升而言,是打下「地基」的工作。
如何构建?
- 建立跨部门沟通机制:通过定期的跨部门会议或项目复盘,确保技术、产品、业务等不同职能部门之间的沟通顺畅。可以采用 OKR 或双向沟通机制,让各部门了解彼此的目标和进展,减少信息孤岛。
- 鼓励知识共享:定期组织 技术分享会、内部培训,以及设立 技术博客 或 Wiki,这样可以促进技术积累和知识在团队内的流动。还可以通过内部的 导师制,帮助新员工快速融入团队。
- 认可和激励创新:设立相应的 奖项 或 肯定机制,对提出创新方案或成功实施新技术的员工进行公开表扬和奖励。比如可以设立 季度创新奖,以鼓励员工在日常工作中不断试验和改进。
- 领导层的共识:研发负责人应确保与高层管理者达成一致,使研发效能提升工作得到高层支持。领导层的共识会帮助在资源分配、目标设定、团队管理等层面为研发效能的提升提供保障。
我们可以进行如下的一些具体的操作:
- 定期组织 跨部门的需求讨论会 或 研发复盘会,确保各个部门的需求和反馈能够及时传递。
- 设立 激励计划,对优秀的创新项目和技术方案进行奖励。
- 通过 员工满意度调查 或 一对一访谈,了解员工对现有文化的看法,并持续改进。
3.2 调整组织结构
组织结构决定了信息的流动、资源的分配以及决策的效率。一个灵活的、扁平化的组织结构能够促进创新,加速决策过程,同时减少层级间的沟通障碍。通过合理的组织结构设计,可以让团队在面对复杂问题时具备更强的反应能力。
组织结构的调整需要根据实际的团队情况以及业务情况来做优化,是职能型,还是项目型,还是矩阵型等等,可以有如下的一些参考思路:
- 小型化、自治化的团队:采用 跨职能团队 的形式,促进团队成员之间的紧密合作。每个团队都拥有相对独立的决策权,能够快速响应业务需求。采用 Spotify 模式 或 Scrum 团队 的形式,打破职能部门壁垒,形成更快速决策和执行的团队。
- 灵活的项目管理机制:引入 动态人员管理 和 内部创业机制,让团队能够根据项目的需求灵活调整人员和资源配置。通过设立 内部孵化器,让员工能够在公司内部尝试新的项目和解决方案。
- 减少管理层级:通过扁平化管理,减少中间层级的沟通障碍,形成更直接的反馈机制。管理者应该更多地起到 协调者 和 支持者 的作用,而不是微观管理。
在实际操作过程中,我们可以:
- 设立多个 跨职能团队,每个团队独立负责某个产品或项目的端到端交付。
- 引入 OKR 管理机制,确保各个团队的目标与公司整体战略保持一致,并且团队间可以灵活协作。
- 定期进行 组织结构评估,根据业务需求和人员成长情况灵活调整团队架构。
3.3 评估并调整技术架构
技术架构的合理性直接影响团队的研发效率。如果架构设计不合理,团队的开发成本会持续增加,迭代速度会变慢,系统的稳定性和可扩展性也会下降。通过合理的架构设计,可以让团队更高效地应对变化和扩展需求。
以下为一些评估和调整的思路或原则:
- 模块化、低耦合的架构设计:在架构设计中,遵循 高内聚、低耦合 的原则,确保系统模块之间的依赖性降到最低,便于独立开发和部署。采用 微服务架构 或 服务化架构,将系统拆分为相对独立的服务,确保每个模块可以独立扩展和维护。这虽然是老生常谈,但是很少有组织做得很好。且这里需要根据实际的业务需要和当前架构形态来决策。
- 云原生架构:通过云原生架构,使用 Docker、Kubernetes 等容器化和编排技术,实现系统的一致性和可移植性,支持快速部署和环境隔离。
- 灵活的技术栈:根据业务需求选择合适的技术栈,而不是盲目追求技术潮流。技术选择要与团队的技术能力和业务发展阶段相匹配。
- DevOps 和 CI/CD 实践:通过持续集成和持续交付(CI/CD)来加速产品发布,减少人工操作的错误,提升发布频率和质量。
具体操作过程中,我们可以:
- 进行 架构评审,定期对系统的技术架构进行审查,确保架构能够支持当前和未来的业务发展。
- 引入 DevOps 实践,通过自动化工具(如 Jenkins、GitLab CI 等)实现持续集成和交付。
- 采用 微服务架构 进行系统划分,确保各个服务可以独立开发、测试和部署。
3.4 优化研发流程
研发流程设计是确保研发活动高效进行的关键。良好的流程设计可以减少非必要的工作,清晰定义各个阶段的输入、输出和质量标准。同时,优秀的流程设计能帮助团队在每个环节上减少浪费,提升整体效率。
以下为常用的一些优化思路:
- 引入敏捷开发方法:采用 Scrum 或 Kanban 等敏捷开发方法,确保团队能够快速响应需求变化,并通过短周期迭代逐步交付产品。不能为了敏捷而敏捷,根据当前团队情况来实施。
- 精益开发思想:通过 精益思想(Lean),消除流程中的浪费,减少不增值的工作。例如,减少不必要的会议、文档、审批流程,提升团队专注于高价值任务的时间。
- 自动化流程:通过引入自动化工具,简化开发、测试和发布流程,减少手工操作和人为错误。比如自动化代码检查、自动化测试、自动化部署等。
- 数据驱动的流程优化:通过 数据分析工具(如 Jira、SonarQube 等)监控流程中的瓶颈点和低效环节,并持续优化流程。
实际操作过程中可以通过以下的方式来做一些落地的操作:
- 定期进行 流程审查会议,分析当前流程中的低效环节和瓶颈,提出改进方案。
- 采用 需求交付周期 和 需求吞吐量 等指标,衡量每个迭代的效率,并根据数据优化流程。
- 使用 自动化工具 完成代码检查、测试和部署,减少人工干预。
3.5 优化工程系统
工程系统是研发效能提升的基础设施。包括代码管理、构建、测试、部署等一系列工程实践。通过系统化的工具和方法,可以减少重复性工作,提升研发的效率和稳定性。
工程系统如何优化?
- 统一的开发环境:建立统一的开发环境和工具链,确保团队成员在同一套标准下工作,降低环境差异带来的问题。采用 Docker 等容器化技术,确保本地开发环境与生产环境的一致性。
- 自动化测试平台:通过自动化测试平台(如 Selenium、JUnit、TestNG 等),实现单元测试、集成测试、回归测试的自动化,提高产品质量,减少人工测试的负担。
- 版本控制系统:采用 Git 等版本控制系统,建立合理的分支管理策略(如 GitFlow),确保代码的安全性和可追溯性。
- 监控和日志分析系统:引入 监控工具(如 Prometheus、Grafana)和 日志分析工具(如 ELK Stack),确保系统的运行状况可视化,尽早发现问题并采取措施。
在实际操作过程中我们可以:
- 建立统一的 Docker 镜像仓库,确保开发和生产使用相同的基础环境。
- 使用 持续集成工具(如 Jenkins)进行代码的自动化构建和测试。
- 设立 监控和报警机制,确保系统的健康状况能够被实时监控。
3.6 构建度量考核
度量考核是研发效能提升的反馈机制。它为团队提供了衡量成果和改进的依据,帮助团队识别问题、跟踪进度,并调整优化策略。没有量化的度量,研发效能的提升就缺乏方向和依据。
同时,度量可以让战斗力这个概念可视化出来,更明确什么是有战斗力,什么是没有战斗力。
我们可以用如下的方式落地:
- 建立科学的度量体系:用一套符合团队实际情况的指标体系来衡量效能,覆盖项目进度、产品质量、团队效率等方面。常见的度量指标包括 需求交付周期、缺陷率、代码覆盖率、部署频率 等。
- 定期审视数据:定期对这些指标进行审查,分析趋势和异常,找出影响效能的主要原因,并制定改进措施。
- 将度量结果与激励机制挂钩:通过绩效考核,确保团队成员的贡献能够被量化和认可,并通过奖励机制激励团队不断提升效能。
实际操作:
- 建立 研发效能仪表盘,实时监控团队的效能指标。
- 每月定期召开 效能回顾会议,根据数据分析报告,制定下一步的改进计划。
- 将 研发效能指标 纳入团队的 OKR 或绩效考核体系,确保团队成员的目标与效能提升保持一致。
4 小结
提升研发团队的战斗力是一个体系化、系统化的工程,涉及到组织文化、组织结构、技术架构、流程设计、工程系统和度量考核等多个层面。通过在这些维度上进行持续优化,可以显著增强研发团队的战斗力,提升产品交付的速度、质量和创新能力。
如果要真正的解决研发团队没有战斗力的问题,在上面界定问题、分析问题和解决问题的基础上,还需要有如下的一些操作和逻辑:
- 建立目标和成功判断
- 制定详细的解决方案
- 设定里程碑
- 制定详细的工作计划
- 风险判断和未来改进
只有完整落地详细的工作计划,完成里程碑,一步一个脚印,才能真正的打造出有战斗力的研发团队。
每个企业的实际情况不同,因此在执行时需要根据具体场景进行灵活调整。最终目标是帮助研发团队在高速变化的市场环境中,更高效、更稳定地交付高质量的产品,创造更大的商业价值。
来源:juejin.cn/post/7430058214982926386
听我一句劝,业务代码中,别用多线程。
你好呀,我是歪歪。
前几天我在网上冲浪,看到一个哥们在吐槽,说他工作三年多了,没使用过多线程。
虽然八股文背的滚瓜烂熟,但是没有在实际开发过程中写的都是业务代码,没有使用过线程池,心里还是慌得一比。
我只是微微一笑,这不是很正常吗?
业务代码中一般也使不上多线程,或者说,业务代码中不知不觉你以及在使用线程池了,你再 duang 的一下搞一个出来,反而容易出事。
所以提到线程池的时候,我个人的观点是必须把它吃得透透的,但是在业务代码中少用或者不用多线程。
关于这个观点,我给你盘一下。
Demo
首先我们还是花五分钟搭个 Demo 出来。
我手边刚好有一个之前搭的一个关于 Dubbo 的 Demo,消费者、生产者都有,我就直接拿来用了:
这个 Demo 我也是跟着网上的 quick start 搞的:
cn.dubbo.apache.org/zh-cn/overv…
可以说写的非常详细了,你就跟着官网的步骤一步步的搞就行了。
我这个 Demo 稍微不一样的是我在消费者模块里面搞了一个 Http 接口:
在接口里面发起了 RPC 调用,模拟从前端页面发起请求的场景,更加符合我们的开发习惯。
而官方的示例中,是基于了 SpringBoot 的 CommandLineRunner 去发起调用:
只是发起调用的方式不一样而已,其他没啥大区别。
需要说明的是,我只是手边刚好有一个 Dubbo 的 Demo,随手就拿来用了,但是本文想要表达的观点,和你使不使用 Dubbo 作为 RPC 框架,没有什么关系,道理是通用的。
上面这个 Demo 启动起来之后,通过 Http 接口发起一次调用,看到控制台服务提供方和服务消费方都有对应的日志输出,准备工作就算是齐活儿了:
上菜
在上面的 Demo 中,这是消费者的代码:
这是提供者的代码:
整个调用链路非常的清晰:
来,请你告诉我这里面有线程池吗?
没有!
是的,在日常的开发中,我就是写个接口给别人调用嘛,在我的接口里面并没有线程池相关的代码,只有 CRUD 相关的业务代码。
同时,在日常的开发中,我也经常调用别人提供给我的接口,也是一把梭,撸到底,根本就不会用到线程池。
所以,站在我,一个开发人员的角度,这个里面没有线程池。
合理,非常合理。
但是,当我们换个角度,再看看,它也是可以有的。
比如这样:
反应过来没有?
我们发起一个 Http 调用,是由一个 web 容器来处理这个请求的,你甭管它是 Tomcat,还是 Jetty、Netty、Undertow 这些玩意,反正是个 web 容器在处理。
那你说,这个里面有线程池吗?
在方法入口处打个断点,这个 http-nio-8081-exec-1 不就是 Tomcat 容器线程池里面的一个线程吗:
通过 dump 堆栈信息,过滤关键字可以看到这样的线程,在服务启动起来,啥也没干的情况下,一共有 10 个:
朋友,这不就是线程池吗?
虽然不是你写的,但是你确实用了。
我写出来的这个 test 接口,就是会由 web 容器中的一个线程来进行调用。所以,站在 web 容器的角度,这里是有一个线程池的:
同理,在 RPC 框架中,不管是消费方,还是服务提供方,也都存在着线程池。
比如 Dubbo 的线程池,你可以看一下官方的文档:
cn.dubbo.apache.org/zh-cn/overv…
而对于大多数的框架来说,它绝不可能只有一个线程池,为了做资源隔离,它会启用好几个线程池,达到线程池隔离,互不干扰的效果。
比如参与 Dubbo 一次调用的其实不仅一个线程池,至少还有 IO 线程池和业务线程池,它们各司其职:
我们主要关注这个业务线程池。
反正站在 Dubbo 框架的角度,又可以补充一下这个图片了:
那么问题来了,在当前的这个情况下?
当有人反馈:哎呀,这个服务吞吐量怎么上不去啊?
你怎么办?
你会 duang 的一下在业务逻辑里面加一个线程池吗?
大哥,前面有个 web 容器的线程池,后面有个框架的线程池,两头不调整,你在中间加个线程池,加它有啥用啊?
web 容器,拿 Tomcat 来说,人家给你提供了线程池参数调整的相关配置,这么一大坨配置,你得用起来啊:
tomcat.apache.org/tomcat-9.0-…
再比如 Dubbo 框架,都给你明说了,这些参数属于性能调优的范畴,感觉不对劲了,你先动手调调啊:
你把这些参数调优弄好了,绝对比你直接怼个线程池在业务代码中,效果好的多。
甚至,你在业务代码中加入一个线程池之后,反而会被“反噬”。
比如,你 duang 的一下怼个线程池在这里,我们先只看 web 容器和业务代码对应的部分:
由于你的业务代码中有线程池的存在,所以当接受到一个 web 请求之后,立马就把请求转发到了业务线程池中,由线程池中的线程来处理本次请求,从而释放了 web 请求对应的线程,该线程又可以里面去处理其他请求。
这样来看,你的吞吐量确实上去了。
在前端来看,非常的 nice,请求立马得到了响应。
但是,你考虑过下游吗?
你的吞吐量上涨了,下游同一时间处理的请求就变多了。如果下游跟不上处理,顶不住了,直接就是崩给你看怎么办?
而且下游不只是你一个调用方,由于你调用的太猛,导致其他调用方的请求响应不过来,是会引起连锁反应的。
所以,这种场景下,为了异步怼个线程池放着,我觉得还不如用消息队列来实现异步化,顶天了也就是消息堆积嘛,总比服务崩了好,这样更加稳妥。
或者至少和下游勾兑一下,问问我们这边吞吐量上升,你们扛得住不。
有的小伙伴看到这里可能就会产生一个疑问了:歪师傅,你这个讲得怎么和我背的八股文不一样啊?
巧了,你背过的八股文我也背过,现在我们来温习一下我们背过的八股文。
什么时候使用线程池呢?
比如一个请求要经过若干个服务获取数据,且这些数据没有先后依赖,最终需要把这些数据组合起来,一并返回,这样经典的场景:
用户点商品详情,你要等半天才展示给用户,那用户肯定骂骂咧咧的久走了。
这个时候,八股文上是怎么说的:用线程池来把串行的动作改成并行。
这个场景也是增加了服务 A 的吞吐量,但是用线程池就是非常正确的,没有任何毛病。
但是你想想,我们最开始的这个案例,是这个场景吗?
我们最开始的案例是想要在业务逻辑中增加一个线程池,对着一个下游服务就是一顿猛攻,不是所谓的串行改并行,而是用更多的线程,带来更多的串行。
这已经不是一个概念了。
还有一种场景下,使用线程池也是合理的。
比如你有一个定时任务,要从数据库中捞出状态为初始化的数据,然后去调用另外一个服务的接口查询数据的最终状态。
如果你的业务代码是这样的:
//获取订单状态为初始化的数据(0:初始化 1:处理中 2:成功 3:失败)
//select * from order where order_status=0;
ArrayList initOrderInfoList = queryInitOrderInfoList();
//循环处理这批数据
for(OrderInfo orderInfo : initOrderInfoList){
//捕获异常以免一条数据错误导致循环结束
try{
//发起rpc调用
String orderStatus = queryOrderStatus(orderInfo.getOrderId);
//更新订单状态
updateOrderInfo(orderInfo.getOrderId,orderStatus);
} catch (Exception e){
//打印异常
}
}
虽然你框架中使用了线程池,但是你就是在一个 for 循环中不停的去调用下游服务查询数据状态,是一条数据一条数据的进行处理,所以其实同一时间,只是使用了框架的线程池中的一个线程。
为了更加快速的处理完这批数据,这个时候,你就可以怼一个线程池放在 for 循环里面了:
//循环处理这批数据
for(OrderInfo orderInfo : initOrderInfoList){
//使用线程池
executor.execute(() -> {
//捕获异常以免一条数据错误导致循环结束
try {
//发起rpc调用
String orderStatus = queryOrderStatus(orderInfo.getOrderId);
//更新订单状态
updateOrderInfo(orderInfo.getOrderId, orderStatus);
} catch (Exception e) {
//打印异常
}
});
}
需要注意的是,这个线程池的参数怎么去合理的设置,是需要考虑的事情。
同时这个线程池的定位,就类似于 web 容器线程池的定位。
或者这样对比起来看更加清晰一点:
定时任务触发的时候,在发起远程接口调用之前,没有线程池,所以我们可以启用一个线程池来加快数据的处理。
而 Http 调用或者 RPC 调用,框架中本来就已经有一个线程池了,而且也给你提供了对应的性能调优参数配置,那么首先考虑的应该是把这个线程池充分利用起来。
如果仅仅是因为异步化之后可以提升服务响应速度,没有达到串行改并行的效果,那么我更加建议使用消息队列。
好了,本文的技术部分就到这里啦。
下面这个环节叫做[荒腔走板],技术文章后面我偶尔会记录、分享点生活相关的事情,和技术毫无关系。我知道看起来很突兀,但是我喜欢,因为这是一个普通博主的生活气息。
荒腔走板
不知道你看完文章之后,有没有产生一个小疑问:最开始部分的 Demo 似乎用处并不大?
是的,我最开始构思的行文结构是是基于 Demo 在源码中找到关于线程池的部分,从而引出其实有一些我们“看不见的线程池”的存在的。
原本周六我是有一整天的时间来写这篇文章,甚至周五晚上还特意把 Demo 搞定,自己调试了一番,该打的断点全部打上,并写完 Demo 那部分之后,我才去睡觉的,想得是第二天早上起来直接就能用。
按照惯例周六睡个懒觉的,早上 11 点才起床,自己慢条斯理的做了一顿午饭,吃完饭已经是下午 1 点多了。
本来想着在沙发上躺一会,结果一躺就是一整个下午。期间也想过起来写一会文章,坐在电脑前又飞快的躺回到沙发上,就是觉得这个事情索然无味,当下的那一刻就想躺着,然后无意识的刷手机,原本是拿来写文章中关于源码的部分的时间就这样浪费了。
像极了高中时的我,周末带大量作业回家,准备来个悬梁刺股,弯道超车,结果变成了一睡一天,捏紧刹车。
高中的时候,时间浪费了是真的可惜。
现在,不一样了。
荒腔走板这张图片,就是我躺在沙发上的时候,别人问我在干什么时随手拍的一张。
我并不为躺了一下午没有干正事而感到惭愧,浪费了的时间,才是属于自己的时间。
很久以前我看到别人在做一些浪费时间的事情的时候,我心里可能会嘀咕几句,劝人惜时。
这两年我不会了,允许自己做自己,允许别人做别人。
来源:juejin.cn/post/7297980721590272040
都说PHP性能差,但PHP性能真的差吗?
今天本能是想测试一个PDO持久化,会不会带来会话混乱的问题
先贴一下PHP代码, 代码丑了点,但是坚持能run就行,反正就是做个测试。
<?php
$dsn = 'mysql:host=localhost;dbname=test;charset=utf8';
$user = 'root';
$password = 'root';
// 设置 PDO 选项,启用持久化连接
$options = [
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
];
try {
// 创建持久化连接
$pdo = new PDO($dsn, $user, $password, $options);
$stmt = $pdo->prepare("INSERT INTO test_last_insert_id (uni) VALUES (:uni);");
$uni = uniqid('', true);
$stmt->bindValue(':uni', $uni);
$aff = $stmt->execute(); //
if ($aff === false) {
throw new Exception("insert fail:");
}
$id = $pdo->lastInsertId();
function getExecutedSql($stmt, $params)
{
$sql = $stmt->queryString;
$keys = array();
$values = array();
// 替换命名占位符 :key with ?
$sql = preg_replace('/\:(\w+)/', '?', $sql);
// 绑定的参数可能包括命名占位符,我们需要将它们转换为匿名占位符
foreach ($params as $key => $value) {
$keys[] = '/\?/';
$values[] = is_string($value) ? "'$value'" : $value;
}
// 替换占位符为实际参数
$sql = preg_replace($keys, $values, $sql, 1, $count);
return $sql;
}
$stmt = $pdo->query("SELECT id FROM test_last_insert_id WHERE uni = '{$uni}'", PDO::FETCH_NUM);
$row = $stmt->fetch();
$value = $row[0];
if ($value != $id) {
throw new Exception("id is diff");
}
echo "success" . PHP_EOL;
} catch (PDOException $e) {
header('HTTP/1.1 500 Internal Server Error');
file_put_contents('pdo_perisistent.log', $e->getMessage() . PHP_EOL);
die('Database connection failed: ' . $e->getMessage());
} catch (Exception $e) {
header('HTTP/1.1 500 Internal Server Error');
file_put_contents('pdo_perisistent.log', $e->getMessage() . PHP_EOL);
die('Exception: ' . $e->getMessage());
}
用wrk压测,一开始uniqid因为少了混淆参数还报了500,加了一下参数,用来保证uni值
% ./wrk -c100 -t2 -d3s --latency "http://localhost/pdo_perisistent.php"
Running 3s test @ http://localhost/pdo_perisistent.php
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 52.17ms 7.48ms 103.38ms 80.57%
Req/Sec 0.96k 133.22 1.25k 75.81%
Latency Distribution
50% 51.06ms
75% 54.17ms
90% 59.45ms
99% 80.54ms
5904 requests in 3.10s, 1.20MB read
Requests/sec: 1901.92
Transfer/sec: 397.47KB
1900 ~ 2600 之间的QPS,其实这个数值还是相当满意的,测试会话会不会混乱的问题也算完结了。
但是好奇心突起,之前一直没做过go和php执行sql下的对比,正好做一次对比压测
package main
import (
"database/sql"
"fmt"
"net/http"
"sync/atomic"
"time"
_ "github.com/go-sql-driver/mysql"
"log"
)
var id int64 = time.Now().Unix() * 1000000
func generateUniqueID() int64 {
return atomic.AddInt64(&id, 1)
}
func main() {
dsn := "root:root@tcp(localhost:3306)/test?charset=utf8"
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("Error opening database: %v", err)
}
defer func() { _ = db.Close() }()
//// 设置连接池参数
//db.SetMaxOpenConns(100) // 最大打开连接数
//db.SetMaxIdleConns(10) // 最大空闲连接数
//db.SetConnMaxLifetime(time.Hour) // 连接最大存活时间
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
var err error
uni := generateUniqueID()
// Insert unique ID int0 the database
insertQuery := `INSERT INTO test_last_insert_id (uni) VALUES (?)`
result, err := db.Exec(insertQuery, uni)
if err != nil {
log.Fatalf("Error inserting data: %v", err)
}
lastInsertID, err := result.LastInsertId()
if err != nil {
log.Fatalf("Error getting last insert ID: %v", err)
}
// Verify the last insert ID
selectQuery := `SELECT id FROM test_last_insert_id WHERE uni = ?`
var id int64
err = db.QueryRow(selectQuery, uni).Scan(&id)
if err != nil {
log.Fatalf("Error selecting data: %v", err)
}
if id != lastInsertID {
log.Fatalf("ID mismatch: %d != %d", id, lastInsertID)
}
fmt.Println("success")
})
_ = http.ListenAndServe(":8080", nil)
}
truncate表压测结果,这低于预期了吧
% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 54.05ms 36.86ms 308.57ms 80.77%
Req/Sec 0.98k 243.01 1.38k 63.33%
Latency Distribution
50% 43.70ms
75% 65.42ms
90% 99.63ms
99% 190.18ms
5873 requests in 3.01s, 430.15KB read
Requests/sec: 1954.08
Transfer/sec: 143.12KB
开个连接池,清表再测,结果半斤八两
% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 54.07ms 35.87ms 281.38ms 79.84%
Req/Sec 0.97k 223.41 1.40k 60.00%
Latency Distribution
50% 44.91ms
75% 66.19ms
90% 99.65ms
99% 184.51ms
5818 requests in 3.01s, 426.12KB read
Requests/sec: 1934.39
Transfer/sec: 141.68KB
然后开启不清表的情况下,php和go的交叉压测
% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 52.51ms 43.28ms 436.00ms 86.91%
Req/Sec 1.08k 284.67 1.65k 65.00%
Latency Distribution
50% 40.22ms
75% 62.10ms
90% 102.52ms
99% 233.98ms
6439 requests in 3.01s, 471.61KB read
Requests/sec: 2141.12
Transfer/sec: 156.82KB
% ./wrk -c100 -t2 -d3s --latency "http://localhost/pdo_perisistent.php"
Running 3s test @ http://localhost/pdo_perisistent.php
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 41.41ms 10.44ms 77.04ms 78.07%
Req/Sec 1.21k 300.99 2.41k 73.77%
Latency Distribution
50% 38.91ms
75% 47.62ms
90% 57.38ms
99% 69.84ms
7332 requests in 3.10s, 1.50MB read
Requests/sec: 2363.74
Transfer/sec: 493.98KB
// 这里骤降是我很不理解的不明白是因为什么
% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 156.72ms 75.48ms 443.98ms 66.10%
Req/Sec 317.93 84.45 480.00 71.67%
Latency Distribution
50% 155.21ms
75% 206.36ms
90% 254.32ms
99% 336.07ms
1902 requests in 3.01s, 139.31KB read
Requests/sec: 631.86
Transfer/sec: 46.28KB
% ./wrk -c100 -t2 -d3s --latency "http://localhost/pdo_perisistent.php"
Running 3s test @ http://localhost/pdo_perisistent.php
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 43.47ms 10.04ms 111.41ms 90.21%
Req/Sec 1.15k 210.61 1.47k 72.58%
Latency Distribution
50% 41.17ms
75% 46.89ms
90% 51.27ms
99% 95.07ms
7122 requests in 3.10s, 1.45MB read
Requests/sec: 2296.19
Transfer/sec: 479.87KB
% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 269.08ms 112.17ms 685.29ms 73.69%
Req/Sec 168.22 125.46 520.00 79.59%
Latency Distribution
50% 286.58ms
75% 335.40ms
90% 372.61ms
99% 555.80ms
1099 requests in 3.02s, 80.49KB read
Requests/sec: 363.74
Transfer/sec: 26.64KB
% ./wrk -c100 -t2 -d3s --latency "http://localhost/pdo_perisistent.php"
Running 3s test @ http://localhost/pdo_perisistent.php
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 41.74ms 9.67ms 105.86ms 91.72%
Req/Sec 1.20k 260.04 2.24k 80.33%
Latency Distribution
50% 38.86ms
75% 46.77ms
90% 49.02ms
99% 83.01ms
7283 requests in 3.10s, 1.49MB read
Requests/sec: 2348.07
Transfer/sec: 490.71KB
% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 464.85ms 164.66ms 1.06s 71.97%
Req/Sec 104.18 60.01 237.00 63.16%
Latency Distribution
50% 467.00ms
75% 560.54ms
90% 660.70ms
99% 889.86ms
605 requests in 3.01s, 44.31KB read
Requests/sec: 200.73
Transfer/sec: 14.70KB
% ./wrk -c100 -t2 -d3s --latency "http://localhost/pdo_perisistent.php"
Running 3s test @ http://localhost/pdo_perisistent.php
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 50.62ms 9.16ms 85.08ms 75.74%
Req/Sec 0.98k 170.66 1.30k 69.35%
Latency Distribution
50% 47.93ms
75% 57.20ms
90% 61.76ms
99% 79.90ms
6075 requests in 3.10s, 1.24MB read
Requests/sec: 1957.70
Transfer/sec: 409.13KB
% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 568.84ms 160.91ms 1.04s 66.38%
Req/Sec 81.89 57.59 262.00 67.27%
Latency Distribution
50% 578.70ms
75% 685.85ms
90% 766.72ms
99% 889.39ms
458 requests in 3.01s, 33.54KB read
Requests/sec: 151.91
Transfer/sec: 11.13KB
go 的代码随着不断的测试,很明显处理速度在不断的下降,这说实话有点超出我的认知了。
PHP那边却是基本稳定的,go其实一开始我还用gin测试过,发现测试结果有点超出预料,还改了用http库来测试,这结果属实差强人意了。
突然明白之前经常看到别人在争论性能问题的时候,为什么总有人强调PHP性能并不差。
或许PHP因为fpm的关系导致每次加载大量文件导致的响应相对较慢,比如框架laravel 那个QPS只有一两百的家伙,但其实这个问题要解决也是可以解决的,也用常驻内存的方式就好了。再不行还有phalcon
我一直很好奇一直说PHP性能问题的到底是哪些人, 不会是从PHP转到其他语言的吧。
% php -v
PHP 8.3.12 (cli) (built: Sep 24 2024 18:08:04) (NTS)
Copyright (c) The PHP Gr0up
Zend Engine v4.3.12, Copyright (c) Zend Technologies
with Xdebug v3.3.2, Copyright (c) 2002-2024, by Derick Rethans
with Zend OPcache v8.3.12, Copyright (c), by Zend Technologies
% go version
go version go1.23.1 darwin/amd64
这结果,其实不太能接受,甚至都不知道原因出在哪了,有大佬可以指出问题一下吗
加一下时间打印再看看哪里的问题
package main
import (
"database/sql"
"fmt"
"net/http"
"sync/atomic"
"time"
_ "github.com/go-sql-driver/mysql"
"log"
)
var id int64 = time.Now().Unix() * 1000000
func generateUniqueID() int64 {
return atomic.AddInt64(&id, 1)
}
func main() {
dsn := "root:root@tcp(localhost:3306)/test?charset=utf8"
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("Error opening database: %v", err)
}
defer func() { _ = db.Close() }()
// 设置连接池参数
db.SetMaxOpenConns(100) // 最大打开连接数
db.SetMaxIdleConns(10) // 最大空闲连接数
db.SetConnMaxLifetime(time.Hour) // 连接最大存活时间
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
reqStart := time.Now()
var err error
uni := generateUniqueID()
start := time.Now()
// Insert unique ID int0 the database
insertQuery := `INSERT INTO test_last_insert_id (uni) VALUES (?)`
result, err := db.Exec(insertQuery, uni)
fmt.Printf("insert since: %v uni:%d \n", time.Since(start), uni)
if err != nil {
log.Fatalf("Error inserting data: %v", err)
}
lastInsertID, err := result.LastInsertId()
if err != nil {
log.Fatalf("Error getting last insert ID: %v", err)
}
selectStart := time.Now()
// Verify the last insert ID
selectQuery := `SELECT id FROM test_last_insert_id WHERE uni = ?`
var id int64
err = db.QueryRow(selectQuery, uni).Scan(&id)
fmt.Printf("select since:%v uni:%d \n", time.Since(selectStart), uni)
if err != nil {
log.Fatalf("Error selecting data: %v", err)
}
if id != lastInsertID {
log.Fatalf("ID mismatch: %d != %d", id, lastInsertID)
}
fmt.Printf("success req since:%v uni:%d \n", time.Since(reqStart), uni)
})
_ = http.ListenAndServe(":8080", nil)
}
截取了后面的一部分输出,这不会是SQL库的问题吧,
success req since:352.310146ms uni:1729393975000652
insert since: 163.316785ms uni:1729393975000688
insert since: 154.983173ms uni:1729393975000691
insert since: 158.094503ms uni:1729393975000689
insert since: 136.831695ms uni:1729393975000697
insert since: 141.857079ms uni:1729393975000696
insert since: 128.115216ms uni:1729393975000702
select since:412.94524ms uni:1729393975000634
success req since:431.383768ms uni:1729393975000634
select since:459.596445ms uni:1729393975000601
success req since:568.576336ms uni:1729393975000601
insert since: 134.39147ms uni:1729393975000700
select since:390.926517ms uni:1729393975000643
success req since:391.622183ms uni:1729393975000643
select since:366.098937ms uni:1729393975000648
success req since:373.490764ms uni:1729393975000648
insert since: 136.318919ms uni:1729393975000699
select since:420.626209ms uni:1729393975000640
success req since:425.243441ms uni:1729393975000640
insert since: 167.181068ms uni:1729393975000690
select since:272.22808ms uni:1729393975000671
单次请求的时候输出结果是符合预期的, 但是并发SQL时会出现执行慢的问题,这就很奇怪了
% curl localhost:8080
insert since: 1.559709ms uni:1729393975000703
select since:21.031284ms uni:1729393975000703
success req since:22.62274ms uni:1729393975000703
经群友提示还和唯一键的区分度有关,两边算法一致有点太难了,Go换了雪法ID之后就正常了。
因为之前 Go这边生成的uni值是递增的导致区分度很低,最终导致并发写入查询效率变低。
% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 44.51ms 24.87ms 187.91ms 77.98%
Req/Sec 1.17k 416.31 1.99k 66.67%
Latency Distribution
50% 37.46ms
75% 54.55ms
90% 80.44ms
99% 125.72ms
6960 requests in 3.01s, 509.77KB read
Requests/sec: 2316.02
Transfer/sec: 169.63KB
2024-10-23 更新
今天本来是想验证一下有关,并发插入自增有序的唯一键高延迟的问题,发现整个有问题的只有一行代码。
就是在查询时,类型转换的问题,插入和查询都转换之后,空表的情况下QPS 可以到4000多。即使在已有大数据量(几十万)的情况也有两千多的QPS。
现在又多了一个问题,为什么用雪花ID时不会有这样的问题。雪花ID也是int64类型的,这是为什么呢。
// 旧代码
err = db.QueryRow(selectQuery, uni).Scan(&id)
if err != nil {
log.Fatalf("Error selecting data: %v", err)
}
// 新代码 变化只有一个就是把uni 转成字符串之后就没有问题了
var realId int64
err = db.QueryRow(selectQuery, fmt.Sprintf("%d", uni)).Scan(&realId)
if err != nil {
log.Fatalf("Error selecting data: %v", err)
}
来源:juejin.cn/post/7427455855941976076
从《逆行人生》聊聊中年程序员的出路
赶在下架前去看了《逆行人生》。
这部电影讲述了高志垒——一个架构师,被裁员后,在找工作两个月颗粒无收的情况下,被逼无奈去跑外卖的故事。
个人感觉还是很值得一看的,是一部很好的现实题材电影,并没有网上说的资本迫害打工人还要打工人努力奋斗的感觉。
有年轻人说,难以共情。70万年薪的人最后要落到为了 15k 的月薪而奔波,他不理解为什么。然而就我亲身经历而言,无路可走的时候,我们的确会做这样的选择。
我们先来看看中年程序员有哪些选择。
中年程序员有哪些出路?
继续打工
打工,无疑是多数人的选择。毕竟上一天班赚一天的钱,这种稳稳的幸福还是大部分人的追求。但打工也不能停滞不前,还是要不断学习、拓展自己的能力,尤其是我们IT行业,技术更新迭代快。如果不学习,很可能 3 到 5 年就被淘汰了。
程序员要晋升、跳槽,主要学习方向以下两种:
- 拓展技术的深度与广度:高级开发、架构师、热门行业的开发如AI等;
- 向外拓展自己的能力:培训机构老师、高校老师;技术管理;
自己单干
继续打工,无疑都会碰到被裁员的风险,你个人的命运始终掌握在老板甚至顶头上司的手里。如果你不甘于此,就要开创性地走其他路了。这对个人的挑战都是极大的。
程序员可以凭借技术能力逐渐向外扩展:
- 独立开发:承接项目或者自研产品
- 创业:成立公司、团队,完成开发工作
彻底转行
也有部分人被彻底伤透了心,完全不再进入这个行业了,转向其他行业了。
- 网络兼职:写手、影视剪辑等;
- 中年三保、铁人三项:保安、保姆、保洁、快递、司机、外卖。这个是被大家调侃最多的;
- 其他行业的打工者:如制造业、外贸等行业;
- 开店或者创业:存上一笔钱开店或者做一间自己喜欢的公司,也是一些人的选择。
我们应该如何选择?
如上所见,程序员能做的选择还是比较多的。我们将这些工作列个表,列一下所需要的能力与所承担的责任,以及最后的风险,以便做选择:
可以看到,每个方向其实都是有风险的,并没有不存在无风险的职业与方向。那是不是我们就可以完全凭借个人喜好来决定呢?并非如此,这些选择对大部分人而言,还是有优劣之分的。
不推荐铁人三项、中年三宝
首先,我个人其实非常不建议程序员转行去做起他行业的,除非迫不得已,尤其是从事体力劳动。
因为这需要消耗大量的体力与时间。中年人无法靠比拼体力取胜,工作时间长,也无法取得工作生活平衡。在电影《逆行人生》中,高志垒虽然赢了第一个单王,但可以看出其靠的更多是运气,行业老大哥或退出竞赛、或家里有事提早离开。
另外就是,AI 技术发展和市场供需变化。不久前武汉的萝卜快跑落地,相信大部分滴滴司机都感受到了被淘汰的可能。而且这类工作市场基本上已经饱和,所以薪酬只会越来越低。
其他的网络兼职、去制造业服务业打工,这些都是门槛低,程序员即使有技术与能力,也不见得有任何优势的,所以也是不推荐的。
而开店或按自己的兴趣来创业,则非常看你个人能力了,同样需要更谨慎的考虑,当然你如果家财万贯,倒是可以任性一把。
更推荐提早规划、提早行动
剩下的职业方向其实都是推荐的,因为多多少少跟我们自身学习的技术是相关的。将我们的能力逐步往外扩,逐渐走出舒适圈,是更合适的一个发展路径。但是需要注意的是,建议尽早立下目标,提前规划,尽快行动的。
如,希望做老师,可以提早在企业内部做讲师、技术讲师,给新人讲解。锻炼好自己的沟通表达能力,多想想如何让新人更好地融入企业、进入工作状态。
又如,你想自己创业,那可以开始就留意你手头上做的产品是如何开发、运营的。公司如何分配人力物力去做的,如何做商业变现的,如何寻找客户的等等这些问题。不仅要站在技术角度、也要站在公司的角度多思考、多学习、多实践。甚至在时机成熟的时候,提出转岗去做产品、技术管理,更早地锻炼自己所需的创业的能力,能让自己日后的路走的更顺。
高志垒为何还是选择送外卖?
回到电影,既然都不建议程序员从事体力劳动,高志垒好好的一个架构师,也是有脑子的,为啥最后还是选择了外卖员呢?
首先,从影片一开始可以看出,高志垒选择了架构师或者技术管理偏技术方向,因其手头上还有一线开发的任务。显然对于 45 岁的他,在打工这条路上几乎已经到顶了。
然而,他并没有做好职业规划,甚至从未考虑过失业的风险。在突然失业时,才发现市场上几乎找不到自己的职位、薪酬,最后简历也是乱投一气了;而中产返贫三件套:高额房贷、全职太太、国际学校,他几乎全都拥有;并且还大笔地投资了 P2P ,因其爆雷导致家庭财产大量损失;再加上其父亲突发重病,住院急需要钱。
所有的状况同时出现,所有的压力压在身上,在两个月投递简历无果时,他听说送外卖能补上房贷月供差额的数目,宛如找到救命稻草一般,毅然加入了外卖行业。
如何避免陷入被动状况?
如何避免我们也陷入高志垒的状况?
除了像上面说的提早积攒自己的能力,提早做规划、更早地行动外,程序员也应提升技能多样性,特别是专业外的技能;同时在职业中后期应寻找到更利于个人发展的公司或项目;还需要拓展人脉,保持与行业内的沟通交流;在最后,保持健康的生活习惯和平衡好工作,让自己的职业寿命尽可能地延长。
而在财务上,做好失业准备、甚至为后续独立开发、创业等积攒资金都是必要的,所以需要采取一些措施,做好家庭财务的规划,如:
- 留出紧急备用金:为应对突发事件,如失业或疾病,应建立足够的紧急基金,一般建议为家庭日常开支的3-6个月。
- 谨慎投资:只投资自己熟悉的产品;了解自身的风险承受能力再投资;同时避免将所有资金投入到单一的高风险产品中,如P2P,应进行资产配置,分散风险。
- 购买保险:为家庭成员购买适当的健康保险,以减轻因病致贫的风险。
- 做好财务预算、规划:每年、每月做好财务预算;同时对于房贷和教育投资等大额支出,应进行详细的财务规划,确保在收入中断时也能应对。
- 增加收入来源:尽可能地增加家庭收入来源,比如配偶就业或开展副业,减少对单一收入的依赖。
总结与思考
在戏里的高志垒无疑是幸运的,家庭和睦,家人都给予最大的支持,愿意一起度过难关。再加上自己开发的小程序“路路通”,同事间互助互利,最后,成功拿到了单王,并帮家里度过经济危机。
然而最后的结局,高志垒并没有“逆袭”人生,而是在“逆行”人生中,调整了自己。最后他卖掉了大房子,搬到了小房子住,老婆依然在工作,孩子也放弃了就读国际学校、老人靠自身意志力完成了康复。
这也是我觉得这部电影还算现实主义之处。并没有理想中的事情发生,就像现实生活中那些受挫的人们一样,最后选择降低生活标准,继续前行。
最后的最后,问一下大家,如果你面临电影结尾彩蛋中的情景,有一个外卖公司的高层老板对你开发的“路路通”小程序感兴趣,你会如何选择?
- 卖掉小程序,拿钱走人
- 加入外卖公司,继续开发
- 不卖,开源
欢迎留下你的答案与思考,一起讨论。
来源:juejin.cn/post/7414732910240972835
15 种超赞的 MyBatis 写法
序言
MyBatis的前身是iBatis,最初是Apache的一个开源项目。随着时间的推移,为了更好地适应Java持久层框架的需求,iBatis在2010年重构并更名为MyBatis。
这一转变标志着MyBatis在功能和性能上的显著提升,同时也意味着它能够更好地服务于日益复杂的企业级应用。
今天,我们就来探讨 15 种超赞的 MyBatis 写法,让你的数据库操作更加高效和灵活。
1. 批量操作优化
批量操作是提高数据库操作效率的重要手段。MyBatis提供了<foreach>
标签,可以有效地进行批量插入、更新或删除操作,从而减少与数据库的交互次数。
批量插入示例:
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO user (username, email, create_time) VALUES
<foreach collection="list" item="item" separator=",">
(#{item.username}, #{item.email}, #{item.createTime})
</foreach>
</insert>
批量更新示例:
<update id="batchUpdate" parameterType="java.util.List">
<foreach collection="list" item="item" separator=";">
UPDATE user
SET username = #{item.username}, email = #{item.email}
WHERE id = #{item.id}
</foreach>
</update>
批量删除示例:
<delete id="batchDelete" parameterType="java.util.List">
DELETE FROM user WHERE id IN
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
通过使用<foreach>
标签,我们可以将多个操作合并为一条SQL语句,大大减少了数据库交互次数,提高了操作效率。
2. 动态SQL
动态SQL是MyBatis的强大特性之一,允许我们根据不同的条件动态构建SQL语句。<if>
标签是实现动态SQL的核心。
动态查询示例:
<select id="findUsers" resultType="User">
SELECT * FROM user
WHERE 1=1
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
<if test="status != null">
AND status = #{status}
</if>
</select>
在这个例子中,我们根据传入的参数动态添加查询条件。如果某个参数为空,对应的条件就不会被添加到SQL语句中。
3. 多条件分支查询
对于更复杂的查询逻辑,我们可以使用<choose>
、<when>
和<otherwise>
标签来实现多条件分支查询。
多条件分支查询示例:
<select id="findUsersByCondition" resultType="User">
SELECT * FROM user
WHERE 1=1
<choose>
<when test="searchType == 'username'">
AND username LIKE CONCAT('%', #{keyword}, '%')
</when>
<when test="searchType == 'email'">
AND email LIKE CONCAT('%', #{keyword}, '%')
</when>
<otherwise>
AND (username LIKE CONCAT('%', #{keyword}, '%') OR email LIKE CONCAT('%', #{keyword}, '%'))
</otherwise>
</choose>
</select>
这个例子展示了如何根据不同的搜索类型选择不同的查询条件,如果没有指定搜索类型,则默认搜索用户名和邮箱。
4. SQL语句优化
使用<trim>
标签可以帮助我们优化生成的SQL语句,避免出现多余的AND或OR关键字。
SQL语句优化示例:
<select id="findUsers" resultType="User">
SELECT * FROM user
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
<if test="status != null">
AND status = #{status}
</if>
</trim>
</select>
在这个例子中,<trim>
标签会自动去除第一个多余的AND或OR,并在有查询条件时添加WHERE关键字。
5. 自动生成主键
在插入操作中,我们经常需要获取数据库自动生成的主键。MyBatis提供了<selectKey>
标签来实现这一功能。
自动生成主键示例:
<insert id="insertUser" parameterType="User" useGeneratedKeys="true" keyProperty="id">
<selectKey keyProperty="id" order="AFTER" resultType="java.lang.Long">
SELECT 2531020
</selectKey>
INSERT INTO user (username, email, create_time)
VALUES (#{username}, #{email}, #{createTime})
</insert>
在这个例子中,插入操作完成后,会自动执行SELECT 2531020
获取新插入记录的ID,并将其赋值给传入的User对象的id属性。
6. 注解方式使用MyBatis
除了XML配置,MyBatis还支持使用注解来定义SQL操作,这种方式可以使代码更加简洁。
注解方式示例:
public interface UserMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
User getUserById(Long id);
@Insert("INSERT INTO user (username, email, create_time) VALUES (#{username}, #{email}, #{createTime})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insertUser(User user);
@Update("UPDATE user SET username = #{username}, email = #{email} WHERE id = #{id}")
int updateUser(User user);
@Delete("DELETE FROM user WHERE id = #{id}")
int deleteUser(Long id);
}
这种方式适合简单的CRUD操作,但对于复杂的SQL语句,仍然建议使用XML配置。
7. 高级映射
MyBatis提供了强大的对象关系映射功能,可以处理复杂的表关系。
一对多映射示例:
<resultMap id="userWithOrdersMap" type="User">
<id property="id" column="user_id"/>
<result property="username" column="username"/>
<collection property="orders" ofType="Order">
<id property="id" column="order_id"/>
<result property="orderNumber" column="order_number"/>
<result property="createTime" column="order_create_time"/>
</collection>
</resultMap>
<select id="getUserWithOrders" resultMap="userWithOrdersMap">
SELECT u.id as user_id, u.username, o.id as order_id, o.order_number, o.create_time as order_create_time
FROM user u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id = #{userId}
</select>
这个例子展示了如何将用户和订单信息映射到一个复杂的对象结构中。
8. MyBatis-Plus集成
MyBatis-Plus是MyBatis的增强工具,它提供了许多便捷的CRUD操作和强大的条件构造器。
MyBatis-Plus使用示例:
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
public List<User> findUsersByCondition(String username, String email) {
return this.list(new QueryWrapper<User>()
.like(StringUtils.isNotBlank(username), "username", username)
.eq(StringUtils.isNotBlank(email), "email", email));
}
}
在这个例子中,我们使用MyBatis-Plus提供的条件构造器来动态构建查询条件,无需编写XML。
9. 代码生成器
MyBatis Generator是一个强大的代码生成工具,可以根据数据库表自动生成MyBatis的Mapper接口、实体类和XML映射文件。
MyBatis Generator配置示例:
<!DOCTYPE generatorConfiguration PUBLIC
"-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="DB2Tables" targetRuntime="MyBatis3">
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/mydb"
userId="root"
password="password">
</jdbcConnection>
<javaModelGenerator targetPackage="com.example.model" targetProject="src/main/java">
<property name="enableSubPackages" value="true" />
<property name="trimStrings" value="true" />
</javaModelGenerator>
<sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources">
<property name="enableSubPackages" value="true" />
</sqlMapGenerator>
<javaClientGenerator type="XMLMAPPER" targetPackage="com.example.mapper" targetProject="src/main/java">
<property name="enableSubPackages" value="true" />
</javaClientGenerator>
<table tableName="user" domainObjectName="User" >
<generatedKey column="id" sqlStatement="MySQL" identity="true" />
</table>
</context>
</generatorConfiguration>
使用这个配置文件,我们可以自动生成与user表相关的所有必要代码。
10. 事务管理
在Spring环境中,我们可以使用@Transactional
注解来管理事务,确保数据的一致性。
事务管理示例:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional
public void createUserWithOrders(User user, List<Order> orders) {
userMapper.insert(user);
for (Order order : orders) {
order.setUserId(user.getId());
orderMapper.insert(order);
}
}
}
在这个例子中,创建用户和订单的操作被包装在一个事务中,如果任何一步失败,整个操作都会回滚。
11. 缓存机制
MyBatis提供了一级缓存和二级缓存,可以有效提高查询性能。
二级缓存配置示例:
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
这个配置启用了LRU淘汰策略的二级缓存,缓存容量为512个对象,每60秒刷新一次。
12. 插件使用
MyBatis插件可以拦截核心方法的调用,实现如分页、性能分析等功能。
分页插件示例 (使用PageHelper):
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public PageInfo<User> getUserList(int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.selectAll();
return new PageInfo<>(users);
}
}
这个例子展示了如何使用PageHelper插件实现简单的分页功能。
13. 多数据源配置
在某些场景下,我们需要在同一个应用中操作多个数据库。MyBatis支持配置多个数据源来实现这一需求。
多数据源配置示例:
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public SqlSessionFactory primarySqlSessionFactory(@Qualifier("primaryDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
return factoryBean.getObject();
}
@Bean
public SqlSessionFactory secondarySqlSessionFactory(@Qualifier("secondaryDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
return factoryBean.getObject();
}
}
这个配置类定义了两个数据源和对应的SqlSessionFactory,可以在不同的Mapper中使用不同的数据源。
14. 读写分离
读写分离是提高数据库性能的常用策略。MyBatis可以通过配置多数据源来实现简单的读写分离。
读写分离配置示例:
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public DataSource routingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.MASTER, masterDataSource);
targetDataSources.put(DataSourceType.SLAVE, slaveDataSource);
AbstractRoutingDataSource routingDataSource = new AbstractRoutingDataSource() {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
};
routingDataSource.setTargetDataSources(targetDataSources);
routingDataSource.setDefaultTargetDataSource(masterDataSource);
return routingDataSource;
}
}
这个例子定义了一个动态数据源,可以根据上下文选择主库或从库。你需要实现一个DataSourceContextHolder
来管理当前线程的数据源类型。
15. SQL分析和优化
MyBatis提供了SQL执行分析功能,可以帮助我们找出性能瓶颈。
SQL分析配置示例:
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
<property name="offsetAsPageNum" value="true"/>
<!-- 设置为true时,使用RowBounds分页会进行count查询 -->
<property name="rowBoundsWithCount" value="true"/>
</plugin>
<plugin interceptor="org.apache.ibatis.plugin.Interceptor">
<property name="properties">
sqlCollector=com.example.SqlCollector
</property>
</plugin>
</plugins>
在这个配置中,我们不仅加入了分页插件,还加入了一个自定义的SQL收集器,可以用于分析SQL执行情况。
总结
我们详细介绍了15种MyBatis的高级用法和技巧,涵盖了从基本的CRUD操作优化到复杂的多数据源配置和读写分离等高级主题。这些技巧可以帮助开发者更高效地使用MyBatis,构建出性能更好、可维护性更强的应用系统。
来源:juejin.cn/post/7417681630884233268
MapStruct这么用,同事也开始模仿
前言
hi,大家好,我是大鱼七成饱。
前几天同事review我的代码,发现mapstruct有这么多好用的技巧,遇到POJO转换的问题经常过来沟通。考虑到不可能每次都一对一,所以我来梳理五个场景,谁在过来问,直接甩出总结。
环境准备
由于日常使用都是spring,所以后面的示例都是在springboot框架中运行的。关键pom依赖如下:
<properties>
<java.version>1.8</java.version>
<org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
<org.projectlombok.version>1.18.30</org.projectlombok.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
场景一:常量转换
这是最简单的一个场景,比如需要设置字符串、整形和长整型的常量,有的又需要日期,或者新建类型。下面举个例子,演示如何转换
//实体类
@Data
public class Source {
private String stringProp;
private Long longProp;
}
@Data
public class Target {
private String stringProperty;
private long longProperty;
private String stringConstant;
private Integer integerConstant;
private Long longWrapperConstant;
private Date dateConstant;
}
- 设置字符串常量
- 设置long常量
- 设置java内置类型默认值,比如date
那么mapper这么设置就可以
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface SourceTargetMapper {
@Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")
@Mapping(target = "longProperty", source = "longProp", defaultValue = "-1l")
@Mapping(target = "stringConstant", constant = "Constant Value")
@Mapping(target = "integerConstant", constant = "14")
@Mapping(target = "longWrapperConstant", constant = "3001L")
@Mapping(target = "dateConstant", dateFormat = "yyyy-MM-dd", constant = "2023-09-")
Target sourceToTarget(Source s);
}
解释下,constant用来设置常量值,source的值如果没有设置,则会使用defaultValue的值,日期可以按dateFormat解析。
Talk is cheap, show me the code !废话不多说,自动生成的转换类如下:
@Component
public class SourceTargetMapperImpl implements SourceTargetMapper {
public SourceTargetMapperImpl() {
}
public Target sourceToTarget(Source s) {
if (s == null) {
return null;
} else {
Target target = new Target();
if (s.getStringProp() != null) {
target.setStringProperty(s.getStringProp());
} else {
target.setStringProperty("undefined");
}
if (s.getLongProp() != null) {
target.setLongProperty(s.getLongProp());
} else {
target.setLongProperty(-1L);
}
target.setStringConstant("Constant Value");
target.setIntegerConstant(14);
target.setLongWrapperConstant(3001L);
try {
target.setDateConstant((new SimpleDateFormat("dd-MM-yyyy")).parse("09-01-2014"));
return target;
} catch (ParseException var4) {
throw new RuntimeException(var4);
}
}
}
}
是不是一目了然
场景二:转换中调用表达式
比如id不存在使用UUID生成一个,或者使用已有参数新建一个对象作为属性。当然可以用after mapping,qualifiedByName等实现,感觉还是不够优雅,这里介绍个雅的(代码少点的)。
实体类如下:
@Data
public class CustomerDto {
public Long id;
public String customerName;
private String format;
private Date time;
}
@Data
public class Customer {
private String id;
private String name;
private TimeAndFormat timeAndFormat;
}
@Data
public class TimeAndFormat {
private Date time;
private String format;
public TimeAndFormat(Date time, String format) {
this.time = time;
this.format = format;
}
}
Dto转customer,加创建TimeAndFormat作为属性,mapper实现如下:
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, imports = UUID.class)
public interface CustomerMapper {
@Mapping(target = "timeAndFormat",
expression = "java( new TimeAndFormat( s.getTime(), s.getFormat() ) )")
@Mapping(target = "id", source = "id", defaultExpression = "java( UUID.randomUUID().toString() )")
Customer toCustomer(CustomerDto s);
}
解释下,id为空则走默认的defaultExpression,通过imports引入,java括起来调用。新建对象直接new TimeAndFormat。有的小伙伴喜欢用qualifiedByName自定义方法,可以对比下,哪个合适用哪个,都能调用转换方法。
生成代码如下:
@Component
public class CustomerMapperImpl implements CustomerMapper {
public CustomerMapperImpl() {
}
public Customer toCustomer(CustomerDto s) {
if (s == null) {
return null;
} else {
Customer customer = new Customer();
if (s.getId() != null) {
customer.setId(String.valueOf(s.getId()));
} else {
customer.setId(UUID.randomUUID().toString());
}
customer.setTimeAndFormat(new TimeAndFormat(s.getTime(), s.getFormat()));
return customer;
}
}
}
场景三:类共用属性,如何复用
比如下面的Bike和车辆类,都有id和creationDate属性,我又不想重复写mapper属性注解
public class Bike {
/**
* 唯一id
*/
private String id;
private Date creationDate;
/**
* 品牌
*/
private String brandName;
}
public class Car {
/**
* 唯一id
*/
private String id;
private Date creationDate;
/**
* 车牌号
*/
private String chepaihao;
}
解决起来很简单,写个共用的注解,使用的时候引入就可以,示例如下:
//通用注解
@Retention(RetentionPolicy.CLASS)
//自动生成当前日期
@Mapping(target = "creationDate", expression = "java(new java.util.Date())")
//忽略id
@Mapping(target = "id", ignore = true)
public @interface ToEntity { }
//使用
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface TransportationMapper {
@ToEntity
@Mapping( target = "brandName", source = "brand")
Bike map(BikeDto source);
@ToEntity
@Mapping( target = "chepaihao", source = "plateNo")
Car map(CarDto source);
}
这里Retention修饰ToEntity注解,表示ToEntity注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期,辅助生成mapper实现类。上面定义了creationDate和id的转换规则,新建日期,忽略id。
生成的mapper实现类如下:
@Component
public class TransportationMapperImpl implements TransportationMapper {
public TransportationMapperImpl() {
}
public Bike map(BikeDto source) {
if (source == null) {
return null;
} else {
Bike bike = new Bike();
bike.setBrandName(source.getBrand());
bike.setCreationDate(new Date());
return bike;
}
}
public Car map(CarDto source) {
if (source == null) {
return null;
} else {
Car car = new Car();
car.setChepaihao(source.getPlateNo());
car.setCreationDate(new Date());
return car;
}
}
}
坚持一下,还剩俩场景,剩下的俩更有意思
场景四:lombok和mapstruct冲突了
啥冲突?用了builder注解后,mapstuct转换不出来了。哎,这个问题困扰了我那同事两天时间。
解决方案如下:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</dependency>
加上lombok-mapstruct-binding就可以了,看下生成的效果:
@Builder
@Data
public class Person {
private String name;
}
@Data
public class PersonDto {
private String name;
}
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface PersonMapper {
Person map(PersonDto dto);
}
@Component
public class PersonMapperImpl implements PersonMapper {
public PersonMapperImpl() {
}
public Person map(PersonDto dto) {
if (dto == null) {
return null;
} else {
Person.PersonBuilder person = Person.builder();
person.name(dto.getName());
return person.build();
}
}
}
从上面可以看到,mapstruct匹配到了lombok的builder方法。
场景五:说个难点的,转换的时候,如何注入springBean
有时候转换方法比不是静态的,他可能依赖spring bean,这个如何导入?
这个使用需要使用抽象方法了,上代码:
@Component
public class SimpleService {
public String formatName(String name) {
return "您的名字是:" + name;
}
}
@Data
public class Student {
private String name;
}
@Data
public class StudentDto {
private String name;
}
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public abstract class StudentMapper {
@Autowired
protected SimpleService simpleService;
@Mapping(target = "name", expression = "java(simpleService.formatName(source.getName()))")
public abstract StudentDto map(StudentDto source);
}
接口是不支持注入的,但是抽象类可以,所以采用抽象类解决,后面expression直接用皆可以了,生成mapperimpl如下:
@Component
public class StudentMapperImpl extends StudentMapper {
public StudentMapperImpl() {
}
public StudentDto map(StudentDto source) {
if (source == null) {
return null;
} else {
StudentDto studentDto = new StudentDto();
studentDto.setName(this.simpleService.formatName(source.getName()));
return studentDto;
}
}
}
思考
以上场景肯定还有其他解决方案,遵循合适的原则就可以。驾驭不了的代码,可能带来更多问题,先简单实现,后续在迭代优化可能适合更多的业务场景。
本文示例代码放在了github,需要的朋友请关注公众号大鱼七成饱,回复关键词MapStruct使用即可获得。
来源:juejin.cn/post/7297222349731627046
如果你想做副业,不妨看看我这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
大龄程序员尝试了不一样的兼职体验
- 自我理解
我可能和大部分程序员是一样的,就是一个普普通通的程序员,没有远大理想,没有清晰的职业规划,踏踏实实的做好工作,平时做好技术积累。即使目前已经36了,但不善于交际,依然是一个最底层的程序员,每天依然是写代码。在一家很小的小公司里面干了十多年了,可能公司比较小,危机也没有大公司那么强烈,即使经历了三年疫情,一个小公司活下来也是奇迹。在35岁前,我也并没有什么工作危机感。但是随着过了35岁,感觉可能程序员并不能干一辈子,而且这个感觉越来越强烈(不知道为什么会有)。所以一直就想找找其他的职业方向,列如兼职什么的,先尝试一下,毕竟之前什么也没做过。
- 想法的迸发
自从有了这个想法,我就开始不断的去了解那些是可以做的。例如自媒体,自媒体的选材又很多,知识分享类的,感受分享类的,直播讲课类的等。总之想了很多,但由于自己表达能力不是很好。多次录视频尝试后,还是不行。不得以放弃掉。后来接触送外卖是从一个朋友那里了解的,他也是兼职送,每周末兼职。大概每天能赚个200到300左右。经过朋友的鼓励,我选择尝试一下。
- 开始准备工作
可能送外卖和大家理解的还不一样,不是有个电动车就能送的。得租一个或者买一个能够换电的电动车,能够随时换电,不用担心没电的情况。接单也不是注册app就能接,这个也得有方法的,下面讲。开始准备租车,租车方式很多,大概租车价格都差不多,基本在500到700之间,包含租车和租电。总之租好车和注册号APP,按照流程做好认证,把学习课程做完就可以正式跑单了。
- 跑单开始
开始跑单的时候是由朋友带着跑了一次,怎么接单,接派单,怎么取单,怎么送单。跑了一次后也就明白了整个流程。我就开始自己跑,由于自己刚开始跑,我就选择接一单跑一单(外卖大神都是一次接十多单)。但是跑的过程中也是好多问题,商家找不到了,客户楼栋找错了啊,路线导航错了,送错人了,车到一半没电了等等。好多问题,但是万幸的是到目前还没有超时过。
- 跑单时间和收入
最开始是周末跑的,后来感觉停不下来了,现在每天中午休息的时候跑一个半小时,晚上不跑好好休息,毕竟还要上班,主业还是很重要的,周末继续跑。现在收入大概平时中午能收入二三十左右。周末能到200左右了。截至目前已经连续跑了两周了。
- 跑单后的感受
第一个身体上的感受,感觉之前的身体太弱了,最开始非常累,现在已经习惯点了。感觉自己的精神和专注度比之前要提升很多。虽然累,但是第二天还是很精神的,最起码感觉比之前要有很大的提升。 第二个就是眼界上面,每天都能够接触不同行业的人,看到很多的事情,比之前接触的人要多得多,而且大多数人对送外卖的是非常友好的。第三个就是通过送外卖可以看清很多东西,列如有些外卖真的环境很差,而且点的人还超级多。每次我都想提醒下这些人下次不要点了。有的时候打包外卖的人很快,有的就非常慢(这大概也是摸鱼),通过别人看自己也能理解一些东西。
- 给大家的建议
虽然感觉大家不一定能够点进来看,但是万一你点进来,并且看到了这里。我希望给你的建议就是如果你找到一个兼职的方向,请马上行动起来,并且坚持,如果不行,就换一个,马上行动,再坚持。
也希望大家能够给我一些兼职方面的建议,我的眼界太局限了。可能有些行业我能够做好的,但是我并不知道的。在这里我谢谢大家。
来源:juejin.cn/post/7428785902640316451
Spring 实现 3 种异步流式接口,干掉接口超时烦恼
大家好,我是小富~
如何处理比较耗时的接口?
这题我熟,直接上异步接口,使用 Callable
、WebAsyncTask
和 DeferredResult
、CompletableFuture
等均可实现。
但这些方法有局限性,处理结果仅返回单个值。在某些场景下,如果需要接口异步处理的同时,还持续不断地向客户端响应处理结果,这些方法就不够看了。
Spring 框架提供了多种工具支持异步流式接口,如 ResponseBodyEmitter
、SseEmitter
和 StreamingResponseBody
。这些工具的用法简单,接口中直接返回相应的对象或泛型响应实体 ResponseEntity<xxxx>
,如此这些接口就是异步的,且执行耗时操作亦不会阻塞 Servlet
的请求线程,不影响系统的响应能力。
下面将逐一介绍每个工具的使用及其应用场景。
ResponseBodyEmitter
ResponseBodyEmitter
适应适合于需要动态生成内容并逐步发送给客户端的场景,例如:文件上传进度、实时日志等,可以在任务执行过程中逐步向客户端发送更新。
举个例子,经常用GPT你会发现当你提问后,得到的答案并不是一次性响应呈现的,而是逐步动态显示。这样做的好处是,让你感觉它在认真思考,交互体验比直接返回完整答案更为生动和自然。
使用ResponseBodyEmitter
来实现下这个效果,创建 ResponseBodyEmitter 发送器对象,模拟耗时操作逐步调用 send 方法发送消息。
注意:ResponseBodyEmitter 的超时时间,如果设置为
0
或-1
,则表示连接不会超时;如果不设置,到达默认的超时时间后连接会自动断开。其他两种工具也是同样的用法,后边不在赘述了
@GetMapping("/bodyEmitter")
public ResponseBodyEmitter handle() {
// 创建一个ResponseBodyEmitter,-1代表不超时
ResponseBodyEmitter emitter = new ResponseBodyEmitter(-1L);
// 异步执行耗时操作
CompletableFuture.runAsync(() -> {
try {
// 模拟耗时操作
for (int i = 0; i < 10000; i++) {
System.out.println("bodyEmitter " + i);
// 发送数据
emitter.send("bodyEmitter " + i + " @ " + new Date() + "\n");
Thread.sleep(2000);
}
// 完成
emitter.complete();
} catch (Exception e) {
// 发生异常时结束接口
emitter.completeWithError(e);
}
});
return emitter;
}
实现代码非常简单。通过模拟每2秒响应一次结果,请求接口时可以看到页面数据在动态生成。效果与 GPT 回答基本一致。
SseEmitter
SseEmitter
是 ResponseBodyEmitter
的一个子类,它同样能够实现动态内容生成,不过主要将它用在服务器向客户端推送实时数据,如实时消息推送、状态更新等场景。在我之前的一篇文章 我有 7种 实现web实时消息推送的方案 中详细介绍了 Server-Sent Events (SSE)
技术,感兴趣的可以回顾下。
SSE在服务器和客户端之间打开一个单向通道,服务端响应的不再是一次性的数据包而是text/event-stream
类型的数据流信息,在有数据变更时从服务器流式传输到客户端。
整体的实现思路有点类似于在线视频播放,视频流会连续不断的推送到浏览器,你也可以理解成,客户端在完成一次用时很长(网络不畅)的下载。
客户端JS实现,通过一次 HTTP 请求建立连接后,等待接收消息。此时,服务端为每个连接创建一个 SseEmitter
对象,通过这个通道向客户端发送消息。
<body>
<div id="content" style="text-align: center;">
<h1>SSE 接收服务端事件消息数据</h1>
<div id="message">等待连接...</div>
</div>
<script>
let source = null;
let userId = 7777
function setMessageInnerHTML(message) {
const messageDiv = document.getElementById("message");
const newParagraph = document.createElement("p");
newParagraph.textContent = message;
messageDiv.appendChild(newParagraph);
}
if (window.EventSource) {
// 建立连接
source = new EventSource('http://127.0.0.1:9033/subSseEmitter/'+userId);
setMessageInnerHTML("连接用户=" + userId);
/**
* 连接一旦建立,就会触发open事件
* 另一种写法:source.onopen = function (event) {}
*/
source.addEventListener('open', function (e) {
setMessageInnerHTML("建立连接。。。");
}, false);
/**
* 客户端收到服务器发来的数据
* 另一种写法:source.onmessage = function (event) {}
*/
source.addEventListener('message', function (e) {
setMessageInnerHTML(e.data);
});
} else {
setMessageInnerHTML("你的浏览器不支持SSE");
}
</script>
</body>
在服务端,我们将 SseEmitter
发送器对象进行持久化,以便在消息产生时直接取出对应的 SseEmitter 发送器,并调用 send
方法进行推送。
private static final Map<String, SseEmitter> EMITTER_MAP = new ConcurrentHashMap<>();
@GetMapping("/subSseEmitter/{userId}")
public SseEmitter sseEmitter(@PathVariable String userId) {
log.info("sseEmitter: {}", userId);
SseEmitter emitterTmp = new SseEmitter(-1L);
EMITTER_MAP.put(userId, emitterTmp);
CompletableFuture.runAsync(() -> {
try {
SseEmitter.SseEventBuilder event = SseEmitter.event()
.data("sseEmitter" + userId + " @ " + LocalTime.now())
.id(String.valueOf(userId))
.name("sseEmitter");
emitterTmp.send(event);
} catch (Exception ex) {
emitterTmp.completeWithError(ex);
}
});
return emitterTmp;
}
@GetMapping("/sendSseMsg/{userId}")
public void sseEmitter(@PathVariable String userId, String msg) throws IOException {
SseEmitter sseEmitter = EMITTER_MAP.get(userId);
if (sseEmitter == null) {
return;
}
sseEmitter.send(msg);
}
接下来向 userId=7777
的用户发送消息,127.0.0.1:9033/sendSseMsg/7777?msg=欢迎关注-->程序员小富,该消息可以在页面上实时展示。
而且SSE有一点比较好,客户端与服务端一旦建立连接,即便服务端发生重启,也可以做到自动重连。
StreamingResponseBody
StreamingResponseBody
与其他响应处理方式略有不同,主要用于处理大数据量或持续数据流的传输,支持将数据直接写入OutputStream
。
例如,当我们需要下载一个超大文件时,使用 StreamingResponseBody 可以避免将文件数据一次性加载到内存中,而是持续不断的把文件流发送给客户端,从而解决下载大文件时常见的内存溢出问题。
接口实现直接返回 StreamingResponseBody 对象,将数据写入输出流并刷新,调用一次flush
就会向客户端写入一次数据。
@GetMapping("/streamingResponse")
public ResponseEntity<StreamingResponseBody> handleRbe() {
StreamingResponseBody stream = out -> {
String message = "streamingResponse";
for (int i = 0; i < 1000; i++) {
try {
out.write(((message + i) + "\r\n").getBytes());
out.write("\r\n".getBytes());
//调用一次flush就会像前端写入一次数据
out.flush();
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(stream);
}
demo这里输出的是简单的文本流,如果是下载文件那么转换成文件流效果是一样的。
总结
这篇介绍三种实现异步流式接口的工具,算是 Spring 知识点的扫盲。使用起来比较简单,没有什么难点,但它们在实际业务中的应用场景还是很多的,通过这些工具,可以有效提高系统的性能和响应能力。
文中 Demo Github 地址:github.com/chengxy-nds…
来源:juejin.cn/post/7425399689825140786
为什么推荐用Redisson实现分布式锁,看完直呼好好好
开心一刻
一男人站在楼顶准备跳楼,楼下有个劝解员拿个喇叭准备劝解
劝解员:兄弟,别跳
跳楼人:我不想活了
劝解员:你想想你媳妇
跳楼人:媳妇跟人跑了
劝解员:你还有兄弟
跳楼人:就是跟我兄弟跑的
劝解员:你想想你家孩子
跳楼人:孩子是他俩的
劝解员:死吧,妈的,你活着也没啥意义了

写在前面
关于锁,相信大家都不陌生,一般我们用其在多线程环境中控制对共享资源的并发访问;单服务下,用 JDK 中的 synchronized
或 Lock
的实现类可实现对共享资源的并发访问,分布式服务下,JDK 中的锁就显得力不从心了,分布式锁也就应运而生了;分布式锁的实现方式有很多,常见的有如下几种
- 基于 MySQL,利用行级悲观锁(select ... for update)
- 基于 Redis,利用其 (setnx + expire) 或 set
- 基于 Zookeeper,利用其临时目录和事件回调机制
本文不讲这些,网上资料很多,感兴趣的小伙伴自行去查阅;本文的重点是基于 Redis 的 Redisson,从源码的角度来看看为什么推荐用 Redisson 来实现分布式锁;推荐大家先去看看
搞懂了 Redis 的订阅、发布与Lua,Redisson的加锁原理就好理解了
有助于理解后文
分布式锁特点
可以类比 JDK 中的锁
- 互斥
不仅要保证同个服务中不同线程的互斥,还需要保证不同服务间、不同线程的互斥;如何处理互斥,是自旋、还是阻塞 ,还是其他 ?
- 超时
锁超时设置,防止程序异常奔溃而导致锁一直存在,后续同把锁一直加不上
- 续期
程序具体执行的时长无法确定,所以过期时间只能是个估值,那么就不能保证程序在过期时间内百分百能运行完,所以需要进行锁续期,保证业务是在加锁的情况下完成的
- 可重入
可重入锁又名递归锁,是指同一个线程在外层方法已经获得锁,再进入该线程的中层或内层方法会自动获取锁;简单点来说,就是同个线程可以反复获取同一把锁
- 专一释放
通俗点来讲:谁加的锁就只有它能释放这把锁;为什么会出现这种错乱释放的问题了,举个例子就理解了
线程 T1 对资源 lock_zhangsan 加了锁,由于某些原因,业务还未执行完,锁已经过期自动释放了,此时线程 T2 对资源 lock_zhangsan 加锁成功,T2 还在执行业务的过程中,T1 业务执行完后释放资源 lock_zhangsan 的锁,结果把 T2 加的锁给释放了
- 公平与非公平
公平锁:多个线程按照申请锁的顺序去获得锁,所有线程都在队列里排队,这样就保证了队列中的第一个先得到锁
非公平锁:多个线程不按照申请锁的顺序去获得锁,而是同时直接去尝试获取锁
JDK 中的 ReentrantLock 就有公平和非公平两种实现,有兴趣的可以去看看它的源码;多数情况下用的是非公平锁,但有些特殊情况下需要用公平锁
你们可能会有这样的疑问
引入一个简单的分布式锁而已,有必要考虑这么多吗?
虽然绝大部分情况下,我们的程序都是在跑正常流程,但不能保证异常情况 100% 跑不到,出于健壮性考虑,异常情况都需要考虑到;下面我们就来看看 Redisson 是如何实现这些特点的
Redisson实现分布式锁
关于 Redisson
,更多详细信息可查看官方文档,它提供了非常丰富的功能,分布式锁 只是其中之一;我们基于 Redisson 3.13.6
,来看看分布式锁的实现
- 先将 Redis 信息配置给 Redisson,创建出 RedissonClient 实例
Redis 的部署方式不同,Redisson 配置模式也会不同,详细信息可查看:Configuration,我们就以最简单的
Single mode
来配置
@Before
public void before() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://192.168.1.110:6379");
redissonClient = Redisson.create(config);
}
- 通过 RedissonClient 实例获取锁
RedissonClient 实例创建出来后,就可以通过它来获取锁
/**
* 多线程
* @throws Exception
*/
@Test
public void multiLock() throws Exception {
RLock testLock = redissonClient.getLock("multi_lock");
int count = 5;
CountDownLatch latch = new CountDownLatch(count);
for (int i=1; i<=count; i++) {
new Thread(() -> {
try {
System.out.println("线程 " + Thread.currentThread().getName() + " 尝试获取锁");
testLock.lock();
System.out.println(String.format("线程 %s 获取到锁, 执行业务中...", Thread.currentThread().getName()));
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.format("线程 %s 业务执行完成", Thread.currentThread().getName()));
latch.countDown();
} finally {
testLock.unlock();
System.out.println(String.format("线程 %s 释放锁完成", Thread.currentThread().getName()));
}
}, "t" + i).start();
}
latch.await();
System.out.println("结束");
}
完整示例代码:redisson-demo
用 Redisson 实现分布式锁就是这么简单,但光会使用肯定是不够的,我们还得知道其底层实现原理
知其然,并知其所以然!
那如何知道其原理呢?当然是看其源码实现
客户端创建
客服端的创建过程中,会生成一个 id
作为唯一标识,用以区分分布式下不同节点中的客户端

id 值就是一个 UUID,客户端启动时生成;至于这个 id 有什么用,大家暂且在脑中留下这个疑问,我们接着往下看
锁获取
我们从 lock
开始跟源码

最终会来到有三个参数的 lock 方法
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
// 尝试获取锁;ttl为null表示锁获取成功; ttl不为null表示获取锁失败,其值为其他线程占用该锁的剩余时间
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return;
}
// 锁被其他线程占用而获取失败,使用redis的发布订阅功能来等待锁的释放通知,而非自旋监测锁的释放
RFuture<RedissonLockEntry> future = subscribe(threadId);
// 当前线程会阻塞,直到锁被释放时当前线程被唤醒(有超时等待,默认 7.5s,而不会一直等待)
// 持有锁的线程释放锁之后,redis会发布消息,所有等待该锁的线程都会被唤醒,包括当前线程
if (interruptibly) {
commandExecutor.syncSubscriptionInterrupted(future);
} else {
commandExecutor.syncSubscription(future);
}
try {
while (true) {
// 尝试获取锁;ttl为null表示锁获取成功; ttl不为null表示获取锁失败,其值为其他线程占用该锁的剩余时间
ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
break;
}
// waiting for message
if (ttl >= 0) {
try {
// future.getNow().getLatch() 返回的是 Semaphore 对象,其初始许可证为 0,以此来控制线程获取锁的顺序
// 通过 Semaphore 控制当前服务节点竞争锁的线程数量
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
if (interruptibly) {
throw e;
}
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else {
if (interruptibly) {
future.getNow().getLatch().acquire();
} else {
future.getNow().getLatch().acquireUninterruptibly();
}
}
}
} finally {
// 退出锁竞争(锁获取成功或者放弃获取锁),则取消锁的释放订阅
unsubscribe(future, threadId);
}
// get(lockAsync(leaseTime, unit));
}
主要三个点:尝试获取锁
、订阅
、取消订阅
- 尝试获取锁
尝试获取锁主要做了两件事:1、尝试获取锁,2、锁续期;尝试获取锁主要涉及到一段 Lua 代码
结合 搞懂了 Redis 的订阅、发布与Lua,Redisson的加锁原理就好理解了 来看这段 Lua 脚本,还是很好理解的
- 用 exists 判断 key 不存在,则用 hash 结构来存放锁,key = 资源名,field = uuid + : + threadId,value 自增 1;设置锁的过期时间(默认是 lockWatchdogTimeout = 30 * 1000 毫秒),并返回 nil
- 用 hexists 判断 field = uuid + : + threadId 存在,则该 field 的 value 自增 1,并重置过期时间,最后返回 nil
这里相当于实现了锁的重入
- 上面两种情况都不满足,则说明锁被其他线程占用了,直接返回锁的过期时间
给你们提个问题
为什么 field = uuid + : + threadId,而不是 field = threadId
友情提示:从多个服务(也就是多个 Redisson 客户端)来考虑
这个问题想清楚了,那么前面提到的:在 Redisson 客户端创建的过程中生成的 id(一个随机的 uuid 值),它的作用也就清楚了
尝试获取锁成功之后,会启动一个定时任务(即
WatchDog
,亦称看门狗
)实现锁续期,也涉及到一段 Lua 脚本
这段脚本很简单,相信你们都能看懂
默认情况下,锁的过期时间是 30s,锁获取成功之后每隔 10s 进行一次锁续期,重置过期时间成 30s
若锁已经被释放了,则定时任务也会停止,不会再续期
- 订阅
获取锁的过程中,尝试获取锁失败(锁被其他线程锁占有),则会完成对该锁频道的订阅,订阅过程中线程会阻塞;持有锁的线程释放锁时会向锁频道发布消息,订阅了该锁频道的线程会被唤醒,继续去获取锁,
给你们提个问题
如果持有锁的线程意外停止了,未向锁频道发布消息,那订阅了锁频道的线程该如何唤醒
Redisson 其实已经考虑到了,提供了超时机制来处理
默认超时时长 = 3000 + 1500 * 3 = 7500 毫秒
再给你们提个问题
为什么要用 Redis 的发布订阅
如果我们不用 Redis 的发布订阅,我们该如何实现,自旋?自旋有什么缺点? 自旋频率难以掌控,太高会增大 CPU 的负担,太低会不及时(锁都释放半天了才检测到);可以类比
生产者与消费者
来考虑这个问题 - 取消订阅
有订阅,肯定就有取消订阅;当阻塞的线程被唤醒并获取到锁时需要取消对锁频道的订阅,当然,取消获取锁的线程也需要取消对锁频道的订阅
比较好理解,就是取消当前线程对锁频道的订阅
锁释放
我们从 unlock
开始

代码比较简单,我们继续往下跟

主要有两点:释放锁
和 取消续期定时任务
- 释放锁
重点在于一个 Lua 脚本
我们把参数具象化,脚本就好理解了
KEYS[1] = 锁资源,KEYS[2] = 锁频道
ARGV[1] = 锁频道消息类型,ARGV[2] = 过期时间,ARGV[3] = uuid + : + threadId
- 如果当前线程未持有锁,直接返回 nil
- hash 结构的 field 的 value 自减 1,counter = 自减后的 value 值
如果 counter > 0,表示线程重入了,重置锁的过期时间,返回 0
如果 counter <= 0,删除锁,并对锁频道发布锁释放消息(频道订阅者则可收到消息,然后唤醒线程去获取锁),返回 1
- 上面 1、2 都不满足,则直接返回 nil
两个细节:1、重入锁的释放,2、锁彻底释放后的消息发布
- 取消续期定时任务
比较简单,没什么好说的
总结
我们从分布式锁的特点出发,来总结下 Redisson 是如何实现这些特点的
- 互斥
Redisson 采用 hash 结构来存锁资源,通过 Lua 脚本对锁资源进行操作,保证线程之间的互斥;互斥之后,未获取到锁的线程会订阅锁频道,然后进入一定时长的阻塞
- 超时
有超时设置,给 hash 结构的 key 加上过期时间,默认是 30s
- 续期
线程获取到锁之后会开启一个定时任务(watchdog 即 看门狗),每隔一定时间(默认 10s)重置 key 的过期时间
- 可重入
通过 hash 结构解决,key 是锁资源,field(值:uuid + : + threadId) 是持有锁的线程,value 表示重入次数
- 专一释放
通过 hash 结构解决,field 中存放了线程信息,释放的时候就能够知道是不是当前线程加上的锁,是才能够进行锁释放
- 公平与非公平
由你们在评论区补充
- 互斥
来源:juejin.cn/post/7425786548061683727
90后在技术上开始被00后嘲笑了,90后该何去何从?
只要你不断的学习,哪怕学习一项新技术生命周期只有五年,未来十年内这个行业找口饭吃还是不难的, 如果互联网赛道太卷,也可以换个赛道
前言
事情是这样的,最近我正在做一个项目需要用到Elasticsearch做全文检索,我们组刚好进了一个00后新人,这个项目需求是在原来的搜索上增加很多新的字段,他说:新增加的字段他一会整理下然后添加到老索引上,我当时说:我们Elasticsearch索引里面有好几千万的数据,有一套添加字段的流程,需要重建索引比较麻烦的,大致流程是:先创建一个新的索引,然后reindex老数据到新索引中,然后再将别名重新指向新索引,具体流程可以参考我之前的文章《EalsticSearch添加字段后重建索引方法》。他说不需要这么麻烦啊,直接添加就好了,当时我就愣住了,我说你这有点超出了我的认知,在我的认知里Elasticsearch添加字段都是需要重建索引了,你确定索引里添加新字段不需要重建索引也能被检索到,他被我这么一问也有点不自信,然后我就实际求正了一下,在Elasticsearch7.10里添加字段确实不需要重建索引,就和Mysql添加字段一样,可能是我的知识有点落伍了,所以这里也学习、记录、反思一下。
一、添加索引
1.1 创建索引
第一步我们创建一个user索引,里面添加一个id字段:
PUT /user
{
"mappings": {
"properties":
{
"id": {
"type": "long"
}
}
}
}
然后我们添加一条数据:
POST /user/_doc/1
{
"id":1
}
1.2 添加字段
现在我们需要为user索引添加一个userName字段,类型为keyword
PUT /user/_mapping
{
"properties": {
"userName": {
"type": "keyword"
}
}
}
然后 GET /user/_mapping
发现字段已经添加上了
{
"user" : {
"mappings" : {
"properties" : {
"id" : {
"type" : "long"
},
"userName" : {
"type" : "keyword"
}
}
}
}
}
为了验证添加字段是否能补检索,我们添加一条数据
POST /user/_doc/2
{
"id":2,
"userName":"赵侠客"
}
▲可以搜索出添加的字段
验证完全没有问题,字段添加上了,也能搜索,事实证明我的知识确实需要更新了,我也不知道为什么我的认知里添加字段是需要重建索引的,而且我身边的开发者也是这么认为的,可能是最初使用者用了这个方法,后来留给我们了,然后大家都觉得添加新字段需要重建索引,也就一直这么用下来了!!!
1.3 历史数据处理
新加字段后新数据都有了userName字段,但是老数据是没有userName字段的,处理方法有两种
- 老数据从数据库批量同步一下
- 老数据可以通过_update_by_query设置个默认值
POST /user/_update_by_query
{
"script":{
"source": "ctx._source['userName'] = "公众号:【赵侠客】""
},
"query": {
"range": {
"id": {
"gte": 0,
"lte": 1
}
}
}
}
_update_by_query还有很多其它参数,比如异步执行、查看执行任务、取消任务等等,具体可以参考官方文档: Update By Query API
二、修改|删除索引字段类型
2.1 修改索引字段类型
在 Elasticsearch 中,一旦创建索引,就不能更改现有字段的数据类型,因为Elasticsearch是构建了倒排序索引,试想一下比如userName现在是keyword类型,如果我想改成text类型并且使用ik_max_word分词器分词,如果Elasticsearch能支持修改,那么它需要 将所有的历史数据都使用ik_max_word分词一下再重建索引,如果数据量巨大这个过程是非常缓慢,就像Mysql大表修改索引是非常慢的,所以就不支持了。想要修改字段类型,最好的方法应该是按我之前写的使用别名+reindex方式《EalsticSearch添加字段后重建索引方法》
2.2 删除索引字段
Elasticsearch已经建立好的索引数据是无法直接删除一个字段的,不过可以有两种方式来解决:
- 第一种:是和修改字段类型一样,使用别名+reindex方式重建索引,
- 第二种:
是通过_update_by_query将历史数据中的字段删除掉,这种方法只能删除数据中的userName 字段值,mapping中的userName还是存在的,不过只要数据中没有userName字段其实和删除字段效果是一样的
POST /user/_update_by_query
{
"script": "ctx._source.remove("userName")",
"query": {
"bool": {
"must": [
{
"exists": {
"field": "userName"
}
}
]
}
}
}
2.3 批量删除数据
既然有了_update_by_query,那肯定就有_delete_by_query,我们可以批量删除数据
POST /user/_delete_by_query
{
"query": {
"match_all": {}
}
}
同样_delete_by_query也有很多其它用法,可以参考官方文档: Delete by query API
最后总结:
- 索引可以添加字段
- 索引字段类型不能修改
- 索引字段不能删除
三、焦虑的原因
前面写Elasticsearch不是本文的重点,只是我的引言,我是想让真正爱学习、有耐心看到最后的人能看到接下来的内容,本文的重点我是想聊聊对程序员这个行业的一点看法,为什么这个行业大都数人都非常的焦虑,刚工作的焦虑自己技术差,工作几年的焦虑自己要非升即走,工作很多年的焦虑自己会被裁。我觉得主要的原因就和上面写的案例有点关系。数学老师为什么不焦虑,越老越吃香?因为数学老师的教学经验会随着工作年限的增长而不断积累。比如三角函数有很多很难记的公式如:sin(A+B)=sinAcosB+cosAsinB,对于老师来说在刚工作可能25岁的时候下功夫记住,探索出一套自己的教学方法,在他65岁的时间照样可以用,如果每年还能优化一下教学方法,那么他会在教学岗位上干的越来越顺,教学效果也会越来越好,自然越老越吃香。反观我们这个行业,10年前你可能对SSH框架(struts+spring+hibernate)非常精通,可是现在呢?完全没人用了,不仅不能帮助你,写在简历上别人肯定觉得你比较落后,也就是说你的经验是不能一直积累的。就好比今天上面这个添加字段的案列,可能今天来看Elasticsearch不支持修改字段类型,但是5年后你还敢说Elasticsearch不支持修改字段类型吗?你现在学习的知识也许只能使用5年,5年后现在学习的知识很可能不但不能帮助你 还可能拖累你,而且随着年龄的增长,你的学习的时间、精力和效率还在不断的下降,这放在谁身上都会焦虑的。还拿数据老师教三角函数来说,如果每年都有几个三角函数公式被证明是错误,然后会有新的公式来取代,老的教学方法就不适用了,要探索新的教学方法 那我觉得老师可能比程序员更焦虑。
四、破解之法
4.1、职业选择
选择大于努力,第一步我觉得要认清程序员这个行业的现状,不能因为初期工资高就一门脑袋往里扎,就像最近比较火的太原理工大学2024软件工程招60个班,近2000人,有多少人是了解这个行业的,现在不清楚自己是不是适合这个行业,一门脑子扎进去,以后会随着时间的发展会陷的越来越深,想跳出来也会越来越难。所以我觉得前期要定位自己适不适合这个行业非常重要,我觉得有以下特点的人是非常适合这个行业的:
- 逻辑思维强:编程工作需要严谨的逻辑推理,能将复杂问题拆解成简单的步骤,并用代码实现。
- 耐心 和专注:编程过程中常遇到调试错误、修复bug等琐碎但关键的工作,这需要有足够的耐心和专注力。
- 好奇心和学习 能力:技术更新快,编程语言和框架不断变化,适合做程序员的人通常对新知识感兴趣,并有较强的学习能力。
- 解决问题的能力:编程本质上是解决问题的过程,适合做程序员的人喜欢面对挑战,乐于通过逻辑推理和分析找到解决方案。
- 自我驱动力强:编程项目通常需要独立完成或长时间集中开发,具备自我激励、主动学习的能力尤为重要。
- 注重细节:代码中的小错误可能会导致程序崩溃,适合做程序员的人往往对细节有高度敏感性,善于发现和修复问题。
- 抽象思维:编程需要将现实问题抽象成数据结构和算法,适合做程序员的人通常能在高度抽象的层次上思考问题。
- 沟通与协作能力:尽管编程看似是独立工作,但在实际项目中,程序员需要与产品经理、设计师、其他开发者等团队成员密切合作,清晰的沟通能力是关键。
- 抗压能力:程序开发过程中难免会遇到紧急需求、临时修改或技术难题,良好的抗压能力能够帮助程序员在高压环境下保持冷静。
- 结果导向:编程工作最终是为了实现功能或解决业务需求,适合做程序员的人能够以目标为导向,高效达成任务。
同时我觉得有以下特点的人是不适合干程序员:
- 不喜欢学习新技术:程序员需要不断学习和适应新技术、编程语言、框架等。如果对新知识没有兴趣,或排斥学习,那么很难跟上技术发展的步伐。
- 缺乏耐心和细心:编程需要大量的调试和修复错误,这些过程往往繁琐且时间长。如果缺乏耐心或不注重细节,容易导致代码质量低或频繁出错。
- 逻辑思维较弱:编程本质上是逻辑的工作,如果无法清晰地理解和推理复杂的逻辑问题,或者在面对问题时感觉思路混乱,可能会在编程中遇到较大困难。
- 不喜欢长时间独立工作:程序员的工作常常需要长时间独立思考和编码。如果不喜欢独处或静心工作,可能难以适应编程的工作节奏。
- 抗压能力差:程序开发中常会遇到紧急需求、项目延期、复杂的技术难题等高压情况。如果在压力下容易崩溃或无法保持稳定的心态,可能会影响工作效率和结果。
- 缺乏责任感和自律性:编程工作需要高度的自律和责任感,特别是在解决bug、优化性能时。如果没有足够的责任心,容易出现偷工减料或敷衍了事的情况,进而影响项目的整体质量。
- 不擅长沟通和团队协作:尽管编程常被认为是独立工作,但在实际项目中,需要与团队中的其他角色(如产品经理、测试人员等)频繁沟通。如果不善于沟通或抗拒与他人合作,可能难以融入团队工作环境。
- 厌恶重复性工作:编程虽然涉及创新和解决问题,但也有大量重复性的编码、调试、测试等工作。如果对这些重复性任务感到极度厌烦,可能会降低工作热情和效率。
- 缺乏结果导向:编程最终是为了实现功能和解决问题。如果过于追求完美或沉迷于技术细节,无法在规定时间内交付有用的结果,可能会影响项目进度和团队合作。
- 对电脑和技术无兴趣:程序员需要长时间与电脑打交道,对技术本身的兴趣是持续发展的动力。如果对电脑操作、技术细节等完全无感或厌烦,难以在这个行业长期坚持。
4.2、职业规划
如果你已经选择进入了这个行业,那我觉得必须要有一个清晰的职业规划,毕竟程序员的职业生涯是比较短暂的,黄金时期可能也就十年左右,我觉得程序员的职业规划主要分为三部分,前期工作三年内、中期工作四到七年,后期工作八年以上,这三个阶段的工作重点或者说目标是不一样的。
- 前期(三年内)
这个阶段我觉得是:面向技术开发。因为是刚参加工作,技术能力比较差的,工资也是非常低的,所以我觉得这个时期的工作重点是快速提升自己的技术能力,你所做的一切必须要快速的提升你的技术能力,要快速的多做项目、多学习,如果公司升职加薪通道比较窄,我觉得要果断跳槽, 因为你的技术在快速的提升,如果你的工资和职位增速与你的技术增速不匹配,唯有跳槽。
- 中期(四到七年)
这个阶段我觉得是:面向履历开发。拥有一个好的履历将会受益终生,比如你在BAT待过,比如你参加了某某公司的双十一架构设计,这些写在简历上都是浓墨重彩的一笔。这一时期你的技术基本稳定成长,如果有去大厂的机会,建议还是去大厂。如果没有,我觉得需要在当前公司找到属于自己的位置,要有自己的核心负责产品,能帮助自己稳步晋升,如果没有而且技术也增长不上去、自己学习也没什么动力,我觉得这个时期转型是比较明智的。因为在这时期你大概三十不到,可能没有结婚,没有房贷,不需要养娃,也不用养老人,自己应该也有点积蓄,是工作后最没有压力的时期,所以有大把的时间和精力来寻找机会。如果到了后期,可能有房贷、车贷、养娃、养老人,压力是非常大的,没有太多的空窗期让你转型,你很可能就只能向生活低头,在公司一直苟着,担心自己终于有一天被裁,也会越来越焦虑,这就是所谓的中年危机,这一时期也是避免后期中年危机的最好时间段。
- 后期 (八年以上)
这个阶段如果还能做技术,我觉得是:面向管理开发。因为这个阶段你在技术方面,如果不是那种不断学习进步,你的技术可能慢慢不如新人,你的技术慢慢变的没有优势,你对公司的价值也在慢慢变小,但是能在公司项目组留下来的都是元老级人物,对公司的环境、 业务、领导、同事、甚至行业都非常的了解,公司是需要一个有经验和公司一起成长的人来管理、带团队的,所以这时期我觉得工作重点是要了解公司的整体业务、行业的发展,要提升自己管理能力,向管理岗位不断靠近。但是管理岗位毕竟是少数,如果自己没有管理能力该怎么办?破解之法 就是:副业。我有很多同事都是副业转正,现在赚的可不是打工人每月这点工资了。作为程序员具体的副业就太多了,常见的如:做外包、做开源、做自建站、写工具、做自媒体,还有一点要重点说的是:程序员一定要关注海外市场,还记录承德的程序员Github接单被没收百万收入并处以罚款的新闻吗?国内是个小市场,只有打开国际视野你才能看到一个更大的市场,会有更多的机会。
最后
以上仅仅是我个人的一些浅薄观点,不一定适合你,需要根据自己的实际情况多加思考,今年的整体行情非常的差,在可预见的几年内可能都不会有太大的转变,未来程序员这个行业可能会越来越卷。我也在一直思考自己在这个行业的定位,和未来的发展方向, 其实也没能找到自己的答案,也是走一步,看一步,再想一步。未来行情是卷的,但是中国在未来十年内随着人口的减少,必须要加大数字化和智能化建设,所以只要你不断的学习,哪怕学习一项新技术生命周期只有五年,未来十年内这个行业找口饭吃还是不难的, 如何互联网赛道太卷,也可以换个赛道,比如去传统行业做数字化,目前传统行业对程序员需求量非常大,互联网过去的都算是高级人才,而且很有可能比在互联拿的更多!!
来源:juejin.cn/post/7403576996394385444
社会现实告诉我,00后整顿职场就是个笑话
00后整顿职场,也算是我之前的关键词吧。
我硬怼老板要加班费和提成,和他们辩论什么是我的财产,什么是公司的财产。
甚至还能在即将被开除的时候,反将一军把老板开除。
而正是因为这一次把老板开除,让我得到了机会。可以站在了相对于之前更高的位置,来俯瞰整个职场。
也真正意义上让我感受到了,00后整顿职场,就是一个互联网笑话罢了。
1、职场宫斗,成功上位
我之前在苏州工作,堪称工作中的宫斗,并且在这场宫斗大戏中胜出,将原有的项目负责人开除,成功上位。
而这个项目存在的问题非常多,我就在六月被派遣去项目的总部合肥进行学习,等到打通项目的全部链路后,再回到苏州。
届时我将以这个项目的负责人,重新搭建团队,开展这个项目。所以我在合肥那边,以员工的身份深入各个工作组进行学习。
在市场部,运营部的办公大厅工作过,也在各部门的独立办公室工作过。
我感觉自己像个间谍,一边在以平级的打工人身份和我的同事们相处,一边又以苏州负责人的身份,参与那些领导才能参与的内部会议。
2、内心变化的开端
我在合肥总部工作中,接触了很多躺平摆烂的同事,但这个“躺平摆烂“要加上双引号。
他们是00后,90后,甚至有85后。如果放在三个月前,我可以不假思索地说,他们全都是我最讨厌的人。他们如同牛羊一般任人宰割,上级让加班,他们就加班,有时候加班甚至超过四五个小时也没有怨言。
我甚至从来没听他们感慨过为什么没有加班费。亲眼看着他们被自己的上级用一些与工作无关的鸡毛蒜皮之事骂得狗血淋头,但他们也只会在被骂完之后,背地里吐槽那个领导估计是在家被老婆骂了,才来拿他们泄愤。
我打听了他们的工资,只能说中规中矩,起码不是能让人当牛做马的数字。偶尔我见到一两个有骨气的人,觉得拿这么点钱就应该干这么点事。干不爽就马上离职,但马上就会有下一个人替补他的位置,形成闭环。
我惊讶于怎么有人能惹到这个地步,但后来和他们日渐熟落,我们一起吃饭,一起打游戏,一起下班顺路回家,还参加了他们的生日聚会。我发现他们活得其实真的很洒脱。一切都是随遇而安,下班时间一到,他们就真的可以无忧无虑。
因为他们有一份工资还行的工作,养活自己。他们没有啃老,也没有用卑鄙的手段,去抢想要努力的人应该分到的蛋糕,也压根不去想要赚很多钱,因为没有什么需要太高消费的需求。
加上现在的环境,找到一份可观收入的工作确实很难。所以公司偶尔的加班,领导偶尔的泄愤,这些毕竟还是少数时候的偶尔,也都没有超过他们的心理承受阈值,那也就得过且过了。
所以我们其实都一样,只是个普通人罢了。而像我们这样的普通人,取之不尽,用之不竭。这到底是好事还是坏事呢?
3、复杂的职场生态环境
建立在这个基础上,视觉转换到高层领导们这里。他们当着我的面说,这样的人就是个底层打工仔,缺人就招,加班照旧,心情不好还要扣他们的全勤绩效。
压根就不怕这些底层打工仔闹事,纵使有一两个所谓的决心者辞职,也能在很快时间找到下一位。
两者形成互补,共同铸就了这样恶劣的职场生态环境。但我说职场无法改变,远不止这么一点原因。
在这个项目中,我说好听一些只能算是项目负责人,在此之上还有着项目股东,这还要细分成大股东和小股东。而我所在的项目属于互联网赛道,也就是说需要一些新鲜事物的眼光和思维来对待。
但这些股东们经常提出一些奇怪的意见,就如同用微商时代的卖货思维,来指点直播带货,并且他们是出钱的股东,他们提出的战略方针不容我驳回,因为在他们的光辉历史中,有大量的成功案例,来佐证他们的思路是对的。
我刚开始觉得也有道理。他们能有钱投资,肯定是有什么过人的本领能让他们赚到钱,但是随着相处下来,我发现不过是他们本身家里条件就优越,在九几年就能拿出一百万给他们创业。
他们把这一百万分散到二十个领域,每个投资五万总能撞上那么一两个风口,让他们实现钱生钱。
九几年的五万也算是一笔不少的投资。他们这样的发财经历,让我很难不产生质疑,这不是给我我也行吗?
毕竟他们如果真的有什么过人的本领和远见,也不至于在每次内部开会之前,都要组织喊这样的口号:“好,很好,非常好,越来越好“
甚至试图把这样的口号,带到每一次迎接客户的项目介绍会上。我以自曝式要挟制止他们这个行为,我说如果你们这么干,那我当天就辞职,内部都是自己人,我可以陪你们这样弄,但如果对外这么搞,被录下来说我们是传销,我都不知道怎么辩解。
4、职场中的背锅人
他们就是这样坚信着自己能成功,是因为自己有过人的才华。所以自我洗脑着自己提出的方向没有错。如果出错了,亏损了,那一定是负责人的问题。
但好巧不巧,我就是那个负责人。我已经无数次告诉他们,我们这个项目压根就不需要穿黑丝短裙跳舞的小姐姐。
我甚至写了一篇报告给他们,分析我们的项目为什么不能用擦边这种手段引流。但他们执意要,说这样来流量快,我都有点分不清到底是他们自己想看,还是深信这样做确实是可行。
但如果最后这样还是没成功,导致项目亏损,大概率还是在我身上找原因吧。
面对他们这样的大佬,我心里很清楚,这已经远远不是宫斗了,这也绝对不是靠几个心计,或者有实力撑腰就能取胜上位了。这场权力的游戏,不是我等草民玩得起的。
5、换个思路,创造属于自己的职场
一边是被提供资金,但是瞎指挥的股东们摧残,一边是在有限的预算下,我作为负责人,确实很难做到尊重打工人的内心挣扎,回到苏州我虽然能身居高位,但我终将成为我曾经最鄙视的人。
我不要当这个背锅侠,我也不想在这个环境中,去逐渐接受这样的价值观。
这样看来确实如此,00后整顿职场不过是一场互联网的狂欢罢了。
这个题材的故事,也永远只能发生在职场的最底层。由一群家境优越,体验生活的公子哥和我这种不知好歹的普通人共同出演。
大部分人只是在手机屏幕前把我们当个乐子,成了扣个666,然后一起吃胜利的果实。没成,那就确实是看了个乐子。
或许是因为他们心里也清楚,凭我们压根就做不到。
00后现在确实整顿不了职场,因为社会的资源和命脉还不掌握在00后手上。
但就止步于此了吗?我曾说过我想有一个自己的小工作室,遵守劳动法,双休,按时发工资,交纳五险一金。
是的,换个思路,也许00后不需要整顿职场,而是直接创造属于自己的职场,那么接下来我就要向着这个目标去努力了,毕竟二十年后我也还是00后,不如到时候再来说00后整顿职场吧。
来源:juejin.cn/post/7311603432929984552
Android ConstraintLayout使用进阶
前言
曾经Android有五大布局,LinearLayout、FrameLayout、TableLayout、RelativeLayout和AbsoluteLayout,那会我们比较常用的布局就两三个,写xml的时候根据界面灵活选择布局,但是往往会面临布局嵌套过深的问题,阅读也不方便。随着Android生态的发展,Google后来推出了新的布局——ConstraintLayout(约束布局)。
我很快去学习并将其用在项目中,刚开始的时候觉得比较抽象难懂,各种不适应;一段时间过后,这玩意儿真香!
本文不讲ConstraintLayout基本使用(网上资料很多),而是关于使用ConstraintLayout的进阶。
导入依赖:(2.x版本)
implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
进阶1
在开发中可能需要实现如下效果:
文本外层有背景,短文本的时候宽度自适应,长文本超过屏幕的时候,背景贴右边,文字显示...,这样的UI需求很常见,我们来一步步拆解。
1、文本背景需要占满屏幕,并且文本显示...
<TextView
android:layout_width="0dp"
android:ellipsize="end"
android:maxLines="1"
android:singleLine="true"
android:background="@drawable/xxx"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
/>
2、这时候TextView会水平居中,我们需要添加
app:layout_constraintHorizontal_bias="0"
layout_constraintHorizontal_bias表示水平偏移,即“当组件左侧和右侧 ( 或者 开始 和 结束 ) 两边被约束后, 两个联系之间的比例”,取值为0-1,具体看ConstraintLayout 偏移 ( Bias ) 计算方式详解,我们只需要将水平偏移量设置为0,控件就会被约束在左侧了。
3、最后一步,短文本的时候宽度自适应,长文本的时候占满屏幕,需要添加
app:layout_constraintWidth_max="wrap"
layout_constraintWidth_max表示指定视图的最大宽度,取值为“wrap”,它和“wrap_content”不同,虽然都是适应内容,但仍然允许视图比约束要求的视图更小。
最终代码:
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/xxx"
android:ellipsize="end"
android:maxLines="1"
android:singleLine="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintWidth_max="wrap"
tools:text="这是一个测试文案"
/>
进阶2
再来看个效果图:
还是文本适配的问题,短昵称的时候自适应,长昵称的时候,性别图标跟随文本长度移动,但是图标必须在“聊天”按钮左侧,文本显示...
我们再来一步步拆解(仅针对昵称Textview):
一、重复上面的步骤1和步骤2,代码如下(注意layout_width="wrap_content",上面的是0dp)
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我是昵称"
android:singleLine="true"
android:ellipsize="end"
android:maxLines="1"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/iv_head"
app:layout_constraintEnd_toStartOf="@id/iv_gender"
/>
二、这时候我们会发现布局是居中的,而且昵称TextView都需要收尾元素相连,我们可以使用layout_constraintHorizontal_chainStyle改变整条链的约束状态,它有三个值,分别是spread、spread_inside和packed,其中packed表示将所有 Views 打包到一起不分配多余的间隙(当然不包括通过 margin 设置多个 Views 之间的间隙),然后将整个组件组在可用的剩余位置居中(可以查看Chains链布局),同时由于layout_constraintHorizontal_bias="0"的作用,布局将会向左侧偏移。
app:layout_constraintHorizontal_chainStyle="packed"
三、最后,当我们输入文本时,发现文本并没有约束到“聊天”按钮左侧,因为layout_width="wrap_content",添加的约束是不起作用的,所以需要强制约束
app:layout_constrainedWidth="true"
代码动态改变约束
初始约束:
修改后的约束:
如上图,初始状态,中间按钮约束在按钮1右侧,某个条件下需要将中间按钮约束在按钮2左侧,这种时候,我们就需要在代码动态设置约束了。
具体代码:
constraintLayout?.let {
//初始化一个ConstraintSet
val set = ConstraintSet()
//将原布局复制一份
set.clone(it)
//分别将“中间按钮”START方向和BOTTOM方向的约束清除
set.clear(“中间按钮”, ConstraintSet.START)
set.clear(“中间按钮”, ConstraintSet.BOTTOM)
//重新建立新的约束
//“中间按钮”的END约束“按钮2”控件的START
//相当于 app:layout_constraintEnd_toStartOf="@id/按钮2"
set.connect(
“中间按钮”,
ConstraintSet.END,
“按钮2”,
ConstraintSet.START,
resources.getDimensionPixelSize(R.dimen.dp_9)
)
//以及底部方向的约束
...
//最后将更新的约束应用到布局
set.applyTo(it)
}
MotionLayout
接下来是今天重头戏——MotionLayout。
MotionLayout继承自ConstraintLayout,能够通过约束关系构建丰富的view动画,动画状态分为start与end两个状态,它还能作为支持库,兼容到api 14。
来看下效果图,这是我司App某个页面的动画效果,就是用MotionLayout实现。
我们可以写个简单的demo实现上面一部分动画效果,如下图
首先我们需要在资源文件夹 res 下新建一个名为 xml 的资源文件夹,然后在 文件夹内新建一个根节点是 MotionScene 的 xml 文件,文件名为 test_motion_scene.xml,如下:
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">
</MotionScene>
activity的xml根布局改为MotionLayout,使用app:layoutDescription与之关联
再编写视图,定义视图具体的view和对应id
<androidx.constraintlayout.motion.widget.MotionLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:layoutDescription="@xml/test_motion_scene"
...
>
<ImageView
android:id="@+id/iv_head"
...
/>
<TextView
android:id="@+id/tv1"
...
/>
然后切换到test_motion_scene.xml,我们需要明确动画布局的两个状态,start和end。
在MotionScene标签下定义Transition标签,指定动画的start和end状态
<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="500">
</Transition>
之后,在Transition同级下再定义ConstrainSet标签,它表示用于指定所有视图在动画序列中某一点上的位置和属性,你可以把它理解成一个集合,集合了所有参与动画的view相关位置和属性,如下:
<?xml version="1.0" encoding="utf-8"?>
<MotionScene
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">
<Transition>
...
</Transition>
<ConstraintSet android:id="@+id/start">
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
</ConstraintSet>
</MotionScene>
大体的框架搭建好了,最后就是填充约束view状态的代码了。这时候我们需要明确动画的start状态和end状态,即
(start状态)↓
(end状态)↓
前面提到,ConstraintSet是存放一些view 约束和属性的的集合,而具体描述View约束和属性是通过Constraint 标签。我们声明Constraint标签,它支持一组标准 ConstraintLayout 属性,用于添加每个view start状态的约束。
<ConstraintSet android:id="@+id/start">
<Constraint
<!-- "android:id"表示activity的xml对应的view id
android:id="@id/iv_head"
android:layout_width="90dp"
android:layout_height="90dp"
motion:layout_constraintTop_toTopOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintEnd_toEndOf="parent"/>
<Constraint
android:id="@id/iv1"
.../>
<Constraint
android:id="@id/iv2"
.../>
...
</ConstraintSet>
接下来以同样的方式添加end状态的view约束
<ConstraintSet android:id="@+id/end">
...
</ConstraintSet>
最后,我们需要让它动起来,在Transition标签写添加一个OnClick标签,run,就能让动画动起来
<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="1000">
<!-- 点击-->
<OnClick
motion:clickAction="toggle"
motion:targetId="@id/search_go_btn"/>
</Transition>
OnClick:表示由用户点击触发
属性:
motion:targetId="@id/target_view" (目标View的id)
如果不指定次属性,就是点击整个屏幕触发如果写了这个属性,就是点击对应id的View 触发转场动画
motion:clickAction=“action” 点击后要进行的行为 ,此属性可以设置以下几个值:
transitionToStart
过渡到 元素 motion::constraintSetStart 属性指定的状态,有过度动画效果。
transitionToEnd
过渡到 元素motion:constraintSetEnd 属性指定的状态,有过度动画效果。
jumpToStart
直接跳转到 元素 motion::constraintSetStart 属性指定的状态,没有动画效果。
jumpToEnd
直接跳转到 元素 motion:constraintSetEnd 属性指定的状态。
toggle
默认值就是这个,在 元素motion:constraintSetStart和 motion:constraintSetEnd 指定的布局之间切换,如果处于start状态就过度到end状态,如果处于end状态就过度到start状态,有过度动画。
除了OnClick之外,还有OnSwipe,它是根据用户滑动行为调整动画的进度,具体可查看文末资料。
改变动画运动过程(关键帧KeyFrameSet)
上面讲解了动画的start与end状态,但是如果我们想在动画运动过程去改变一些属性,比如设置view的透明度、旋转,又或者是改变动画运动过程的轨迹等,这时候可以用到关键帧。
KeyFrameSet是Transition的子元素,与OnClick、OnSwipe同级。KeyFrameSet中可以包含KeyPosition、KeyAttribute、KeyCycle、KeyTimeCycle、KeyTrigger,它们都可以用来改变动画过程。
此外还有与KeyFrameSet同级的KeyPosition、KeyAttribute,具体大家根据需要自行了解即可。
最后再提一下MotionLayout一些常用的java api:
loadLayoutDescription() ——对应xml"app:layoutDescription",通过代码加载MotionScene;
transitionToStart() ——表示切换到动画start状态;
transitionToEnd() ——表示切换到动画end状态;
它们都默认有过渡效果,如果不需要过渡效果,可以通过**setProgress(float pos)**处理过渡进度,取值0-1;
transitionToState(int id) ——表示切换到动画某个状态,可以是start也可以是end,参数id指的是ConstraintSet标签定义的id;
setTransitionListener(MotionLayout.TransitionListener listener) ——监听MotionLayout动画执行过程,接口有四个方法,onTransitionStarted、onTransitionChange、onTransitionCompleted、onTransitionTrigger。
OK,最最后,ConstraintLayout能有效提升日常的开发效率,通过这篇文章的介绍,此刻你学废了嘛~
参考
ConstraintLayout / MotionLayout GitHub示例
来源:juejin.cn/post/6886337167279259661
开发小同学的骚操作,还好被我发现了
大家好,我是程序员鱼皮。今天给朋友们还原一个我们团队真实的开发场景。
开发现场
最近我们编程导航网站要开发 用户私信
功能,第一期要做的需求很简单:
- 能让两个用户之间 1 对 1 单独发送消息
- 用户能够查看到消息记录
- 用户能够实时收到消息通知
这其实是一个双向实时通讯的场景,显然可以使用 WebSocket 技术来实现。
团队的后端开发小 c 拿到需求后就去调研了,最后打算采用 Spring Boot Starter 快速整合 Websocket 来实现,接受前端某个用户传来的消息后,转发到接受消息的用户的会话,并在数据库中记录,便于用户查看历史。
小 c 的代码写得还是不错的,用了一些设计模式(像策略模式、工厂模式)对代码进行了一些抽象封装。虽然在我看来对目前的需求来说稍微有点过度设计,但开发同学有自己的理由和想法,表示尊重~
前端同学小 L 也很快完成了开发,并且通过了产品的验收。
看似这个需求就圆满地完成了,但直到我阅读前端同学的代码时,才发现了一个 “坑”。
这是前端同学小 L 提交的私信功能代码,看到这里我就已经发现问题了,朋友们能注意到么?
解释一下,小 L 引入了一个 nanoid
库,这个库的作用是生成唯一 id。看到这里,我本能地感到疑惑:为什么要引入这个库?为什么前端要生成唯一 id?
难道。。。是作为私信消息的 id?
果不其然,通过这个库在前端给每个消息生成了一个唯一 id,然后发送给后端。
后端开发的同学可能会想:一般情况下不都是后端利用数据库的自增来生成唯一 id 并返回给前端嘛,怎么需要让前端来生成呢?
这里小 L 的解释是,在本地创建消息的时候,需要有一个 id 来追踪状态,不会出现消息没有 id 的情况。
首先,这么做的确 能够满足需求 ,所以我还是通过了代码审查;但严格意义上来说,让前端来生成唯一 id 其实不够优雅,可能会有一些问题。
前端生成 id 的问题
1)ID 冲突:同时使用系统的前端用户可能是非常多的,每个用户都是一个客户端,多个前端实例可能会生成相同的 ID,导致数据覆盖或混乱。
2)不够安全:切记,前端是没有办法保证安全性的!因为攻击者可以篡改或伪造请求中的数据,比如构造一个已存在的 id,导致原本的数据被覆盖掉,从而破坏数据的一致性。
要做这件事成本非常低,甚至不需要网络攻击方面的知识,打开 F12 浏览器控制台,重放个请求就行实现:
3)时间戳问题:某些生成 id 的算法是依赖时间戳的,比如当前时间不同,生成的 id 就不同。但是如果前端不同用户的电脑时间不一致,就可能会生成重复 id 或无效 id。比如用户 A 电脑是 9 点时生成了 id = 06030901,另一个用户 B 电脑时间比 A 慢了一个小时,现在是 8 点,等用户 B 电脑时间为 9 点的时候,可能又生成了重复 id = 06030901,导致数据冲突。这也被称为 “分布式系统中的全局时钟问题”。
明确前后端职责
虽然 Nanoid 这个库不依赖时间戳来生成 id,不会受到设备时钟不同步的影响,也不会因为时间戳重复而导致 ID 冲突。根据我查阅的资料,生成大约 10 ^ 9 个 ID 后,重复的可能性大约是 10 ^ -17,几乎可以忽略不计。但一般情况下,我个人会更建议将业务逻辑统一放到后端实现,这么做的好处有很多:
- 后端更容易保证数据的安全性,可以对数据先进行校验再生成 id
- 前端尽量避免进行复杂的计算,而是交给后端,可以提升整体的性能
- 职责分离,前端专注于页面展示,后端专注于业务,而不是双方都要维护一套业务逻辑
我举个典型的例子,比如前端下拉框内要展示一些可选项。由于选项的数量并不多,前端当然可以自己维护这些数据(一般叫做枚举值),但后端也会用到这些枚举值,双方都写一套枚举值,就很容易出现不一致的情况。推荐的做法是,让后端返回枚举值给前端,前端不用重复编写。
所以一般情况下,对于 id 的生成,建议统一交给后端实现,可以用雪花算法根据时间戳生成,也可以利用数据库主键生成自增 id 或 UUID,具体需求具体分析吧~
来源:juejin.cn/post/7376148503087169562
HR的骚操作,真的是卧龙凤雏!
现在基本已经对通过面试找工作不抱啥希望了。
有时候面试官和我聊的,还没有前面hr小姐姐和我聊的多,我一听开场白就基本知道就是拿我走个过场,没戏!
现在的面试流程都是人事先和你聊半天,没什么硬伤大坑才会放你去见面试官。
二零一几年那会可不是这样,第一次的详聊都是直接业务层,业务的人觉得你ok,你再和人事沟通,定个薪资就完了。
13年的时候我在一家外企,三千的月薪。当时我一个小目标就是月薪过五千。
可别笑,13年的月薪五千,那还是能勉强算上一个小白领的。
我就老琢磨着升职加薪。但眼下的公司规模小,人员基本不扩增,不流通,我就想跳槽了。
当时我同时面了AB两家外资游戏公司。都过了业务层的面试,只剩和人事定薪资。
我给A公司报价5500,给B公司报价6000,因为我知道B公司刚来国内开拓业务,属于扩张期。
这时候,A公司HR的骚操作就来了,她说:“嗯,5500嘛,有难度,但不是不可能,我可以帮你跟老板争取。”
然后又问我:“你已经从现在的公司里面离职了吗?”
我说:“还没呢,我想先把下家定了。”
她就说:“哎呀,那有点难办,你得先从现在这家公司离职,我得确保我帮你争取下来后,你不会鸽我,不然我没法和老板交代,要不你先把那边离职办了吧。”
我说:“那我再考虑考虑吧。”
然后没过两天,我收到了B公司的offer。人家都没还价,直接定了6000,我就开始走离职手续。
这时候A公司的HR又出来问我,你从现在的公司离职了吗?
我说离了,她说你给我看看离职证明,我就拍照给她看离职证明。
然后她连假装让我等一会儿,说自己去问一下老板的戏都不演了,直接秒回说:“我帮你问了老板了,老板说不行,5500给不了,最多给到4500,要不你先入职呢,后面有机会提加薪的。”
瞬间给我恶心的,怎么会有这么恶心的人事!先把你忽悠离职,然后翻脸不认人,可劲往下砍价,为了公司的KPI,自己做人的脸都不要了。
我当时就觉得这样的人真傻,就算我认了4500的杀价入了职,我把和她的对话记录公司群里一发,老板会怎么看她,同事会怎么看她。
咱做人得有底线呀,你用这种脏办法帮公司省那几百块钱,还把自己的名声信誉搭进去了,真的值得吗?
后来我在入职B公司差不多半年后,传来了A公司解散倒闭的消息,我心里还暗爽了一把,幸亏当年没进那个火坑。
但半年后,我所在的B公司也解散了。
2013年那是一个手游刚兴起的疯狂年代,数不清的大小公司起家,创业,失败,解散,换批核心班子,再起家,再失败,浮浮沉沉,我也成了疯狂年代下的沧海一粟。
来源:juejin.cn/post/7426685644230213643
初中都没念完的我,是怎么从IT这行坚持下去的...
大家好,我是一名二线(伪三线,毕竟连续两年二线城市了)的程序员。
现阶段状态在职,28岁,工作了10年左右,码农从事了5年左右,现薪资9k左右。如文章标题所说,初二辍学,第一学历中专,自己报的成人大专。
在掘金也看了不少经历性质的文章,大多都是很多大牛的文章,在大城市的焦虑,在大厂的烦恼,所以今天换换口味,看一看我这个没有学历的二线的程序员的经历。
1.辍学
我是在初二的时候辍学不上的,原因很简单,太二笔了。
现在想来当时的我非常的der,刚从村里的小学出来上中学之后(我还是年级第7名进中学,殊不知这就是我这辈子最好的成绩了),认为别人欺负我我就一定要还回来,完全不知道那是别人的地盘,嚣张的一批,不出意外就被锤了,但是当时个人武力还是很充沛的,按着一个往地上锤,1V7的战绩也算可以了。自此之后,我就开始走上了不良的道路,抽烟喝酒打架,直到中专毕业那天。
我清楚的记得我推着电车望着天,心里只想着一个问题,我毕业了,要工作了,我除了打游戏还会什么呢,我要拿什么生存呢...
这是当时我心里真实的想法,我好像就在这一刻、这一瞬间长大了。
2.深圳之旅
因为我特别喜欢玩游戏,而且家里电脑总是出问题,所以我就来到了我们这当地的一个电脑城打工,打了半年工左右想学习一下真正的维修技术,也就是芯片级维修,毅然决然踏上了深圳的路。
在深圳有一家机构叫做迅维的机构,还算是在业内比较出名的这么一个机构,学习主板显卡的维修,学习电路知识,学习手机维修的技术。现在的我想想当时也不太明白我怎么敢自己一个人就往深圳冲,家里人怎么拦着我都没用,当时我就好像着了魔一样必须要去...
不过在深圳的生活真的很不错,那一年的时光仍旧是我现在非常怀念的,早晨有便宜好吃的肠粉、米粉、甜包,中午有猪脚饭、汤饭、叉烧饭,晚上偶尔还会吃一顿火锅,来自五湖四海的朋友也是非常的友好,教会了我很多东西,生活非常的不错。
3.回家开店
为什么说我工作了10年左右呢,因为我清楚记得我18岁那年在本地开了一个小店,一个电脑手机维修的小店。现在想想我当时也是非常的二笔,以下列举几个事件:
- 修了一个显示器因为没接地线烧了,还跟人家顾客吵了一架。
- 修苹果手机翘芯片主板线都翘出来了,赔了一块。
- 自己说过要给人家上门保修,也忘了,人家一打电话还怼了一顿。
- 因为打游戏不接活儿。
以上这几种情况比比皆是,哪怕我当时这么二笔也是赚了一些钱,还是可以维持的,唯一让我毅然决然转行的就是店被偷了,大概损失了顾客机器、我的机器、图纸、二手电脑等一系列的商品,共计7万元左右,至今仍没找回!
4.迷茫
接下来这三年就是迷茫的几年了,第一件事就是报成人大专,主要从事的行业就杂乱无章了,跟我爸跑过车,当过网吧网管,超市里的理货员,但是这些都不是很满意,也是从这时候开始接触了C和C++开始正式踏入自学编程的路,直到有一次在招聘信息里看到java,于是在b站开始自学java,当时学的时候jdk还是1.6,学习资料也比较古老,但是好歹是入了门了。
5.入职
在入门以后自我感觉非常良好,去应聘了一个外包公司,当时那个经理就问了我一句话,会SSM吗,我说会,于是我就这么入职了,现在想想还是非常幸运的。
当时的我连SSM都用不明白,就懂一些java基础,会一些线程知识,前端更是一窍不通,在外包公司这两年也是感谢前辈带我做一些项目,当时自己也是非常争气,不懂就学,回去百度、b站、csdn各种网站开始学习,前端学习了H5、JS、CSS还有一个经典前端框架,贤心的Layui。
干的这两年我除了学习态度非常认真,工作还是非常不在意,工作两年从来没有任何一个月满勤过,拖延症严重,出现问题从来就是逃避问题,职场的知识是一点也不懂,当时的领导也很包容我,老板都主持了我的婚礼哈哈哈。但是后来我也为我的嚣张买了单,怀着侥幸心理喝了酒开车,这一次事情真真正正的打醒了我,我以后不能这样了...
6.第二家公司
在第二家公司我的态度就变了很多很多 当时已经25岁了,开始真真正正是一个大人了,遵纪守法,为了父母和家人考虑,生活方面也慢慢的好了起来(在刚结婚两年和老婆经常吵架,从这时候开始到现在没有吵过任何架了就),生活非常和睦。工作方面也是从来不迟到早退,听领导的安排,认真工作,认真学习,认识了很多同行,也得到了一些人的认可,从那开始才开始学习springboot、mq、redis、ES一些中间件,学习了很多知识,线程知识、堆栈、微服务等一系列的知识,也算是能独当一面了。但好景不长,当时我的薪资已经到13K左右了,也是因为我们部门的薪资成本、服务器成本太大,入不敷出,公司决定代理大厂的产品而不是自研了,所以当时一个部门就这么毕业了...
7.现阶段公司
再一次找工作就希望去一些自研的大公司去做事情了,但是也是碍于学历,一直没有合适的,可以说是人厌狗嫌,好的公司看不上我,小公司我又不想去,直到在面试现在公司的时候聊得非常的好,也是给我个机会,说走个特批,让我降薪入职,算上年终奖每个月到手大概10k(构成:9k月薪,扣除五险一金到手7.5k,年终奖27k,仨月全薪,所以每个月到手10k),我也是本着这个公司非常的大、非常的稳定、制度非常健全、工作也不是很忙也就来了,工作至今。
总结
- 任何时候想改变都不晚,改变不了别人改变自己。
- 面对问题绝对不能逃避,逃避没有任何用,只有面对才能更好的继续下去。
- 不要忘了自己为什么踏入这行,因为我想做游戏。
- 解决问题不要为了解决而解决,一定要从头学到尾,要不然以后出现并发问题无从下手。
- 任何事情都要合规合法。
- 工作了不要脱产做任何事情,我是因为家里非常支持,我妈至今都难以相信我能走到今天(我认为我大部分是运气好,加上赶上互联网浪潮的尾巴)。
- 最重要的,任何事情都没有家人重要,想回家就回家吧,挣钱多少放一边,IT行业找个副业还是非常简单的,多陪陪他们!
来源:juejin.cn/post/7309645869644480522
请不要自己写,Spring Boot非常实用的内置功能
在 Spring Boot 框架中,内置了许多实用的功能,这些功能可以帮助开发者高效地开发和维护应用程序。
松哥来和大家列举几个。
一 请求数据记录
Spring Boot提供了一个内置的日志记录解决方案,通过 AbstractRequestLoggingFilter
可以记录请求的详细信息。
AbstractRequestLoggingFilter
有两个不同的实现类,我们常用的是 CommonsRequestLoggingFilter
。
通过 CommonsRequestLoggingFilter
开发者可以自定义记录请求的参数、请求体、请求头和客户端信息。
启用方式很简单,加个配置就行了:
@Configuration
public class RequestLoggingConfig {
@Bean
public CommonsRequestLoggingFilter logFilter() {
CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter();
filter.setIncludeQueryString(true);
filter.setIncludePayload(true);
filter.setIncludeHeaders(true);
filter.setIncludeClientInfo(true);
filter.setAfterMessagePrefix("REQUEST ");
return filter;
}
}
接下来需要配置日志级别为 DEBUG,就可以详细记录请求信息:
logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG
二 请求/响应包装器
2.1 什么是请求和响应包装器
在 Spring Boot 中,请求和响应包装器是用于增强原生 HttpServletRequest
和 HttpServletResponse
对象的功能。这些包装器允许开发者在请求处理过程中拦截和修改请求和响应数据,从而实现一些特定的功能,如请求内容的缓存、修改、日志记录,以及响应内容的修改和增强。
请求包装器
ContentCachingRequestWrapper
:这是 Spring 提供的一个请求包装器,用于缓存请求的输入流。它允许多次读取请求体,这在需要多次处理请求数据(如日志记录和业务处理)时非常有用。
响应包装器
ContentCachingResponseWrapper
:这是 Spring 提供的一个响应包装器,用于缓存响应的输出流。它允许开发者在响应提交给客户端之前修改响应体,这在需要对响应内容进行后处理(如添加额外的头部信息、修改响应体)时非常有用。
2.2 使用场景
- 请求日志记录:在处理请求之前和之后记录请求的详细信息,包括请求头、请求参数和请求体。
- 修改请求数据:在请求到达控制器之前修改请求数据,例如添加或修改请求头。
- 响应内容修改:在响应发送给客户端之前修改响应内容,例如添加或修改响应头,或者对响应体进行签名。
- 性能测试:通过缓存请求和响应数据,可以进行性能测试,而不影响实际的网络 I/O 操作。
2.3 具体用法
请求包装器的使用
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class RequestWrapperFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
// 可以在这里处理请求数据
byte[] body = requestWrapper.getContentAsByteArray();
// 处理body,例如记录日志
//。。。
filterChain.doFilter(requestWrapper, response);
}
}
响应包装器的使用
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class ResponseWrapperFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
filterChain.doFilter(request, responseWrapper);
// 可以在这里处理响应数据
byte[] body = responseWrapper.getContentAsByteArray();
// 处理body,例如添加签名
responseWrapper.setHeader("X-Signature", "some-signature");
// 必须调用此方法以将响应数据发送到客户端
responseWrapper.copyBodyToResponse();
}
}
在上面的案例中,OncePerRequestFilter
确保过滤器在一次请求的生命周期中只被调用一次,这对于处理请求和响应数据尤为重要,因为它避免了在请求转发或包含时重复处理数据。
通过使用请求和响应包装器,开发者可以在不改变原有业务逻辑的情况下,灵活地添加或修改请求和响应的处理逻辑。
三 单次过滤器
3.1 OncePerRequestFilter
OncePerRequestFilter
是 Spring 框架提供的一个过滤器基类,它继承自 Filter
接口。这个过滤器具有以下特点:
- 单次执行:
OncePerRequestFilter
确保在一次请求的生命周期内,无论请求如何转发(forwarding)或包含(including),过滤器逻辑只执行一次。这对于避免重复处理请求或响应非常有用。 - 内置支持:它内置了对请求和响应包装器的支持,使得开发者可以方便地对请求和响应进行包装和处理。
- 简化代码:通过继承
OncePerRequestFilter
,开发者可以减少重复代码,因为过滤器的执行逻辑已经由基类管理。 - 易于扩展:开发者可以通过重写
doFilterInternal
方法来实现自己的过滤逻辑,而不需要关心过滤器的注册和执行次数。
3.2 OncePerRequestFilter 使用场景
- 请求日志记录:在请求处理之前和之后记录请求的详细信息,如请求头、请求参数和请求体,而不希望在请求转发时重复记录。
- 请求数据修改:在请求到达控制器之前,对请求数据进行预处理或修改,例如添加或修改请求头,而不希望这些修改在请求转发时被重复应用。
- 响应数据修改:在响应发送给客户端之前,对响应数据进行后处理或修改,例如添加或修改响应头,而不希望这些修改在请求包含时被重复应用。
- 安全控制:实现安全控制逻辑,如身份验证、授权检查等,确保这些逻辑在一次请求的生命周期内只执行一次。
- 请求和响应的包装:使用
ContentCachingRequestWrapper
和ContentCachingResponseWrapper
等包装器来缓存请求和响应数据,以便在请求处理过程中多次读取或修改数据。 - 性能监控:在请求处理前后进行性能监控,如记录处理时间,而不希望这些监控逻辑在请求转发时被重复执行。
- 异常处理:在请求处理过程中捕获和处理异常,确保异常处理逻辑只执行一次,即使请求被转发到其他处理器。
通过使用 OncePerRequestFilter
,开发者可以确保过滤器逻辑在一次请求的生命周期内只执行一次,从而避免重复处理和潜在的性能问题。这使得 OncePerRequestFilter
成为处理复杂请求和响应逻辑时的一个非常有用的工具。
OncePerRequestFilter
的具体用法松哥就不举例了,第二小节已经介绍过了。
四 AOP 三件套
在 Spring 框架中,AOP(面向切面编程)是一个强大的功能,它允许开发者在不修改源代码的情况下,对程序的特定部分进行横向切入。AopContext
、AopUtils
和 ReflectionUtils
是 Spring AOP 中提供的几个实用类。
我们一起来看下。
4.1 AopContext
AopContext
是 Spring 框架中的一个类,它提供了对当前 AOP 代理对象的访问,以及对目标对象的引用。
AopContext
主要用于获取当前代理对象的相关信息,以及在 AOP 代理中进行一些特定的操作。
常见方法有两个:
getTargetObject()
: 获取当前代理的目标对象。currentProxy()
: 获取当前的代理对象。
其中第二个方法,在防止同一个类中注解失效的时候,可以通过该方法获取当前类的代理对象。
举个栗子:
public void noTransactionTask(String keyword){ // 注意这里 调用了代理类的方法
((YourClass) AopContext.currentProxy()).transactionTask(keyword);
}
@Transactional
void transactionTask(String keyword) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) { //logger
//error tracking
}
System.out.println(keyword);
}
同一个类中两个方法,noTransactionTask 方法调用 transactionTask 方法,为了使事务注解不失效,就可以使用 AopContext.currentProxy() 去获取当前代理对象。
4.2 AopUtils
AopUtils
提供了一些静态方法来处理与 AOP 相关的操作,如获取代理对象、获取目标对象、判断代理类型等。
常见方法有三个:
getTargetObject()
: 从代理对象中获取目标对象。isJdkDynamicProxy(Object obj)
: 判断是否是 JDK 动态代理。isCglibProxy(Object obj)
: 判断是否是 CGLIB 代理。
举个栗子:
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;
public class AopUtilsExample {
public static void main(String[] args) {
MyService myService = ...
// 假设 myService 已经被代理
if (AopUtils.isCglibProxy(myService)) {
System.out.println("这是一个 CGLIB 代理对象");
}
}
}
4.3 ReflectionUtils
ReflectionUtils
提供了一系列反射操作的便捷方法,如设置字段值、获取字段值、调用方法等。这些方法封装了 Java 反射 API 的复杂性,使得反射操作更加简单和安全。
常见方法:
makeAccessible(Field field)
: 使私有字段可访问。getField(Field field, Object target)
: 获取对象的字段值。invokeMethod(Method method, Object target, Object... args)
: 调用对象的方法。
举个栗子:
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Field;
import java.util.Map;
public class ReflectionUtilsExample {
public static void main(String[] args) throws Exception {
ExampleBean bean = new ExampleBean();
bean.setMapAttribute(new HashMap<>());
Field field = ReflectionUtils.findField(ExampleBean.class, "mapAttribute");
ReflectionUtils.makeAccessible(field);
Object value = ReflectionUtils.getField(field, bean);
System.out.println(value);
}
static class ExampleBean {
private Map<String, String> mapAttribute;
public void setMapAttribute(Map<String, String> mapAttribute) {
this.mapAttribute = mapAttribute;
}
}
}
还有哪些实用内置类呢?欢迎小伙伴们留言~
来源:juejin.cn/post/7417630844100231206
你小子,一个bug排查一整天,你在🐟吧!
楔子
在每日的例行会议上,空气中弥漫着紧张的气息。一位实习组员语速略急地说道:“昨天我主要的工作是排查一个线上bug
,目前还没有得到解决,今天我得继续排查。”。
组长眉头微皱,冷冷地盯了他一眼:“你小子,一个bug
排查一整天,怕是在摸鱼吧!到底是什么问题?说来听听,我稍后看看。”。
组员无奈地摊了摊手,耸了耸肩,长叹一口气:“前两天,订单表格新增定制信息匹配失败情况的展示。自己没有经过仔细的测试,就直接发布上线了,导致现在整个订单列表渲染缓慢。这个bug
超出了我的能力范围,我排查了一天也排查不出来,摸鱼是404
的。”。
组长深吸一口气,眼神中露出几分聪慧:“那不就是你编写的组件有问题吗?你最好没有摸鱼!不然你就等着吃鱼吧!”。
组员按捺不住心中的窃喜:“我如果不说一天,又怎么能请动你这尊大神呢?”。
排查
果不其然,控制台果真报错了。组长看了眼报错信息,摇了摇头,面色凝重:“你小子,居然都不看控制台的报错信息?这bug怎么排查的?”。组员下意识地捏紧了拳头,声音也不自觉地低了几分,结结巴巴道:“我、我真的不知道控制台还有这操作!学废了。”。

组长怀着忐忑不安的心情打开vsCode
, 只见一大串代码赫然映入眼帘:
<template>
<div class="design-wrapper">
<designProducts v-if="showBtn" btnText="设计" class="mr10" :data="data" @success="success" />
<el-tooltip v-if="showStatus" trigger="click" placement="right" :disabled="disabled">
<baseTable1 class="hide-tabs" :data="tableData" :option="option">
<template #content="{ row }">
<defaultImg v-if="imageType(row)" :src="image(row)" :size="100" />
<span v-else>{{ text(row) }}</span>
</template>
<template #mapping="{ row }">
<i :class="icon(row)"></i>
</template>
<template #importLabelCode="{ row }">
<span v-if="!row.hide">{{ row.importLabelCode }}</span>
</template>
<template #matchStatus="{ row }">
{{ matchSuccess(row) ? '已匹配' : '未匹配' }}
</template>
<template #design="{ row }">
<defaultImg
v-if="!row.hide && imageType(row)"
:disabled="row.disabled"
:src="row.importContent"
:size="100"
@error="error(row)"
>
<template #defaultImg>
<div class="flex-middle">{{ row.importContent || '无' }}</div>
</template>
</defaultImg>
<div v-else-if="!row.hide && !imageType(row)">{{ row.importContent || '无' }}</div>
</template>
</baseTable1>
<color-text-btn slot="reference" @click="toDesign">{{ status }}</color-text-btn>
</el-tooltip>
<span v-else></span>
</div>
</template>
当扫到el-tooltip (文字提示), 组长拍案而起,额头上暴起的青筋在不断颤抖。急切的声音,仿佛要撕裂虚空:“你小子,短短几十行代码,至少犯了2个致命错误!”。
问题分析
1. 从代码源头分析el-tooltip
(控制台报错的原因)
el-tooltip
组件主要是针对文字提示,而el-popover
才是针对组件进行展示。这两者是截然不同的,不然element
也不会分出两套组件,去分别处理这两种情况。- 我们的项目之所以能正常使用
vueX
和router
,是因为我们在main.js
中引入并挂载了
同理,分析
el-tooltip
组件的代码实现,它只挂载了data
属性。因此,当强行在el-tooltip
组件中使用自定义组件:如果组件内部使用的是非国际语言(i18n
)的纯文本,控制台不会报错;如果在该组件中使用了诸如vueX
、路由跳转等在内的变量或者方法时,控制台就会疯狂报错,因为这些并没有在初始化时注入到el-tooltip
组件中。
2. 如何在el-tooltip
中使用i18n
?
假定有一个非常执拗的人,他看到el-tooltip
组件描述的功能是文字提示。他就不想在el-popover
中使用$t
, 而想在el-tooltip
组件中使用i18n
。那么可以做到吗?答案是肯定的。
我们可以直接通过继承法则:封装一个base-tooltip
组件,继承el-tooltip
组件。并根据继承规则:先执行el-tooltip
组件的生命周期钩子方法,再执行base-tooltip
组件里面的生命周期钩子方法。通过这种方式,我们成功挂载了i18n
。此时,在base-tooltip
组件中使用$t
,就不会报错了。
<script>
import { Tooltip } from 'element-ui'
import i18n from '@/i18n'
import Vue from 'vue'
export default {
extends: Tooltip,
beforeCreate() {
this.popperVM = new Vue({
data: { node: '' },
i18n,
render(h) {
return this.node;
}
}).$mount()
}
}
</script>
3. el-tooltip
的局限性(订单列表渲染缓慢的原因)
前文提及,我们可以继承el-tooltip
组件。那么,我们如果通过按需引入的方式,将所需要的资源全部挂载到vue
中。这样,就算在base-tooltip
组件中使用vueX
和$route
变量,也不会在控制台上报错。的确如此,但是我们需要注意到el-tooltip
和el-popover
的局限性: 悬浮框内容是直接渲染的,不是等你打开悬浮框才渲染。
这也就意味着,如果我们在表格的每一行都应用了el-tooltip
或el-popover
组件,而且在el-tooltip
或el-popover
的生命周期钩子函数中请求了异步数据。就会导致页面初始化渲染数据的同时,会请求N
个接口(其中,N
为当前表格的数据条数)。一次性请求大于N + 1
个接口,你就说页面会不会卡顿就完事了!
但是,el-popover
这个组件不一样。在它的组件内部,提供了一个show
方法,这个方法在trigger
触发后才执行。于是,我们可以在show
方法中,去请求我们需要的异步数据。 同时注意一个优化点:在悬浮框打开之后,才渲染Popover内嵌的html文本
,避免页面加载时就渲染数据。
由于el-popover
的内容是在弹窗打开后才异步加载的,弹窗可能会在内容完全加载之前就开始计算和渲染位置,导致弹出的位置不对。但是我们遇到事情不要慌,el-popover
组件的混入中提供了一个方法updatePopper
,用于矫正popover
的偏移量,以期获取正确的popover
布局。

解决方法
将上述所有思路结合在一起,我们就能够封装一个公共组件,兼容工作中的大多数场景。
<template>
<el-popover
ref="popover"
@show="onShow"
v-bind="$attrs"
v-on="$listeners"
>
<template v-if="isOpened">
<slot></slot>
</template>
<template slot="reference">
<slot name="reference"></slot>
</template>
</el-popover>
</template>
<script>
import agentMixin from '@/mixins/component/agentMixin'
export default {
// 方便我们直接调用popover组件中的方法
mixins: [agentMixin({ ref: 'popover', methods: ['updatePopper', 'doClose'] })],
props: {
// 方便在打开悬浮框之前,做一些前置操作,比如数据请求等
beforeOpen: Function
},
data() {
return {
isOpened: false
}
},
methods: {
async onShow() {
if(!this.beforeOpen) {
return this.isOpened = true
}
const res = await this.beforeOpen()
if(!res) return this.isOpened = false
this.isOpened = true
await this.$nextTick()
this.updatePopper()
}
}
}
</script>
/* eslint-disable */
import { isArray, isPlainObject } from 'lodash'
export default function ({ ref, methods } = {}) {
if (isArray(methods)) {
methods = methods.map(name => [name, name])
// 如果传入是对象,可以设置别名,防止方法名重复
} else if (isPlainObject(methods)) {
methods = Object.entries(methods)
}
return {
methods: {
...methods.reduce((prev, [name, alias]) => {
prev[alias] = function (...args) {
return this.$refs[ref][name](...args)
}
return prev
}, {})
}
}
}
<template>
<div class="design-wrapper">
<designProducts v-if="showBtn" btnText="设计" class="mr10" :data="data" @success="success" />
<basePopover v-if="showStatus" trigger="click" placement="right" :beforeOpen="beforeOpen" :disabled="disabled">
<baseTable1 class="hide-tabs" :data="tableData" :option="option">
<template #content="{ row }">
<defaultImg v-if="imageType(row)" :src="image(row)" :size="100" />
<span v-else>{{ text(row) }}</span>
</template>
<template #mapping="{ row }">
<i :class="icon(row)"></i>
</template>
<template #importLabelCode="{ row }">
<span v-if="!row.hide">{{ row.importLabelCode }}</span>
</template>
<template #matchStatus="{ row }">
{{ matchSuccess(row) ? '已匹配' : '未匹配' }}
</template>
<template #design="{ row }">
<defaultImg
v-if="!row.hide && imageType(row)"
:disabled="row.disabled"
:src="row.importContent"
:size="100"
@error="error(row)"
>
<template #defaultImg>
<div class="flex-middle">{{ row.importContent || '无' }}</div>
</template>
</defaultImg>
<div v-else-if="!row.hide && !imageType(row)">{{ row.importContent || '无' }}</div>
</template>
</baseTable1>
<color-text-btn slot="reference" @click="toDesign">{{ status }}</color-text-btn>
</basePopover>
<span v-else></span>
</div>
</template>
<script>
methods: {
async beforeOpen() {
const res = await awaitResolveDetailLoading(
microApi.getMatchInfo({
id: this.data.id
})
)
if (!res) return false
this.tableData = res
return true
}
}
</script>
反思
在组长的悉心指导下,组员逐渐揭开了问题的真相。回想起自己在面对bug
时的轻率和慌乱,他不禁感到一阵羞愧。组长平静而富有耐心的声音再次在耳边响起:“排查问题并非一朝一夕之功。急于上线而忽视测试,只会让问题愈加复杂。”这一番话如同醍醐灌顶,瞬间点醒了他,意识到自己的错误不仅在于代码的疏漏,更在于对整个工作流程的轻视。
“编写代码不是一场竞赛,速度永远无法替代质量。”组长边调试代码,边语重心长地说道。组长的语气虽然平淡,却蕴含着深邃的力量。组员心中的敬佩之意油然而生,细细回味着这番话,顿时明白了面对复杂bug
时,耐心与细致才是解决问题的最强利器。组长的话语简洁而富有哲理,令他意识到,曾经追求的“快速上线”与开发中的严谨要求完全背道而驰。
不久之后,组员陷入了沉思,轻声开口:“起初,我还真觉得自己运气不好,偏偏遇上如此棘手的bug
。但现在看来,这更像是一场深刻的教训。若能在上线前认真测试,这个问题本是可以避免的。”他的声音中透出几分懊悔,眼中闪烁着反思的光芒。
组长微微一笑,点头示意:“每一个bug
都是一次学习的契机,能意识到问题的根源,已是进步。”他稍作停顿,眼神愈加坚定:“编程的速度固然重要,但若未经过深思熟虑的测试与分析,那无疑只是纸上谈兵。写代码不仅需要实现功能,更需要经得起时间的考验。”这番话语透着无可辩驳的真理,给予了组员莫大的启迪。
组员感慨道:“今天的排查让我真正领悟到耐心与细致的重要性。排查bug
就像走出迷宫,急躁只会迷失方向,而冷静思考才能找到出路。”他不禁回忆起自己曾经的粗心大意,心中暗自发誓,今后在每一次提交前都要更加谨慎,绝不再犯同样的错误。
“你小子,倒也不算愚钝。”组长调侃道,嘴角勾起一丝轻松的笑意,“但记住,遇到问题时要先冷静分析错误信息,找出原因再行动。不要盲目修改,开发不仅仅是写代码,更需要学会深思熟虑。”他轻轻拍了拍组员的肩膀,那一拍似乎传达着无限的关心与期望。
这一拍虽轻,却如雷霆般震动着组员的心灵。他明白,这不仅是组长对他的鼓励,更是一份期待与责任的传递。心中顿时涌起一股暖流,他暗自立誓:今后的每一次开发,必将怀揣严谨的态度,赋予每一行代码以深刻的责任感,而不再仅仅是为了完成任务。
在回家的路上,组员默默在心中念道:“这次bug
排查,不仅修复了代码,更矫正了我对待开发工作的态度。感谢组长,给予我如此宝贵的经验和鼓励。”他深知,从这次经历中所学到的,绝不仅是技术层面的知识,更需要以一种成熟与稳重的心态,来面对未来的每一个挑战。
怀着这样的领悟,组员的内心充满了期待。他坚信,这必将成为他在开发道路上迈向更高境界的起点。
来源:juejin.cn/post/7423378897381130277
花了一天时间帮财务朋友开发了一个实用小工具
大家好,我是晓凡。
写在前面
不知道大家有没有做财务的朋友,我就有这么一位朋友就经常跟我抱怨。一到月底简直就是噩梦,总有加不完的班,熬不完的夜,做不完的报表。
一听到这儿,这不就一活生生的一个“大表哥”么,这加班跟我们程序员有得一拼了,忍不住邪恶一笑,心里平衡了很多。
身为牛马,大家都不容易啊。我不羡慕你数钱数到手抽筋,你也别羡慕我整天写CRUD 写到手起老茧🤣
吐槽归吐槽,饭还得吃,工作还得继续干。于是乎,真好赶上周末,花了一天的时间,帮朋友写了个小工具
一、功能需求
跟朋友吹了半天牛,终于把需求确定下来了。就一个很简单的功能,通过名字,将表一和表二中相同名字的金额合计。
具体数据整合如下图所示
虽然一个非常简单的功能,但如果不借助工具,数据太多,人工来核对,整合数据,还是需要非常消耗时间和体力的。
怪不得,这朋友到月底就消失了,原来时间都耗在这上面了。
二、技术选型
由于需求比较简单,只有excel导入导出,数据整合功能。不涉及数据库相关操作。
综合考虑之后选择了
PowerBuilder
Pbidea.dll
使用PowerBuilder
开发桌面应用,虽然界面丑一点,但是开发效率挺高,简单拖拖拽拽就能完成界面(对于前端技术不熟的小伙伴很友好)
其次,由于不需要数据库,放弃web开发应用,又省去了云服务器费用。最终只需要打包成exe
文件即可跑起来
Pbidea.dll
算是Powerbuilder
最强辅助开发,没有之一。算是PBer们的福音吧
三、简单界面布局
四、核心代码
① 导入excel
string ls_pathName,ls_FileName //路径+文件名,文件名
long ll_Net
long rows
dw_1.reset()
uo_datawindowex dw
dw = create uo_datawindowex
dw_1.setredraw(false)
ll_Net = GetFileSaveName("请选择文件",ls_pathName,ls_FileName,"xlsx","Excel文(*.xlsx),*.xlsx")
rows = dw.ImportExcelSheet(dw_1,ls_pathName,1,0,0)
destroy dw
dw_1.setredraw(true)
MessageBox("提示信息","导入成功 " + string(rows) + "行数据")
② 数据整合
long ll_row,ll_sum1,ll_sum2
long ll_i,ll_j
long ll_yes
string ls_err
//重置表三数据
dw_3.reset()
//处理表一数据
ll_sum1 = dw_1.rowcount( )
if ll_sum1<=0 then
ls_err = "表1 未导入数据,请先导入数据"
goto err
end if
for ll_i=1 to ll_sum1
ll_row = dw_3.insertrow(0)
dw_3.object.num[ll_row] =ll_row //序号
dw_3.object.name[ll_row]=dw_1.object.name[ll_i] //姓名
dw_3.object.salary[ll_row]=dw_1.object.salary[ll_i] //工资
dw_3.object.endowment[ll_row]=dw_1.object.endowment[ll_i] //养老
dw_3.object.medical[ll_row]=dw_1.object.medical[ll_i] //医疗
dw_3.object.injury[ll_row]=dw_1.object.injury[ll_i] //工伤
dw_3.object.unemployment[ll_row]=dw_1.object.unemployment[ll_i] //失业
dw_3.object.publicacc[ll_row]=dw_1.object.publicacc[ll_i] //公积金
dw_3.object.annuity[ll_row]=dw_1.object.annuity[ll_i] //年金
next
//处理表二数据
ll_sum2 = dw_2.rowcount( )
if ll_sum2<=0 then
ls_err = "表2未导入数据,请先导入数据"
goto err
end if
for ll_j =1 to ll_sum2
string ls_name
ls_name = dw_2.object.name[ll_j]
ll_yes = dw_3.Find("name = '"+ ls_name +"' ",1,dw_3.rowcount())
if ll_yes<0 then
ls_err = "查找失败!"+SQLCA.SQLErrText
goto err
end if
if ll_yes = 0 then //没有找到
ll_row = dw_3.InsertRow (0)
dw_3.ScrollToRow(ll_row)
dw_3.object.num[ll_row] = ll_row //序号
dw_3.object.name[ll_row] = dw_1.object.name[ll_j] //姓名
dw_3.object.salary[ll_row] = dw_1.object.salary[ll_j] //工资
dw_3.object.endowment[ll_row] = dw_1.object.endowment[ll_j] //养老
dw_3.object.medical[ll_row] = dw_1.object.medical[ll_j] //医疗
dw_3.object.injury[ll_row] = dw_1.object.injury[ll_j] //工伤
dw_3.object.unemployment[ll_row] = dw_1.object.unemployment[ll_j] //失业
dw_3.object.publicacc[ll_row] = dw_1.object.publicacc[ll_j] //公积金
dw_3.object.annuity[ll_row] = dw_1.object.annuity[ll_j] //年金
end if
if ll_yes >0 then //找到
dec{2} ld_salary,ld_endowment,ld_medical,ld_injury,ld_unemployment,ld_publicacc,ld_annuity
ld_salary = dw_3.object.salary[ll_yes] + dw_2.object.salary[ll_j]
ld_endowment = dw_3.object.endowment[ll_yes] + dw_2.object.endowment[ll_j]
ld_medical = dw_3.object.medical[ll_yes] + dw_2.object.medical[ll_j]
ld_injury = dw_3.object.injury[ll_yes] + dw_2.object.injury[ll_j]
ld_unemployment = dw_3.object.unemployment[ll_yes] + dw_2.object.unemployment[ll_j]
ld_publicacc = dw_3.object.publicacc[ll_yes] + dw_2.object.publicacc[ll_j]
ld_annuity = dw_3.object.annuity[ll_yes] + dw_2.object.annuity[ll_j]
dw_3.object.salary[ll_yes]= ld_salary //工资
dw_3.object.endowment[ll_yes]=ld_endowment //养老
dw_3.object.medical[ll_yes]=ld_medical //医疗
dw_3.object.injury[ll_yes]=ld_injury //工伤
dw_3.object.unemployment[ll_yes]=ld_unemployment //失业
dw_3.object.publicacc[ll_yes]=ld_publicacc //公积金
dw_3.object.annuity[ll_yes]=ld_publicacc //年金
end if
next
return 0
err:
messagebox('错误信息',ls_err)
③ excel导出
string ls_err
string ls_pathName,ls_FileName //路径+文件名,文件名
long ll_Net
if dw_3.rowcount() = 0 then
ls_err = "整合数据为空,不能导出"
goto err
end if
uo_wait_box luo_waitbox
luo_waitbox = create uo_wait_box
luo_waitBox.OpenWait(64,RGB(220,220,220),RGB(20,20,20),TRUE,"正在导出 ", 8,rand(6) - 1)
long rows
CreateDirectory("tmp")
uo_datawindowex dw
dw = create uo_datawindowex
ll_Net = GetFileSaveName("选择路径",ls_pathName,ls_FileName,"xlsx","Excel文(*.xlsx),*.xlsx")
rows = dw.ExportExcelSheet(dw_3,ls_pathName,true,true)
destroy dw
destroy luo_waitbox
MessageBox("提示信息","成功导出 " + string(rows) + " 行数据")
return 0
err:
messagebox('错误信息',ls_err)
五、最终效果
这次分享就到这吧,★,°:.☆( ̄▽ ̄)/$: .°★ 。希望对您有所帮助,也希望多来几个这样的朋友,不多说了, 蹭饭去了
我们下期再见ヾ(•ω•`)o (●'◡'●)
来源:juejin.cn/post/7404036818973245478
浅谈“过度封装”
干了很多很多所谓的“敏捷”开发的项目之后,对于封装组件有了新的看法,在这里和大家分享一下
为什么要封装组件
封装组件可以复用共通的代码,增加可读性,可以统一UI样式,可以十分方便的管理代码结构。这是所有同学都知道的封装代码的好处,特别是当公司遇到需要“敏捷”开发一个项目,封装组件可以帮助我们提高效率(为了绩效)
往往我们就会选择开源的成熟的好用的组件库(element-ui、ant design等)这些组件库帮助我们开发的同时,也帮助我们更加高效的完成任务。
但是每个人对使用组件库的理解都不一样,很多可以使用组件库中的组件的地方自己反而会手动实现,虽然看上去像是实现了效果,但是严重的破坏了代码结构,极大的增加了后续的维护工作量,对于这些封装往往都是“过度封装”
浅谈“过度封装”
“过度封装”在不同的项目组同学中都有不一样的理解,但是很难有一个标准,我封装的这个组件到底算不算“过度封装”呢?
- 对与项目中已有的组件做二次封装的封装可以算是“过度封装”
- 手动实现一个组件库中存在的类似的组件在项目中使用可以算是“过度封装”
以上是我对一个组件是否是“过度封装”的理解,也可以判断一个方法是不是“过度封装”
对与项目中已有的组件做二次封装的封装可以算是“过度封装”
当我作为后续开发接手一个快要离职的同事的代码时,往往会在components文件夹里看到很多针对element-ui(antd、等其他的组件库)的table组件做的二次封装
这类的封装往往伴随着一些不够灵活的问题。当一些特殊的页面需要不一样的table设置时,往往需要修改组件的带啊才能支持使用,当这个table支持了很多不同页面的个性化需求之后,大量的props没有一个文档说明后续开发人员要阅读这个封装的组件的源码并熟悉之后快速使用。后续维护产生了大量的工作量,十分不友好。
手动实现一个组件库中存在的类似的组件在项目中使用可以算是“过度封装”
有时候设计稿中出现一个组件和组件库中的很像,但是存在差别的时候,我们需要思考一下,组件库中的组件是否完全支持我们的功能(可以多看看文档上的props,或者打开在线编辑器,调试一下看看),而不是看着有一点点的差异就手动实现一个,比如:tag标签组件、image图像组件等等在项目中基本不去使用,往往直接使用原生标签就手动开发了。
不仅仅是组件当中存在这类问题,封装方法的时候也存在这里问题,明明项目导入了lodash、momentjs、dayjs等库,反而在utils中手动实现formatDate、formatTime、节流防抖、深拷贝等方法,实在令人费解。
关于样式封装
关于组件的样式是最难封装的地方,往往开发到最后每一个文件里面就会出现一大堆的修改样式的代码
就算是在统一的样式文件中同意修改还是不免出现超长的修改颜色的各种代码
对于element-ui实在硬伤,摊牌了、我不会了🤷🏻♀️
所以我推荐使用naiveUI开发,对于样式的处理绝对的一流,加之vue3使用hooks配合组件,开发体验也很不错😎,(arco Design、antd 现在处理统一的样式风格也是很棒了)
总结
简单聊了一下“过度封装”,希望这种代码不会出现在大家的代码当中,不要去封装 my-button、my-table 这种组件,世界会更加美好。(^▽^)
来源:juejin.cn/post/7426643406305296419
架构师蔡超亲身经历的十年架构感悟分享
一、介绍
最近在学习的时候,看到了蔡超老师自己的十年架构感悟的分享,非常棒,感觉很多内容都是目前实际工作的问题,很不错,今天分享给大家。
蔡超老师已经工作 17 年了, 担任架构师的职位也超过了 10 年,担任过像 HP、Amazon 这样的世界级团队的架构师,也担任过像汇量科技这样快速成长的中小企业的技术领导。Mobvista 技术 VP 兼首席架构师。SpotMax 云服务创始人。
原文视频链接如下:
time.geekbang.org/opencourse/…
二、正文
以下是老师分享的内容:
“提出问题”难于“解决问题”
跟大家分享的第一个感悟是:“提出问题”难于“解决问题”。包括我在内,工程师们最大的一个特点就是善于解决问题,因为我们通常都是从问题解决者的角度来进行工作的。但是,我们很少会主动提出一些问题,主动从用户的场景出发去提出问题、提出需求。
很多时候,公司里的一些矛盾就来自于工程师和产品经理之间,比如我们常常会说产品经理不懂技术,需求提得不够专业。但我们作为工程师也可以想一下,我们是不是应该把自己的位置再往前挪一点,去看看用户到底有哪些困惑,然后提出一个合理的需求去解决它;或者我们自己去体验一下用户的场景,然后提出一个全新的问题并解决它。
简而言之,我们不要仅仅去做一个解决问题的人,也要做一个提出问题的人,主动去思考什么样的问题、需求,能让我们的业务更加先进。
很多时候,我们会觉得设计一个架构、写一个程序去解决问题是一件很难的事情,当然这也是一个很棒的工作。但如果你静下心来去尝试提出一些问题,改进一些用户的需求,你会发现,这是一件更难的事情,至少对我来讲是如此。
当然,这样的感觉不仅仅是我有,很多伟大的科学家也会有这样的困惑和感悟。
“The mere formulation of a problem is far more essential than its solution, which may be merely a matter of mathematical or experimental skills. To raise new questions, new possibilities, to regard old problems from a new angle requires creative imagination and marks real advances in science.”
这是爱因斯坦说过的一段话,大致意思是:我们解决一个问题的时候,常常只需要用到一些数学以及实验的能力就可以了,但提出一个新的问题,以一种新的角度去看待旧的问题,是需要用到我们的创造力才能够做到的,而这恰恰是真正推动科学进步的一部分。
不仅仅是爱因斯坦,软件大师 Frederick P. Brooks Jr.,《人月神话》的作者,在他最新一本讲设计原理的书《The Design of Design(设计原本)》里也谈到,“The hardest part of design is deciding what to design”,大概意思就是,设计最难的部分就是去决定我们要设计什么。
决定“不要什么”比“要做什么”更难
跟大家分享的第二个感悟是:决定“不要什么”比“要做什么”更难。也许因为人性的本质是贪婪的,所以我们在做项目或架构的时候,常常是什么都想要,什么需求都往里放,对于非功能性需求就更是这样了。
我们去看一个架构设计说明书,往往会看到它在开头提到高的可用性、高的性能、高的扩展性、高的可维护性……几乎所有的架构设计书都是这样,这些非功能性需求仿佛成了一个公共的列表,所有的架构都要满足这些需求。当然,功能性需求更是不用说,产品经理会一个不拉地往里面塞。
我们回过头来仔细想想什么是架构。其实在很多层面上,架构是一种 tradeoff,一种权衡和平衡。作为一个架构师,你才最应该是那个说不的人。
在现实中,有很多东西是不可兼得的。比如产品是尽早发布,还是把所有功能都加上,发布一个完美的产品;再比如一致性和性能之间的 balance,我们是选择强一致性,还是选择性能等等。所有架构师都非常熟悉的 CAP 原则,其实本质上就是一个关于 balance 的准则。
因此,作为架构师,我非常推荐大家在做架构设计的一开始,就去确立一些做事的原则。比如数据一致性优先级最高,再比如尽早发布基础功能版本的优先级大于延迟发布完善功能产品等。当出现矛盾的时候,我们就可以利用这些原则来进行取舍。
这些原则是非常重要的,它们能够指导我们在做架构的时候做出正确的取舍,而不会随着工作的推进而迷失。
“Deciding what not to do is as important as deciding what to do.”
这是引自乔布斯的一句话,意思是决定不做什么和决定做什么同样重要。他在取舍方面一直做得非常棒。
可能大家都知道,由于和董事会的关系,乔布斯在 Apple 2 发布一段时间后,就被逐出了苹果。之后他去创办了自己的事业,NeXT,也非常成功。1997 年的时候,苹果收购 NeXT,乔布斯以顾问的形式回到苹果,却发现苹果有很多问题。
当然,那个时候,苹果之所以请他回来做临时 CEO,也是因为意识到自身出现了很多问题。乔布斯意识到苹果最大的问题在于它的产品线非常多,非常的繁杂,他就在白板上画了一个象限图,四个维度分别是 Desktop、Portable、Pro 和 Consumer,并要求苹果在每个象限里面只做一个产品,然后把这个产品做到极致。
“People think focus means saying yes to the thing you’ve got to focus on. But that’s not what it means at all. It means saying no to the hundred other good ideas that there are. You have to pick carefully.I’m actually as proud of the things we haven’t done as the things I have done. Innovation is saying no to 1,000 things.
”这就是乔布斯非常著名的谈专注的那段话,大概意思是,人们认为专注意味着对你需要专注的事情说 Yes。但并非如此,专注意味着你要对其他 100 个好主意说 No,你必须谨慎选择。相比已经完成的工作,他对那些没有完成的工作一样感到自豪。创新就是对 1000 件事情说 No。
非功能性需求决定架构
跟大家分享的第三个感悟是:非功能性需求决定架构。在很多人心目中,做架构的第一步是收集需求,把各种需求都收集上,这个架构的目的就是要满足这些功能性需求的,毕竟最终产品是要为用户服务的。
事实并非如此,一个好的架构,其实是由非功能性需求决定的,而不是由功能性需求决定的。你会发现,一个功能可以有无数的架构方案来实现,但你为什么最终选择了某个方案,其实是由非功能性需求来进行筛选的。
大家非常清楚什么是非功能性需求,包括性能、伸缩性、可扩展性、可维护性等,甚至还包括了你的团队结构,你团队的技术水平,你对发布周期的要求等等,通过所有这些需求来筛选可使用的方案,最终找到一个合适的架构。
所以,非功能性需求是非常重要的,甚至可以说是在你的架构设计中起到决定性因素的。架构设计完之后,少一个功能性需求,我们很容易就能看出来,未来也可以加上去,它对你的架构不会有本质上的影响。但如果我们忽略的是某一种非功能性需求,那在未来这可以说是一种灾难性的麻烦,很有可能你就需要重写了。比如你架构中的数据一致性问题无法解决,或者在设计的时候没有充分考虑性能问题,这样,所有的功能性的实现其实都没有意义。基本就是 Refactor 了,甚至不应该叫 Refactor,要叫 Recreate 或者 Rewrite,等于你要完全重写整个架构。
实际上在架构领域,大家对这点也是有共识的。比如下图中这个 Micro-Kernel 的架构模式来自《面向模式的软件架构》的第一卷,它一大特点就是有比较好的可扩展性,同时通过 Plugin 之间的隔离,能够提高系统的可用性。
《面向模式的软件架构》这套书多年来一直是架构师的必读经典,书中很多架构都是从非功能性需求的角度展开去讲的,如果你想成为架构师,那就非常推荐给你去看。
“简单”并不容易
跟大家分享的第四个感悟是:“简单”并不容易。很多架构师都会提到保持简单,keep the simple,但很多时候我们会混淆简单和容易,简单是 simple,容易是 easy,我们是 keep it easy,而不是 keep it simple。
正如乔布斯所说,简单有时候要比复杂更难,需要你对问题、事物的研究非常地深入,你才能找到真正简单的方法。简单其实是蕴含着一种巧妙在其中的。例如我们熟知的布隆过滤器,是一个十分简单的高效重复数据过滤算法,它就非常巧妙地解决了一个问题。如果你想把一个事情做简单,你需要做很多深入的工作,比如对于架构的简化,很大程度上来自于我们对于技术、开发过程,以及不同业务场景的深入理解,而不仅仅是这个架构写起来好不好写。
举个例子,我们来回顾一下软件生命周期中各个阶段的成本消耗占比。
可以看到,在整个软件生命周期中,成本消耗最高的并不是设计、编码这些阶段,而是维护阶段。也就是说,如果你让维护变得简单,这会是最有性价比的。
我之前在一家国际公司工作过,主要是为移动运营商设计一个移动设备管理系统,运营商可以通过这个系统实现移动设备的自动注册,固件和软件的同步更新等。当时的移动设备还是摩托罗拉、爱立信之类早期智能手机的时代,打开手机会看到移动菜单或联通菜单,移动运营商就通过这些菜单跟你同步更新,也会对你的系统固件进行升级。这些工作是根据一些管理系统与移动设备之间预定义的协议来完成的,比如 SyncML。而电信专家们会根据业务场景及需求不断调整和新增这些交互协议。
刚开始设计系统的时候,我们也想着 keep it simple,就采用了一种看似简单的实现方式,团队里的软件工程师拿到电信专家设计好的协议后,把协议翻译成对应的程序语言,每一种协议对应一个程序语言。这时候每个程序语言都是一个插件,扩展也很容易,把这个语言实现的 Plugin 插到系统中,或者 Update 一个 Plugin,就可以支持一种新功能了。
这么看你可能觉得还行,反正也是插件结构,看起来也相当简单、直接,于是照着这个设计我们实现了一个系统:任何一个新业务过来,先由电信专家设计协议,再由工程师把协议转换为代码,然后将这个代码写成一个 Server 插件部署到 Server 端,这个协议就被支持了。
但很快,我们就发现事情没那么简单,这套系统的维护成本高到令人发指。为什么?原因其实可以用 Martin Fowler 的一句话来解释:
“I believe that the hardest part of software projects, the most common source of project failure, is communication with the customers and users of that software.
沟通往往是导致软件项目失败的主要问题,的确是这样。
这个系统最大的问题是在上线后的运行维护阶段,电信专家和工程师之间会不断地就新的协议修改和增加进行持续的沟通,但是他们之间的领域知识和词汇都有很大的差别,对彼此专业领域的理解有限,结果就大大影响沟通的效率。这期间系统修改每次都十分艰难,不仅协议更新上线时间慢,而且很多问题由于工程师对于电信协议理解程度有限,都要在开发完成,实际使用后才能被电信专家发现。导致了很多的交换和反复,也造成了很多客户的抱怨。
所以,这个系统只是表面上看起来简单,最终整个过程演变得没那么简单。那什么才是真正的简单?发现上面提到的这些问题,以及背后的原因是沟通后,我们开始重新思考解决的方法。后来我们和电信专家一起设计了一种协议设计语言 DSL,Domain Specific Language。DSL 是用电信专家熟悉的词汇来进行描述的,我们还提供了可视化工具,让电信专家能非常轻松容易地使用。然后这个协议会通过一个类似于编辑器的工具,将电信专家定义好的协议模型转换为内存中的 Java 结构,在线上进行运行。这样整个项目的运行和维护就变得更加简单高效了,省去了低效的交流和不准确人工转换。
这其实并不是一件简单的事情,不论是设计 DSL 语言、做类似编译器的工具,还是构建内存模型,都不简单,但一旦上线,一切就变得简单了。而一开始按电信专家的需求直接实现协议的方法是更为容易的,但是就整个软件生命周期来看,它却不是一个简单高效的方法。
通过这个例子,你应该能体会到什么是简单、什么是容易,两者之间的差别。真正的简单是来自于不容易的,就像那句话说的,It's hard to simple,It's easy to complex,简单是很难的,复杂反而是很容易的。
永远不要停止编码
跟大家分享的第五个感悟是:永远不要停止编码。
这一点非常重要,对一个架构师来说,要永远记住自己是一个程序员。作为架构师,我们可能设计了一个非常 high-level 的架构,但代码是软件的最终实现形态,每一个程序员在架构落地过程中的实现,都可能会影响架构的最终呈现。
另外,如果你放弃编码,最大的影响不是说你代码的技术落后了,或者是敲代码变慢了,最大的影响是你会逐渐丧失对编程的敬畏,忘记作为程序员的感受,特别是编码过程中的那些痛苦所在。你会有一些不切实际的幻想,做出一些不切实际的设计,这才是最大的问题。
大家都知道的 Java 之父 James Gosling,他在 Amazon 的职位是 Distinguished Engineer,level 相当于 SVP,而他依旧在坚持编码,每年的代码量是非常惊人的,常常会超过 10 万行。总而言之,作为一个架构师,一旦你开始放弃编码,那你一定要非常小心,因为你可能正在走向一条不归路,一条为大家设计一些充满幻想但又较为虚无的设计的不归路。
风险优先
跟大家分享的第六个感悟是:风险优先。
可以先思考一个问题,我们为什么要做架构设计?
在我看来,架构设计最主要的功能就是转化、降低、避免整个开发过程中的风险。而架构师很大的一个职责就是在早期识别出系统可能存在的风险,并通过你的设计来转换它、去除它。
我们常说的原型方式,或者架构切片的快速迭代方式,其实也是从另一个角度在早期尽量去测试风险,去测试我们的架构能不能解决相关问题,尤其是那些非功能性需求实现的风险,这些风险往往没有功能性需求这么容易在初期被发现,但修正的代价通常要比修正功能性需求大非常多,甚至可能导致项目的失败。
比如敏捷开发,很多人认为敏捷开发就是更快地开发出一个产品,然后快速地 deliver 到市场上,其实这只是敏捷的一部分。另一部分很重要的是,如果一个项目要失败,也要快速地失败,绝对不要把风险放到最后,这也是一种敏捷。这里再给大家推荐一本书《Just Enough Software Architecture(恰如其分的软件架构)》,这是最近非常流行的一本架构书籍,书中强调,架构设计的目的就是为了化解软件实现中的风险。如果你项目中所有的风险都可以通过未来重构来解决的话,那你根本就不需要进行架构设计,直接等着重构就可以了。这也是我非常赞同的观点,风险优先。
从“问题”开始,而不是“技术”
跟大家分享的第七个感悟是:从“问题”开始,而不是“技术”。
作为技术人员,我们非常乐意学习一些新技术,并且学了之后,我们还会非常有热情去应用这个技术。我经常会有这样的感觉,感觉在某一时刻被某个技术上身,特别想去实践它,以至于忽略当前手上的问题用这个技术来解决是不是最合适的,不知道你有没有相同的感觉。
冷静的时候,其实我们每个人都知道,要从实际出发,从需求出发,从用户的问题出发,而不要从技术出发,但在实际工作中,我们却常常不自觉地忽略这一点。就像手里有了一把锤子,看到什么都是钉子。
但其实这样做有很大的害处,这里想给你分享一个故事,来自我之前工作过的一个团队。当时团队里有一个工程师,他维护的是一个非常简单的服务,就是一个利用 MySQL 作为数据存储的简单服务。后来一个他对当时新出的 DynamoDB 产生了兴趣,并学习了相关知识。然后就发生下面的事:使用 DynamoDB 替换了 MySQL,这是一个噩梦的开始。很快发现 DynamoDB 并不能很好的支持事务特性,在当时只有一个性能极差的客户端类库来支持事务,而由于采用客户端方式,引入了大量的额外交互,导致性能差别达 7 倍之多,效率非常之低。
这时候,这个同学就改用了当时在 NoSQL 领域广泛流行的最终一致技术,采用了一个消息队列来实现,这样每一个数据存储对象的改变都会发布一个消息,如果关心这个改变的业务逻辑,就会订阅这个消息,然后改变其他相关的对象,从而实现最终一致。如果过程中出现错误,就会利用消息队列的重试机制。接着发现 DynamoDB 无法提供 SQL 那样方便的查询机制,为了进行数据分析就采用 EMR/MapReduce Job 来完成。大家可以看到实现一样的功能,但是复杂性大大增加,维护工作也由一个人变成了一个团队。
如果让我总结一下这个故事的话,可以说是我们对技术的热情让事情变得复杂,是我们对技术的热情把生活搞得没有那么美好,也让自己的工作更加烦恼。
过度繁忙使你落后
跟大家分享的第八个感悟是:过度繁忙使你落后。
对于 IT 人而言,忙碌成了习惯,加班常挂在嘴边,“996”似乎也变成了公司高效的标志。但有时候我们需要反思一下,有多久没有在业余时间看和技术相关的书了。我之前在公司也问过这个问题,百分之百的人回答我,下班后已经很晚了,回到家基本上没有时间再看书,刷一下手机,就可以直接睡觉了。这是一个非常值得我们去思考的问题。
作为一个技术人,如果你不更新你的知识,或者繁忙让你没有时间更新知识,那会有什么样的结果呢?
给大家分享一个有意思的现象,我遇到过不少程序员,有之前的同事,也有自己的朋友,他们换了一份工作,一开始进入那家公司的时候跟我说,“这个公司我不是特别看好它,我了解一下它的技术,就准备换家公司。”过了两三年我再问他,“你怎么还在这,还没跳走。”结果他回答我,“我看现在的招聘形势不大好,不太好动。”干了几年倒对公司越来越“忠诚”了。
实际情况是,在一个公司没日没夜地干了几年,没有留一点学习时间给自己,忙碌的工作导致他没有时间更新知识,再想回到市场上的时候,却发现自己已经落伍了,连跳槽的能力和勇气都失去了。在这个高速发展的时代,如果因为过度忙碌,导致你没有时间学习和更新自己的知识,那必然会让你落后。即使你不跳槽,呆在同一家公司里,公司的业务不断发展,数据量会越来越大,用户需求会越来越刁钻,你要面对的问题和场景也会越来越复杂,如果你长期不更新知识,掌握的技能没有发生变化,你会觉得越来越难以应付,最终只能通过不断地加班来应对。
另外还有一种可能是,你不更新知识,不深入思考,那么很大概率,你所创造的技术和业务丧失了领先性,没有领先优势,只能被动紧紧跟随竞争对手,而紧紧跟随就意味着你只能加班。试想一下,你要是都领先同行业五年了,还会在乎通过加班来早一个月发布吗?
这其实是一个恶性循环,你花越多的时间去忙碌,就越没有时间去学习,去提高自己的工作技能,就只能靠加班来追赶,结果就更忙碌,更没有时间学习,最终成为一个井底之蛙,陷在恶性循环里无法挣脱。我是一个健身爱好者,练过健身的朋友都知道,光靠锻炼是不行的,营养的补充和锻炼同样重要,你得专门吃一些蛋白粉、补剂之类的。而且越到后面,营养的重要性就越高,至少能跟锻炼达到 50:50 的比重。个人技术成长其实也是一样的,锻炼就好像实践,营养就好像学习。
人们常说 practice makes you perfect,但光 practice 是不行的,还需要坚持学习。
我们在一个领域工作了一段时间,比方说三五年之后,会对这个领域的业务越来越熟悉,解决问题越来越顺手,但相应的,能学到的知识和技能也就会越来越少。有些人会说这是进入了舒适区,要逃离舒适区,换一个领域,我倒觉得不必如此。
本质上来说,换一个领域其实是促进你进一步学习一些新的知识,你在原来的领域也可以这么做。你可以有意识地摆脱那种麻木,挤出时间来重新学习,然后即使做的是相同的事情,也可以用不同的方式更好更高效地完成它。你会发现,即使在同一个领域,你也完全可以做和别人不一样的事情。
所以,每个技术人员都要保证充足的学习时间,否则很容易成为井底之蛙,从而陷入前面提到的低效循环。最后用一句话来跟大家共勉,不忘初心,坚持匠心,谢谢大家。
三、总结
- “提出问题”难于“解决问题”。程序员要学会如何从用户的角度发现困难,提出需求问题,适配用户场景。不仅仅是一个解决问题的人,而是提出问题的人,不断地思考什么样的需求问题能让我们的产品更先进。
- “设计最难的部分就是去设计我们要设计什么样的问题” - 《设计原本》Brooks
- 什么是架构? 从很多层面上说,架构是一种tradeoff,是一种权衡、平衡。作为一个架构师,你应该是那个说“不”的人。决定不要什么比要什么更难。先确定一个大原则,之后在做选择的时候根据这个原则来取舍,这样就不会随着工作的推进而迷失了。
- “决定不做什么和决定做什么是一样重要的” - 史蒂夫·乔布斯
- “人们认为专注是对你要关注的事情说yes。但是这完全不是专注的本意。专注是对其他一千个好的想法说no。你必须非常谨慎地挑选。实际上我对我拒绝做的事和我做过的事一样感到骄傲。创造力就是对一千件事说NO的能力。” - 史蒂夫·乔布斯
- 非功能需求决定架构。所谓的非功能需求,包括性能,伸缩性,可扩展性,可维护性,甚至还包括了你的团队结构、团队技术水平和发布周期的要求。这些因素来筛选可以使用的方案,最终找到一个合适的架构。
- 非功能性需求在架构中起决定性作用,因为功能性需求在设计完后,即便未来需要添加新功能,对架构的本质影响不大。但忽略非功能性需求可能会导致灾难性后果,可能需要重写整个系统,比如由于架构问题导致的数据一致性问题或性能问题。建议了解一下Micro-Kernel模式架构。
- “简单可以比复杂更难。你必须非常努力地把你的想法想清楚之后,才有可能把事情做得很简单。但这个努力是值得的,因为一旦你达到了这个目的,它会给你带来排山倒海的能量。为了达到真正的简单,你必须思考得足够深入。” -- 史蒂夫·乔布斯
- 简单不同于容易。真正的简单恰恰是来自于不容易,复杂才是容易的,简单蕴含了巧妙。
- 在软件开发生命周期中,软件发布后的维护占据整个成本的一半以上。如果让一件事情变得简单,后期维护也会变得简单,这是性价比最高的选择。
- “我相信软件开发中最难的部分,也是最经常导致失败的部分,是与软件用户的沟通交流。” -- Martin Fowler
- 作为一个架构师,你永远都不要停止编码。如果你停止编码,你就会丧失对编写代码的痛苦感知,容易产生不切实际的幻想,进而做出不切实际的设计。(罗胖也坚持自己做启发俱乐部磨练自己的手艺)
- 风险优先。架构设计中,你要在早期识别系统可能的风险,通过设计消除或转换这些风险,比如通过原型或架构切片的早期迭代,测试架构是否还存在风险。
- 敏捷开发的精髓是,如果项目会失败,那就让它快速失败。
- “如果项目中能预见的风险都可以通过重构解决,那么就没必要设计软件架构,你重写就好了。” -- 《恰如其分的软件架构》
- 从“问题”开始,而不是“技术”。
- 过度繁忙使你落后。如果你没有时间更新知识,几年后你容易“被忠诚”,因为失去了跳槽的能力和勇气。随着工作中的问题变复杂,你会发现越来越难应对,只能通过不断加班解决。如果不能紧跟行业发展,就会陷入恶性循环。试想一下,如果你已经领先业界五年了,还在乎休息一个礼拜吗?
- 做更好的自己。锻炼配合营养,实践结合学习,才能变得更好。
- 要不断学习。你不一定要换领域,但可以用不同的方式做同样的事,并做得更好。
- 不忘初心,坚持匠心。
来源:juejin.cn/post/7426916970662215690
虽然炒股赚了十多万,但差点倾家荡产!劝你别入坑
今天,五阳哥不打算聊技术,而是聊一下炒股的话题。我自认为在这方面有发言权,自述一个程序员的炒股经历。
2019年,我开始涉足股市,在2021年中旬为了购房,将持有的股票全部卖出,赚了十多万元。在最高峰时期,我获利超过了二十多万元,但后来又回吐了一部分利润。虽然我的炒股成绩不是最出色的,但也超过了很多人。因为大多数股民都是亏损的,能够在股市长期盈利的人真的是凤毛麟角。
股市中普遍流传的七亏二平一赚的说法并不只是传闻,事实上,现实中的比例更加残酷,能够长期赚钱的人可能连10%都达不到。
接下来,我想谈谈我的炒股经历和心路历程,与大家分享一下我的内心体验,为那些有意向或正在炒股的朋友提供一些参考。希望劝退大家,能救一个是一个!
本文倒叙描述,先聊聊最后的疯狂和偏执!
不甘失败,疯狂上杠杆
股市有上涨就有下跌,在我卖出以后,股市继续疯涨了很多。当时长春高新,我是四百一股买入,六百一股就卖出了,只赚了2万。可是在我卖出去的两个月以后,它最高涨到了一千。相当于我本可以赚六万,结果赚了两万就跑了。
我简直想把大腿拍烂了,这严重的影响了我的认知。我开始坚信,这只股票和公司就是好的,非常牛,是我始乱终弃,我不应该早早抛弃人家。 除了悔恨,我还在期盼它下跌,好让我再次抄底,重新买入,让我有重新上车的机会!
终于这只股票后来跌了10%,我觉得跌的差不多了,于是我开始抄底买入!抄底买入的价格在900一股(复权前)。
没想到,这次抄底是我噩梦的开始。我想抄他的底,他想抄我的家!
这张图,完美的诠释了我的抄底过程。地板底下还有底,深不见底,一直到我不再敢抄底为止。一直抄到,我天天睡不着觉!
当时我九百多一股开始抄底买入,在此之前我都是100股,后来我开始投入更多的资金在这只股票上。当时的我 定下了规矩,鸡蛋不能放在一个篮子里;不能重仓一只股票,要分散投资;这些道理我都明白,但是真到了节骨眼上,我不想输,我想一把赢回来,我要抄底,摊平我的成本。
正所谓:高位加仓,一把亏光。之前我赚的两万块钱,早就因为高位加仓,亏回去了。可是我不甘心输,我想赢回来。当时意识不到也不愿意承认:这就是赌徒心理。
后来这只股票,从1000,跌倒了600,回调了40%。而我已经被深深的套牢。当时我盈利时,只买了1股。等我被套牢时,持有了9股。 按照1000一股,就是九十万。按照600一股,就是54万。
我刚毕业,哪来的那么多钱!
我的钱,早就在800一股的时候,我就全投进去了,我认为800已经算是底了吧,没想到股价很快就击穿了800。
于是我开始跟好朋友借钱。一共借了10万,商量好借一年,还他利息。后来这10万块钱,也禁不住抄底,很快手里没钱了,股价还在暴跌。我已经忘记当时亏多少钱了,我当时已经不敢看账户了,也不敢细算亏了多少钱!
于是,我又开始从支付宝和招商银行借贷,借钱的利率是相当高的,年利息在6%以上。当时一共借了30万。但是股价还不见底,我开始焦虑的睡不着觉。
不光不见底,还在一直跌,我记得当时有一天,在跌了很多以后,股价跌停 -10%。当时的我已经全部资金都投进去了,一天亏了5万,我的小心脏真的要受不了了。跌的我要吐血! 同事说,那天看见我的脸色很差,握着鼠标手还在发抖!
跌成这样,我没有勇气打开账户…… 我不知道什么时候是个头,除了恐惧只有恐惧,每天活在恐惧之中。
我盘算了一下,当时最低点的我,亏了得有二十多万。从盈利六万,一下子到亏二十多万。只需要一个多月的时间。
我哪里经历过这些,投资以来,我都是顺风顺水的,基本没有亏过钱,从来都是挣钱,怎么会成这个样子。
当时的我,没空反思,我只希望,我要赚回来!我一定会赚回来,当时能借的支付宝和招行都已经借到最大额度了…… 我也没有什么办法了,只能躺平。
所以股价最低点的时候,基本都没有钱加仓。
侥幸反弹,但不忍心止盈
股价跌了四个月,这是我人生极其灰暗的四个月。后来因为种种原因,股价涨回来了,当时被传闻的事情不攻自破,公司用实际的业绩证明了自己。
股价开始慢慢回暖,后来开始凶猛的反弹,当时的我一直认为:股价暴跌时我吃的所有苦,所有委屈,我都要股市给我补回来!
后来这段时间,股价最高又回到了1000元一股(复权前)。最高点,我赚了二十多万,但是我不忍心止盈卖出。
我觉得还会继续涨,我还在畅想:公司达到,万亿市值。
我觉得自己当时真的 失了智了。
结婚买房,卖在最高点
这段时间,不光股市顺丰顺水,感情上也比较顺利,有了女朋友,现在是老婆了。从那时起,我开始反思自己的行为,我开始意识到,自己彻彻底底是一个赌徒。
因为已经回本了,也赚了一点钱,我开始不断的纠结要不要卖出,不再炒股了。
后来因为两件事,第一件是我姐姐因为家里要做小买卖,向我借钱。 当时的我,很纠结,我的钱都在股市里啊,借她钱就得卖股票啊,我有点心疼。奈何是亲姐,就借了。
后来我盘算着,不对劲。我还有带款没还呢,一共三十万。我寻思,我从银行借钱收6%的利息,我借给别人钱,我一分利息收不到。 我TM 妥妥的冤大头啊。
不行,我要把带款全部还上,我Tm亏大了,于是我逐渐卖股票。一卖出便不可收拾。
我开始担心,万一股价再跌回去,怎么办啊。我和女朋友结婚时,还要买房,到时候需要一大笔钱,万一要是被套住了,可怎么办啊!
在这这样的焦虑之下,我把股票全部都卖光了!
冥冥之中,自有天意。等我卖出之后的第二周,长春高新开启了下一轮暴跌,而这一轮暴跌之后,直至今日,再也没有翻身的机会。从股价1000元一股,直至今天 300元一股(复权前是300,当前是150元)。暴跌程度大达 75%以上!
全是侥幸
我觉得我是幸运的,如果我迟了那么一步!假如反应迟一周,我觉得就万劫不复。因为再次开启暴跌后,我又会开始赌徒心理。
我会想,我要把失去的,重新赢回来!我不能现在卖,我要赢回来。再加上之前抄底成功一次,我更加深信不疑!
于是我可能会从1000元,一路抄底到300元。如果真会如此,我只能倾家荡产!
不是每个人都有我这么幸运,在最高点,跑了出去。 雪球上之前有一个非常活泼的用户, 寒月霖枫,就是因为投资长春高新,从盈利150万,到亏光100万本金,还倒欠银行!
然而这一切,他的家人完全不知道,他又该如何面对家人,如何面对未来的人生。他想自杀,想过很多方式了结。感兴趣的朋友可以去 雪球搜搜这个 用户,寒月霖枫。
我觉得 他就是世界上 另一个自己。我和他完全类似的经历,除了我比他幸运一点。我因为结婚买房和被借钱,及时逃顶成功,否则我和他一样,一定会输得倾家荡产!
我觉得,自己就是一个赌狗!
然而,在成为赌狗之前,我是非常认真谨慎对待投资理财的!
极其谨慎的理财开局
一开始,我从微信理财通了解到基金,当时2019年,我刚毕业两年,手里有几万块钱,一直存在活期账户里。其中一个周末,我花时间研究了一下理财通,发现有一些债券基金非常不错。于是分几批买了几个债券基金,当时的我对于理财既谨慎又盲目。
谨慎的一面是:我只敢买债券基金,就是年利息在 5%上下的。像股票基金这种我是不敢买的。
盲目的一面是:我不知道债券基金也是风险很大的,一味的找利息最多的债券基金。
后来的我好像魔怔了,知道了理财这件事,隔三差五就看看收益,找找有没有利息更高的债券基金。直到有一天,我发现了一个指数基金,收益非常稳定。
是美股的指数基金,于是我买了1万块钱,庆幸的是,这只指数基金,三个月就赚了八百多,当时的我很高兴。那一刻,我第一次体会到:不劳而获真的让人非常快乐!
如饥似渴的学习投资技巧
经过一段时间的理财,我对于理财越来越熟悉。
胆子也越来越大,美股的指数基金赚了一点钱,我害怕亏回去,就立即卖了。卖了以后就一直在找其他指数基金,这时候我也在看国内 A股的指数基金,甚至行业主题的基金。
尝到了投资的甜头以后,我开始花更多的时间用来 找基。我开始从方方面面评估一只基金。
有一段时间,我特别自豪,我在一个周末,通过 天天基金网,找到了一个基金,这只基金和社保投资基金的持仓 吻合度非常高。当时的我思想非常朴素, 社保基金可是国家队,国家管理的基金一定非常强,非常专业,眼光自然差不了。这只基金和国家队吻合度如此高,自然也差不了。
于是和朋友们,推荐了这只基金。我们都买了这只基金,而后的一个月,这只基金涨势非常喜人,赚了很多钱,朋友们在群里也都感谢我,说我很厉害,投资眼光真高!
那一刻,我飘飘然……
我开始投入更多的时间用来理财。下班后,用来学习的时间也不学习了,开始慢慢的过度到学习投资理财。我开始不停地 找基。当时研究非常深入,我会把这只基金过往的持仓记录,包括公司都研究到。花费的时间也很多。
我也开始看各种财经分析师对于股市的分析,他们会分析大盘何时突破三千点,什么时候股市情绪会高昂起来,什么行业主题会热门,什么时候该卖出跑路了。
总之,投资理财,可以学习的东西多种多样!似乎比编程有趣多了。
换句话说:我上头了
非常荒谬的炒股开局
当时我还是非常谨慎地,一直在投资基金,包括 比较火爆的 中欧医疗创新C 基金,我当时也买了。当时葛兰的名气还很响亮呢。后来股市下行,医疗股票都在暴跌,葛兰的基金 就不行了,有句话调侃:家里有钱用不完,中欧医疗找葛兰。腰缠万贯没人分,易方达那有张坤。
由此可见,股市里难有常胜将军!
当时的我,进入股市,非常荒谬。有一天,前同事偷偷告诉我,他知道用友的内幕,让我下午开盘赶紧买,我忙追问,什么内幕,他说利润得翻五倍。 我寻思一下,看了一眼用友股票还在低位趴着,心动了。于是我中午就忙不迭的线上开户,然后下午急匆匆的买了 用友。 事后证明,利润不光没有翻五倍,还下降了。当然在这之前,我早就跑了,没赚着钱,也没咋亏钱。
当时的我,深信不疑这个假的小道消息,恨不得立即买上很多股票。害怕来不及上车……
自从开了户,便一发不可收拾,此时差2个月,快到2019年底!席卷全世界的病毒即将来袭
这段时间,股市涨势非常好,半导体基金涨得非常凶猛! 我因为初次进入股市,没有历史包袱,哪个股票是热点,我追哪个,胆子非常大。而且股市行情非常好,我更加相信,自己的炒股实力不凡!
换句话说:越来越上头,胆子越来越大。 学习编程,学个屁啊,炒股能赚钱,还编个屁程序。
刚入股市,就赶上牛市,顺风顺水
2019年底到2020年上半年,A股有几年不遇的大牛市,尤其是半导体、白酒、医疗行业行情非常火爆。我因为初入股市,没有历史包袱,没有锚点。当前哪个行业火爆,我就买那个,没事就跑 雪球 刷股票论坛的时间,比上班的时间还要长。
上班摸鱼和炒股 是家常便饭。工作上虽然不算心不在焉,但是漫不经心!
在这之前,我投入的金额不多。最多时候,也就投入了10万块钱。当时基金收益达到了三万块。我开始飘飘然。
开始炒股,也尝到了甜头,一开始,我把基金里的钱,逐渐的转移到股市里。当时的我给自己定纪律。七成资金投在基金里,三成资金投在股市里。做风险平衡,不能完全投入到风险高的股市里。
我自认为,我能禁得住 炒股这个毒品。
但是逐渐的,股票的收益越来越高,这个比例很快就倒转过来,我开始把更多资金投在股市中,其中有一只股票,我非常喜欢。这只股票后来成为了很多人的噩梦,成为很多股民 人生毁灭的导火索!
长春高新 股票代码:000661。我在这只股票上赚的很多,后来我觉得股市涨了那么多,该跌了吧,于是我就全部卖出,清仓止盈。 当时的我利润有六万,我觉得非常多了,我非常高兴。
其中 长春高新 一只股票的利润在 两万多元。当时这是我最喜欢的一只股票。我做梦也想不到,后来这只股票差点让我倾家荡产……
当时每天最开心的事情就是,打开基金和证券App,查看每天的收益。有的时候一天能赚 两千多,比工资还要高。群里也非常热闹,每个人都非常兴奋,热烈的讨论哪个股票涨得好。商业互吹成风……
换句话说:岂止是炒股上头,我已经中毒了!
之后就发生了,上文说的一切,我在抄底的过程中,越套越牢……
总结
以上都是我的个人真实经历。 我没有谈 A 股是否值得投资,也不评论当前的股市行情。我只是想分享自己的个人炒股经历。
炒股就是赌博
我想告诉大家,无论你在股市赚了多少钱,迟早都会还回去,越炒股越上头,赚的越多越上头。
赌徒不是一天造成的,谁都有赢的时候,无论赚多少,最终都会因为人性的贪婪 走上赌徒的道路。迟早倾家荡产。即使你没有遇到长春高新,也会有其他暴跌的股票等着你!
什么🐶皮的价值投资! 谈价值投资,撒泡尿照照自己,你一个散户,你配吗?
漫漫人生路,总会错几步。股市里错几步,就会让你万劫不复!
”把钱还我,我不玩了“
”我只要把钱赢回来,我就不玩了“
这都是常见的赌徒心理,奉劝看到此文的 程序员朋友,千万不要炒股和买基金。
尤其是喜欢打牌、打德州扑克,喜欢买彩-票的 赌性很强的朋友,一定要远离炒股,远离投资!
能救一个是一个!
来源:juejin.cn/post/7303348013934034983
Nginx UI:全新的 Nginx 在线管理平台
前言
Nginx在程序部署中扮演着至关重要的角色,其高性能、高安全性、易于配置和管理的特点,使得它成为现代Web应用部署中不可或缺的一部分。今天大姚给大家分享一款实用的 Nginx Web UI 工具,希望能够帮助到有需要的同学。
工具介绍
Nginx UI一个功能丰富、易于使用的 Nginx Web UI 工具,它极大地简化了 Nginx 服务器的管理和配置过程。
主要功能
- 在线统计:提供服务器指标如 CPU 使用率、内存使用率、负载平均值和磁盘使用率的在线统计。
- ChatGPT 助手:内置 ChatGPT 助手,提供智能辅助功能。
- 一键部署和自动续期:支持一键部署 Let's Encrypt 证书,并自动续期。
- 在线编辑配置:在线编辑 Nginx 配置文件,编辑器支持 Nginx 配置语法高亮。
- 查看 Nginx 日志:提供在线查看 Nginx 日志的功能。
- 自动测试和重载:自动测试配置文件并在保存后重载 Nginx。
- Web 终端:提供 Web 终端访问功能。
- 暗色模式:支持暗色模式,保护用户视力。
- 响应式网页设计:确保在不同设备上都能良好显示。
支持语言
支持多语言,包括英语、简体中文、繁体中文等。
在线演示
- demo.nginxui.com/#/login (用户名:admin,密码:admin)
开源地址
程序员常用的工具软件
该工具已收录到程序员常用的工具软件栏目中,欢迎关注该栏目发现更多优秀实用的开发工具!
来源:juejin.cn/post/7425885062922174498
还在用轮询、websocket查询大屏数据?sse用起来
常见的大屏数据请求方式
1、http请求轮询:使用定时器每隔多少时间去请求一次数据。优点:简单,传参方便。缺点:数据更新不实时,浪费服务器资源(一直请求,但是数据并不更新)
2、websocket:使用websocket实现和服务器长连接,服务器向客户端推送大屏数据。优点:长连接,客户端不用主动去请求数据,节约服务器资源(不会一直去请求数据,也不会一直去查数据库),数据更新及时,浏览器兼容较好(web、h5、小程序一般都支持)。缺点:有点大材小用,一般大屏数据只需要查询数据不需要向服务端发送消息,还要处理心跳、重连等问题。
3、sse:基于http协议,将一次性返回数据包改为流式返回数据。优点:sse使用http协议,兼容较好、sse轻量,使用简单、sse默认支持断线重连、支持自定义响应事件。缺点:浏览器原生的EventSource不支持设置请求头,需要使用第三方包去实现(event-source-polyfill)、需要后端设置接口的响应头Content-Type: text/event-stream
sse和websocket的区别
- websocket支持双向通信,服务端和客户端可以相互通信。sse只支持服务端向客户端发送数据。
- websocket是一种新的协议。sse则是基于http协议的。
- sse默认支持断线重连机制。websocket需要自己实现断线重连。
- websocket整体较重,较为复杂。sse较轻,简单易用。
Websocket和SSE分别适用于什么业务场景?
根据sse的特点(轻量、简单、单向通信)更适用于大屏的数据查询,业务应用上查询全局的一些数据,比如消息通知、未读消息等。
根据websocket的特点(双向通信)更适用于聊天功能的开发
前端代码实现
sse的前端的代码非常简单
const initSse = () => {
const source = new EventSource(`/api/wisdom/terminal/stats/change/notify/test`);
// 这里的stats_change要和后端返回的数据结构里的event要一致
source.addEventListener('stats_change', function (event: any) {
const types = JSON.parse(event.data).types;
});
// 如果event返回的是message 数据监听也可以这样监听
// source.onmessage =function (event) {
// var data = event.data;
// };
// 下面这两个监听也可以写成addEventListener的形式
source.onopen = function () {
console.log('SSE 连接已打开');
};
// 处理连接错误
source.onerror = function (error: any) {
console.error('SSE 连接错误:', error);
};
setSseSource(source);
};
// 关闭连接
sseSource.close();
这种原生的sse连接是不能设置请求头的,但是在业务上接口肯定是要鉴权需要传递token的,那么怎么办呢? 我们可以使用event-source-polyfill这个库
const source = new EventSourcePolyfill(`/api/wisdom/terminal/stats/change/notify/${companyId}`, {
headers: {
Authorization: sessionStorage.get(StorageKey.TOKEN) || storage.get(StorageKey.TOKEN),
COMPANYID: storage.get(StorageKey.COMPANYID),
COMPANYTYPE: 1,
CT: 13
}
});
//其它的事件监听和原生的是一样
后端代码实现
后端最关键的是设置将响应头的Content-Type设置为text/event-stream、Cache-Control设置为no-cache、Connection设置为keep-alive。每次发消息需要在消息体结尾用"/n/n"进行分割,一个消息体有多个字段每个字段的结尾用"/n"分割。
var http = require("http");
http.createServer(function (req, res) {
var fileName = "." + req.url;
if (fileName === "./stream") {
res.writeHead(200, {
"Content-Type":"text/event-stream",
"Cache-Control":"no-cache",
"Connection":"keep-alive",
"Access-Control-Allow-Origin": '*',
});
res.write("retry: 10000\n");
res.write("event: connecttime\n");
res.write("data: " + (new Date()) + "\n\n");
res.write("data: " + (new Date()) + "\n\n");
interval = setInterval(function () {
res.write("data: " + (new Date()) + "\n\n");
}, 1000);
req.connection.addListener("close", function () {
clearInterval(interval);
}, false);
}
}).listen(8844, "127.0.0.1");
其它开发中遇到的问题
我在开发调试中用的是umi,期间遇到个问题就是sse连接上了但是在控制台一直没有返回消息,后端那边又是正常发出了的,灵异的是在后端把服务干掉的一瞬间可以看到控制台一下接到好多消息。我便怀疑是umi的代理有问题,然后我就去翻umi的文档,看到了下面的东西:
一顿操作之后正常
来源:juejin.cn/post/7424908830902042658
离职后的这半年,我前所未有的觉得这世界是值得的
大家好,我是一名前端开发工程师,属于是没有赶上互联网红利,但赶上了房价飞涨时代的 95 后社畜。2024 年 3 月份我做了个决定,即使已经失业半年、负收入 10w+ 的如今的我,也毫不后悔的决定:辞职感受下这个世界。
为什么要辞职,一是因为各种社会、家庭层面的处境对个人身心的伤害已经达到了不可逆转的程度,传播互联网负面情绪的话我也不想多说了,经历过的朋友懂得都懂,总结来说就是,在当前处境和环境下,已经没有办法感受到任何的快乐了,只剩焦虑、压抑,只能自救;二是我觉得人这一辈子,怎么也得来一次难以忘怀、回忆起来能回甘的经历吧!然而在我的计划中,不辞职的话,做不到。
3 月
在 3 月份,我去考了个摩托车驾-照,考完后购买了一辆摩托车 DL250,便宜质量也好,开始着手准备摩旅。
4 月份正式离职后,我的初步计划是先在杭州的周边上路骑骑练下车技,直接跑长途还是很危险的,这在我后面真的去摩旅时候感受颇深,差点交代了。
4 月
4.19 号我正式离职,在杭州的出租屋里狠狠地休息了一个星期,每天睡到自然醒,无聊了就打打游戏,或者骑着摩托车去周边玩,真的非常非常舒服。
不过在五一之前,我家里人打电话跟我说我母亲生病了,糖尿病引发的炎症,比较严重,花了 2w+ 住院费,也是从这个时候才知道我父母都没有交医保(更别说社保),他们也没有正式、稳定的工作,也没有一分钱存款,于是我立马打电话给老家的亲戚让一个表姐帮忙去交了农村医保。所有这些都是我一个人扛,还有个亲哥时不时问我借钱。
说实话,我不是很理解我的父母为什么在外打工那么多年,一分钱都存不下来的,因为我从小比较懂事,没让他们操过什么心,也没花过什么大钱。虽然从农村出来不是很容易,但和周围的相同条件的亲戚对比,我只能理解为我父母真的爱玩,没有存钱的概念。
我可能也继承了他们的基因吧?才敢这样任性的离职。过去几年努力地想去改变这个处境,发现根本没用,还把自己搞得心力交瘁,现在想想不如让自己活开心些吧。
5 月
母亲出院后,我回到杭州和摩友去骑了千岛湖,还有周边的一些山啊路啊,累计差不多跑了 2000 多公里,于是我开始确立我的摩旅计划,路线是杭州-海南岛-云南-成都-拉萨,后面实际跑的时候,因为云南之前去过,时间又太赶,就没去云南了。
6 月
在摩友的帮助下,给摩托车简单进行了一些改装,主要加了大容量的三箱和防雨的驮包,也配备了一些路上需要的药品、装备,就一个人出发了。
从杭州到海南这部分旅行,我也是简单记录了一下,视频我上传了 B 站,有兴趣的朋友可以看看:
拯救焦虑的29岁,考摩托车驾-照,裸辞,买车,向着自由,出发。
摩托车确实是危险的,毕竟肉包铁,即使大部分情况我已经开的很慢,但是仍然会遇到下大雨路滑、小汽车别我、大货车擦肩而过这种危险情况,有一次在过福建的某个隧道时,那时候下着大雨,刚进隧道口就轮胎打滑,对向来车是连续的大货车,打滑之后摩托车不受控制,径直朝向对向车道冲过去,那两秒钟其实我觉得已经完蛋了,倒是没有影视剧中的人生画面闪回,但是真的会在那个瞬间非常绝望,还好我的手还是强行在对龙头进行扳正,奇迹般地扳回来且稳定住了。
过了隧道惊魂未定,找了个路边小店蹲在地上大口喘气,雨水打湿了全身加上心情无法平复,我全身都是抖的,眼泪也止不住流,不是害怕,是那种久违地从人类身体发出的求生本能让我控制不住情绪的肆意发泄。
在国道开久了人也会变得很麻木,因为没什么风景,路况也是好的坏的各式各样,我现在回看自己的记录视频,有的雨天我既然能在窄路开到 100+ 码,真的很吓人,一旦摔车就是与世长辞了。
不过路上的一切不好的遭遇,在克服之后,都会被给予惊喜,到达海南岛之后,我第一次感觉到什么叫精神自由,沿着海边骑行吹着自由的风,到达一个好看的地方就停车喝水观景,玩沙子,没有工作的烦扰,没有任何让自己感受到压力的事情,就像回到了小时候无忧无虑玩泥巴的日子,非常惬意。
在完成海南环岛之后,我随即就赶往成都,与前公司被裁的前同事碰面了。我们在成都玩了三天左右,主要去看了一直想看的大熊猫🐼!
之后我们在 6.15 号开始从成都的 318 起始点出发,那一天的心情很激动,感觉自己终于要做一件不太一样的事,见不一样的风景了。
小时候在农村,读书后在小镇,大学又没什么经济能力去旅行,见识到的事物都非常有限,但是这一切遗憾在川藏线上彻底被弥补了。从开始进入高原地貌,一路上的风景真的美到我哭!很多时候我头盔下面都是情不自禁地笑着的,发自内心的那种笑,那种快乐的感觉,我已经很久很久很久没有了。
同样地,这段经历我也以视频的方式记录了下来,有兴趣的朋友可以观看:
以前只敢想想,现在勇敢向前踏出了一步,暂时放下了工作,用摩托跑完了318
到拉萨了!
花了 150 大洋买的奖牌,当做证明也顺便做慈善了:)
后面到拉萨之后我和朋友分开了,他去自驾新疆,我转头走 109 国道,也就是青藏线,这条线真的巨壮美,独自一人行驶在这条路,会感觉和自然融合在了一起,一切都很飘渺,感觉自己特别渺小。不过这条线路因为冻土层和大货车非常非常多的原因,路已经凹凸不平了,许多炮弹坑,稍微骑快点就会飞起来。
这条线还会经过青海湖,我发誓青海湖真的是我看到过最震撼的景色了,绿色和蓝色的完美融合,真的非常非常美,以后还要再去!
拍到了自己的人生照片:
经历了接近一个半月的在外漂泊,我到了西宁,感觉有点累了,我就找了个顺丰把摩托车拖运了,我自己就坐飞机回家了。
这一段经历对我来说非常宝贵,遇到的有趣的人和事,遭遇的磨难,见到的美景我无法大篇幅细说,但是每次回想起这段记忆我都会由衷地感觉到快乐,感觉自己真的像个人一样活着。
这次旅行还给了我感知快乐和美的能力,回到家后,我看那些原来觉得并不怎么样的风景,现在觉得都很美,而且我很容易因为生活中的小确幸感到快乐,这种能力很重要。
7 月
回到家大概 7 月中旬。
这两个多月的经历,我的身体和心态都调整的不错了,但还不是很想找工作,感觉放下内心的很多执念后,生活还是很轻松的,就想着在家里好好陪陪母亲吧,上班那几年除了过年都没怎么回家。
在家里没什么事,但是后面工作的技能还是要继续学习的,之前工作经历是第一家公司用的 React 16,后面公司用的是 Vue3,对 React 有些生疏,我就完整地看了下 React 18 的文档,感觉变化也不是很大。
8、9 月
虽然放下了许多执念,对于社会评价(房子、结婚、孩子)也没有像之前一样过于在乎了,但还是要生活的,也要有一定积蓄应对未来风险,所以这段时间在准备面试,写简历、整理项目、看看技术知识点、刷刷 leetcode。
也上线了一个比较有意义的网站,写了一个让前端开发者更方便进行 TypeScript 类型体操的网站,名字是 TypeRoom 类型小屋,题源是基于 antfu 大佬的 type-challenges。
目前 Type Challenges 官方提供了三种刷题方式
- 通过 TypeScript Playground 方式,利用 TypeScript 官方在线环境来刷题。
- 克隆 type-challenges 项目到本地进行刷题。
- 安装 vscode 插件来刷题。
这几种方式其实都很方便,不过都在题目的可读性上有一定的不足,还对开发者有一定的工具负担、IDE 负担。
针对这个问题,也是建立 TypeRoom 的第一个主要原因之一,就是提供直接在浏览器端就能刷题的在线环境,并且从技术和布局设计上让题目描述和答题区域区分开来,更为直观和清晰。不需要额外再做任何事,打开一个网址即可直接开始刷题,并且你的答题记录会存储到云端。
欢迎大家来刷题,网址:typeroom.cn
因为个人维护,还有很多题目没翻译,很多题解没写,也还有很多功能没做,有兴趣一起参与的朋友可以联系我哦,让我一起造福社区!
同时也介绍下技术栈吧:
前端主要使用 Vue3 + Pinia + TypeScript,服务端一开始是 Koa2 的,后面用 Nest 重写了,所以现在服务端为 Nest + Mysql + TypeORM。
另外,作为期待了四年,每一个预告片都看好多遍的《黑神话·悟空》的铁粉,玩了四周目,白金了。
现在
现在是 10 月份了,准备开始投简历找工作了,目前元气满满,不急不躁,对工作没有排斥感了,甚至想想工作还蛮好的,可能是闲久了吧,哈哈哈,人就是贱~
最后
其实大多数我们活得很累,都是背负的东西太多了,而这些大多数其实并不一定要接受的,发挥主观能动性,让自己活得开心些最重要,加油啊,各位,感谢你看到这里,祝你快乐!
这是我的 github profile,上面有我的各种联系方式,想交个朋友的可以加我~❤️
来源:juejin.cn/post/7424902549256224804
为什么JQuery会被淘汰?Vue框架就一定会比JQuery好吗?
前言
曾经面试时碰到过一个问题:为什么现有的Vue框架开发可以淘汰之前的JQuery?
我回答:Vue框架无需自己操作DOM,可以避免自己频繁的操作DOM
面试官接着反问我:Vue框架无需自己操作DOM,有什么优势吗,不用操作DOM就一定是好的吗?
我懵了,在我的认知里Vue框架无需自己操作DOM性能是一定优于自己来操作DOM元素的,其实并不是的.....
声明式框架与命令式框架
首先我们得了解声明式框架和命令式框架的区别
命令式框架关注过程
JQuery就是典型的命令式框架
例如我们来看如下一段代码
$( "button.continue" ).html( "Next Step..." ).on('click', () => { alert('next') })
这段代码的含义就是先获取一个类名为continue的button元素,它的内容为 Next Step...,并为它绑定一个点击事件。可以看到自然语言描述与代码是一一对应的,这更符合我们做事的逻辑
声明式框架更关注结果
现有的Vue,React都是典型的声明式框架
接着来看一段Vue的代码
<button class="continue" @click="() => alert('next')">Next Step...</button>
这是一段类HTML模板,它更像是直接提供一个结果。至于怎么实现这个结果,就交给Vue内部来实现,开发者不用关心
性能比较
首先告诉大家结论:声明式代码性能不优于命令式代码性能
即:声明式代码性能 <= 命令式代码性能
为什么会这样呢?
还是拿上面的代码举例
假设我们要将button的内容改为 pre Step,那么命令式的实现就是:
button.textContent = "pre Step"
很简单,就是直接修改
声明式的实现就是:
<!--之前 -->
<button class="continue" @click="() => alert('next')">Next Step...</button>
<!--现在 -->
<button class="continue" @click="() => alert('next')">pre Step</button>
对于声明式框架来说,它需要找到更改前后的差异并只更新变化的地方。但是最终更新的代码仍然是
button.textContent = "pre Step"
假设直接修改的性能消耗为 A, 找出差异的性能消耗为 B,
那么就有:
- 命令式代码的更新性能消耗 = A
- 声明式代码的更新性能消耗 = A + B
可以看到声明式代码永远要比命令式代码要多出找差异的性能消耗
那既然声明式代码的性能无法超越命令式代码的性能,为什么我们还要选择声明式代码呢?这就要考虑到代码可维护性的问题了。当项目庞大之后,手动完成dom的创建,更新与删除明显需要更多的时间和精力。而声明式代码框架虽然牺牲了一点性能,但是大大提高了项目的可维护性,降低了开发人员的心智负担
那么,有没有办法能同时兼顾性能和可维护性呢?
有!那就是使用虚拟dom
虚拟Dom
首先声明一个点,命令式代码只是理论上会比声明式代码性能高。因为在实际开发过程中,尤其是项目庞大之后,开发人员很难写出绝对优化的命令式代码。
而Vue框架内部使用虚拟Dom + 内部封装Dom元素操作的方式,能让我们不用付出太多精力的同时,还能保证程序的性能下限,甚至逼近命令式代码的性能
在讨论虚拟Dom的性能之前,我们首先要说明一个点:JavaScript层面的计算所需时间要远低于Dom层面的计算所需时间 看过浏览器渲染与解析机制的同学应该很明白为什么会这样。
我们在使用原生JavaScript编写页面时,很喜欢使用innerHTML,这个方法非常特殊,下面我们来比较一下使用虚拟Dom和使用innerHTML的性能差异
创建页面时
我们在使用innerHTML创建页面时,通常是这样的:
const data = "hello"
const htmlString = `<div>${data}</div>`
domcument.querySelect('.target').innerHTML = htmlString
这个过程需要先通过JavaScript层的字符串运算,然后是Dom层的innerHTML的Dom运算 (将字符串赋值给Dom元素的innerHTML属性时会将字符串解析为Dom树)
而使用虚拟Dom的方式通常是编译用户编写的类html模板得到虚拟Dom(JavaScript对象),然后遍历虚拟Dom树创建真实Dom对象
两者比较:
innerHTML | 虚拟Dom | |
---|---|---|
JavaScript层面运算 | 计算拼接HTML字符串 | 创建JavaScript对象(虚拟Dom) |
Dom层面运算 | 新建所有Dom元素 | 新建所有Dom元素 |
可以看到两者在创建页面阶段的性能差异不大。尽管在JavaScript层面,创建虚拟Dom对象貌似更耗时间,但是总体来说,Dom层面的运算是一致的,两者属于同一数量级,宏观来看可认为没有差异
更新页面时
使用innerHTML更新页面,通常是这样:
//更新
const newData = "hello world"
const newHtmlString = `<div>${newData}</div>`
domcument.querySelect('.target').innerHTML = newHtmlString
这个过程同样是先通过JavaScript层的字符串运算,然后是Dom层的innerHTML的Dom运算。但是它在Dom层的运算是销毁所有旧的DOM元素,再全量创建新的DOM元素
而使用虚拟Dom的方式通常是重新创建新的虚拟Dom(JavaScript对象),然后比较新旧虚拟Dom,找到需要更改的地方并更新Dom元素
两者比较:
innerHTML | 虚拟Dom | |
---|---|---|
JavaScript层面运算 | 计算拼接HTML字符串 | 创建JavaScript对象(虚拟Dom)+ Diff算法 |
Dom层面运算 | 销毁所有旧的Dom元素,新建所有新的DOM元素 | 必要的DOM更新 |
可以看到虚拟DOM在JavaScript层面虽然多出一个Diff算法的性能消耗,但这毕竟是JavaScript层面的运算,不会产生数量级的差异。而在DOM层,虚拟DOM可以只更新差异部分,对比innerHTML的全量卸载与全量更新性能消耗要小得多。所以模板越大,元素越多,虚拟DOM在更新页面的性能上就越有优势
总结
现在我们可以回答这位面试官的问题了:JQuery属于命令式框架,Vue属于声明式框架。在理论上,声明式代码性能是不优于命令式代码性能的,甚至差于命令式代码的性能。但是声明式框架无需用户手动操作DOM,用户只需关注数据的变化。声明式框架在牺牲了一点性能的情况下,大大降低了开发难度,提高了项目的可维护性,且声明式框架通常使用虚拟DOM的方式,使其在更新页面时的性能大大提升。综合来说,声明式框架仍旧是更好的选择
来源:juejin.cn/post/7425121392738615350
每一个失业的前端er都必须有一个稳定盈利的独立开发项目
如题,最近非常焦虑,因为考试临近了,所以只好来祸害一下网友了
俺从2023年离职,经历了考研,独立开发,remote,好几段经历
首先是考研,去年考的其实还行,但还是复试被刷,至今被刷原因未知,盲猜是因为本科是民办三本吧
然后remote就是找了个美国的区块链公司,但是因为四月份我忙着搞调剂,过程十分煎熬,根本无心顾暇remote那边天天开会的节奏,所以只能离职,当然啦,最终也没调剂上
这都不是重点,重点是独立开发
从我离职到现在,也快两年了,聪明的人已经发现了,整个互联网技术栈这两年可以说毫无变化,新的端没有,新的框架没有,新的红利也没有,新的独角兽公司也没有
道理很简单,因为现在是僧多粥少的时代,每个人手机上就固定几个app,而且都是存量状态(不需要推翻重来,只需要shi山跳舞)
与此同时,还有若干小公司不断倒闭
懂了吧,现在是需求没了,业务没了,招聘的公司没了
独立开发就只不过是,没有业务,我们自己发现制造业务罢了
但是呢,会更难,因为,资本虽然是傻逼,但它们也不是完全没脑子,如果轻易能成功,他们就不需要跑路了
现实就是,我朋友圈有很多独立开发的,推特上也有很多,但能做到稳定盈利的人,几乎为0
有的是卖小册,有的是搞博客,还有开公司做面试辅导的,也有外包接活的,也有收费技术咨询的
这些统统都是噶韭菜——因为我说的很清楚了,现在是业务没了,是需求没了,但凡不制造需求的,都是瞎扯
——所以我把c站卖了,c站转让前日活5w,但是动漫行业实在太卷了,各种各样的竞品,让我自己都不想看番,更别提服务给他人看了
之前在携程,我的老板和我说,你就当独立创业,携程三万人就是你的第一批客户,我觉得老板说的没错,就是比起b端,我更喜欢c端的用户
所以毫无疑问,我不可能再回去写前端框架了,纯粹浪费时间,浪费我的❤
唉,说了这么多,总而言之,言而总之
回到题目,那就是,每个人失业的前端er都必须有一个稳定盈利的独立开发项目
我也在开新坑了,敬请期待~
来源:juejin.cn/post/7426258631161528335
老板想集成地图又不想花钱,于是让我...
前言
在数字化时代,地图服务已成为各类应用的标配,无论是导航、位置分享还是商业分析,地图都扮演着不可或缺的角色。然而,高质量的地图服务往往伴随着不菲的授权费用。公司原先使用的是国内某知名地图服务,但随着业务的扩展和成本的考量,老板决定寻找一种成本更低的解决方案。于是,我们的目光转向了免费的地图服务——天地图。
天地图简介
天地图(lbs.tianditu.gov.cn/server/guid…
是中国领先的在线地图服务之一,提供全面的地理信息服务。它的API支持地理编码、逆地理编码、周边搜索等多种功能,且完全免费。这正是我们需要的。
具体实现代码
为了将天地图集成到我们的系统中,我们需要进行一系列的开发工作。以下是实现过程中的关键代码段。
1. 逆地理编码
逆地理编码是将经纬度转换为可读的地址。在天地图中,这一功能可以通过以下代码实现:
public static MapLocation reverseGeocode(String longitude, String latitude) {
Request request = new Request();
LocateInfo locateInfo = GCJ02_WGS84Utils.gcj02_To_Wgs84(Double.valueOf(latitude), Double.valueOf(longitude));
longitude = String.valueOf(locateInfo.getLongitude());
latitude = String.valueOf(locateInfo.getLatitude());
String postStr = String.format(REVERSE_GEOCODE_POST_STR, longitude, latitude);
String encodedPostStr = null;
try {
encodedPostStr = URLEncoder.encode(postStr, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String url = REVERSE_GEOCODE_URL + "?tk=" + TK + "&type=" + GEOCODE + "&postStr=" + encodedPostStr;
request.setUrl(url);
Response<String> response = HttpClientHelper.getWithoutUserAgent(request, null);
if (response.getSuccess()) {
String body = response.getBody();
JSONObject jsonObject = JSON.parseObject(body);
String status = jsonObject.getString("status");
if (!"0".equals(status)) {
return null;
}
JSONObject resultObject = jsonObject.getJSONObject("result");
MapLocation mapLocation = new MapLocation();
String formattedAddress = resultObject.getString("formatted_address");
mapLocation.setAddress(formattedAddress);
String locationStr = resultObject.getString("location");
JSONObject location = JSON.parseObject(locationStr);
String lon = location.getString("lon");
String lat = location.getString("lat");
locateInfo = GCJ02_WGS84Utils.wgs84_To_Gcj02(Double.valueOf(lat), Double.valueOf(lon));
lon = String.valueOf(locateInfo.getLongitude());
lat = String.valueOf(locateInfo.getLatitude());
mapLocation.setLongitude(lon);
mapLocation.setLatitude(lat);
JSONObject addressComponent = resultObject.getJSONObject("addressComponent");
String address = addressComponent.getString("address");
mapLocation.setName(address);
mapLocation.setCity(addressComponent.getString("city"));
return mapLocation;
}
return null;
}
2. 周边搜索
周边搜索允许我们根据一个地点的经纬度搜索附近的其他地点。实现代码如下:
public static List<MapLocation> nearbySearch(String query, String longitude, String latitude, String radius) {
LocateInfo locateInfo = GCJ02_WGS84Utils.gcj02_To_Wgs84(Double.valueOf(latitude), Double.valueOf(longitude));
longitude = String.valueOf(locateInfo.getLongitude());
latitude = String.valueOf(locateInfo.getLatitude());
Request request = new Request();
String longLat = longitude + "," + latitude;
String postStr = String.format(NEARBY_SEARCH_POST_STR, query, Integer.valueOf(radius), longLat);
String encodedPostStr = null;
try {
encodedPostStr = URLEncoder.encode(postStr, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String url = SEARCH_URL + "?tk=" + TK + "&type=" + QUERY + "&postStr=" + encodedPostStr;
request.setUrl(url);
Response<String> response = HttpClientHelper.getWithoutUserAgent(request, null);
List<MapLocation> list = new ArrayList<>();
if (response.getSuccess()) {
String body = response.getBody();
JSONObject jsonObject = JSON.parseObject(body);
JSONObject statusObject = jsonObject.getJSONObject("status");
String infoCode = statusObject.getString("infocode");
if (!"1000".equals(infoCode)) {
return new ArrayList<>();
}
String resultType = jsonObject.getString("resultType");
String count = jsonObject.getString("count");
if (!"1".equals(resultType) || "0".equals(count)) {
return new ArrayList<>();
}
JSONArray poisArray = jsonObject.getJSONArray("pois");
for (int i = 0; i < poisArray.size(); i++) {
JSONObject poiObject = poisArray.getJSONObject(i);
MapLocation mapLocation = new MapLocation();
mapLocation.setName(poiObject.getString("name"));
mapLocation.setAddress(poiObject.getString("address"));
String lonlat = poiObject.getString("lonlat");
String[] lonlatArr = lonlat.split(",");
locateInfo = GCJ02_WGS84Utils.wgs84_To_Gcj02(Double.valueOf(lonlatArr[1]), Double.valueOf(lonlatArr[0]));
String lon = String.valueOf(locateInfo.getLongitude());
String lat = String.valueOf(locateInfo.getLatitude());
mapLocation.setLongitude(lon);
mapLocation.setLatitude(lat);
list.add(mapLocation);
}
}
return list;
}
3. 文本搜索
文本搜索功能允许用户根据关键词搜索地点。实现代码如下:
public static List<MapLocation> searchByText(String query, String mapBound) {
Request request = new Request();
String postStr = String.format(SEARCH_BY_TEXT_POST_STR, query, mapBound);
String encodedPostStr = null;
try {
encodedPostStr = URLEncoder.encode(postStr, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String url = SEARCH_URL + "?tk=" + TK + "&type=" + QUERY + "&postStr=" + encodedPostStr;
request.setUrl(url);
Response<String> response = HttpClientHelper.getWithoutUserAgent(request, null);
List<MapLocation> list = new ArrayList<>();
if (response.getSuccess()) {
String body = response.getBody();
JSONObject jsonObject = JSON.parseObject(body);
JSONObject statusObject = jsonObject.getJSONObject("status");
String infoCode = statusObject.getString("infocode");
if (!"1000".equals(infoCode)) {
return new ArrayList<>();
}
String resultType = jsonObject.getString("resultType");
String count = jsonObject.getString("count");
if (!"1".equals(resultType) || "0".equals(count)) {
return new ArrayList<>();
}
JSONArray poisArray = jsonObject.getJSONArray("pois");
for (int i = 0; i < poisArray.size(); i++) {
JSONObject poiObject = poisArray.getJSONObject(i);
MapLocation mapLocation = new MapLocation();
mapLocation.setName(poiObject.getString("name"));
mapLocation.setAddress(poiObject.getString("address"));
String lonlat = poiObject.getString("lonlat");
String[] lonlatArr = lonlat.split(",");
LocateInfo locateInfo = GCJ02_WGS84Utils.wgs84_To_Gcj02(Double.valueOf(lonlatArr[1]), Double.valueOf(lonlatArr[0]));
String lon = String.valueOf(locateInfo.getLongitude());
String lat = String.valueOf(locateInfo.getLatitude());
mapLocation.setLongitude(lon);
mapLocation.setLatitude(lat);
list.add(mapLocation);
}
}
return list;
}
4. 坐标系转换
由于天地图使用的是WGS84坐标系,而国内常用的是GCJ-02坐标系,因此我们需要进行坐标转换。以下是坐标转换的工具类:
/**
* WGS-84:是国际标准,GPS坐标(Google Earth使用、或者GPS模块)
* GCJ-02:中国坐标偏移标准,Google Map、高德、腾讯使用
* BD-09:百度坐标偏移标准,Baidu Map使用(经由GCJ-02加密而来)
* <p>
* 这些坐标系是对真实坐标系统进行人为的加偏处理,按照特殊的算法,将真实的坐标加密成虚假的坐标,
* 而这个加偏并不是线性的加偏,所以各地的偏移情况都会有所不同,具体的内部实现是没有对外开放的,
* 但是坐标之间的转换算法是对外开放,在网上可以查到的,此算法的误差在0.1-0.4之间。
*/
public class GCJ02_WGS84Utils {
public static double pi = 3.1415926535897932384626;//圆周率
public static double a = 6378245.0;//克拉索夫斯基椭球参数长半轴a
public static double ee = 0.00669342162296594323;//克拉索夫斯基椭球参数第一偏心率平方
/**
* 从GPS转高德
* isOutOfChina 方法用于判断经纬度是否在中国范围内,如果不在中国范围内,则直接返回原始的WGS-84坐标。
* transformLat 和 transformLon 是辅助函数,用于进行经纬度的转换计算。
* 最终,wgs84ToGcj02 方法返回转换后的GCJ-02坐标系下的经纬度。
*/
public static LocateInfo wgs84_To_Gcj02(double lat, double lon) {
LocateInfo info = new LocateInfo();
if (isOutOfChina(lat, lon)) {
info.setChina(false);
info.setLatitude(lat);
info.setLongitude(lon);
} else {
double dLat = transformLat(lon - 105.0, lat - 35.0);
double dLon = transformLon(lon - 105.0, lat - 35.0);
double radLat = lat / 180.0 * pi;
double magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
double sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);
double mgLat = lat + dLat;
double mgLon = lon + dLon;
info.setChina(true);
info.setLatitude(mgLat);
info.setLongitude(mgLon);
}
return info;
}
//从高德转到GPS
public static LocateInfo gcj02_To_Wgs84(double lat, double lon) {
LocateInfo info = new LocateInfo();
LocateInfo gps = transform(lat, lon);
double lontitude = lon * 2 - gps.getLongitude();
double latitude = lat * 2 - gps.getLatitude();
info.setChina(gps.isChina());
info.setLatitude(latitude);
info.setLongitude(lontitude);
return info;
}
// 判断坐标是否在国外
private static boolean isOutOfChina(double lat, double lon) {
if (lon < 72.004 || lon > 137.8347)
return true;
if (lat < 0.8293 || lat > 55.8271)
return true;
return false;
}
//转换
private static LocateInfo transform(double lat, double lon) {
LocateInfo info = new LocateInfo();
if (isOutOfChina(lat, lon)) {
info.setChina(false);
info.setLatitude(lat);
info.setLongitude(lon);
return info;
}
double dLat = transformLat(lon - 105.0, lat - 35.0);
double dLon = transformLon(lon - 105.0, lat - 35.0);
double radLat = lat / 180.0 * pi;
double magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
double sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);
double mgLat = lat + dLat;
double mgLon = lon + dLon;
info.setChina(true);
info.setLatitude(mgLat);
info.setLongitude(mgLon);
return info;
}
//转换纬度所需
private static double transformLat(double x, double y) {
double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y
+ 0.2 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(y * pi) + 40.0 * Math.sin(y / 3.0 * pi)) * 2.0 / 3.0;
ret += (160.0 * Math.sin(y / 12.0 * pi) + 320 * Math.sin(y * pi / 30.0)) * 2.0 / 3.0;
return ret;
}
//转换经度所需
private static double transformLon(double x, double y) {
double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1
* Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(x * pi) + 40.0 * Math.sin(x / 3.0 * pi)) * 2.0 / 3.0;
ret += (150.0 * Math.sin(x / 12.0 * pi) + 300.0 * Math.sin(x / 30.0 * pi)) * 2.0 / 3.0;
return ret;
}
}
结论
通过上述代码,我们成功地将天地图集成到了我们的系统中,不仅满足了功能需求,还大幅降低了成本。这一过程中,我们深入理解了地图服务的工作原理,也提升了团队的技术能力。
注意事项
- 确保在使用天地图API时遵守其服务条款,尤其是在商业用途中。
- 由于网络或其他原因,天地图API可能存在访问延迟或不稳定的情况,建议在生产环境中做好异常处理和备用方案。
- 坐标系转换是一个复杂的过程,确保使用可靠的算法和工具进行转换,以保证定位的准确性。
通过这次集成,我们不仅为公司节省了成本,还提升了系统的稳定性和用户体验。在未来的开发中,我们将继续探索更多高效、低成本的技术解决方案。
来源:juejin.cn/post/7419524888041472009
凌晨四点,线上CPU告警,绩效没了
前言
凌晨4点,我被一阵刺耳的手机铃声惊醒。迷迷糊糊地摸索着手机,屏幕上赫然显示着"线上CPU告警"的字样。瞬间,我的困意全无,取而代之的是一阵冷汗和心跳加速。作为公司核心系统的负责人,我深知这意味着什么——用户体验受损、可能的数据丢失,更糟糕的是,我的年终绩效可能就此化为泡影。
我迅速起身,开始了一场与时间赛跑的故障排查之旅。
1. 初步诊断:快速定位问题
首先,我登录了服务器,使用top命令查看系统资源使用情况:
$ top
输出显示CPU使用率接近100%,load average远超服务器核心数。这确实是一个严重的问题。
接下来,我使用htop命令获取更详细的进程信息:
$ htop
我发现有几个Java进程占用了大量CPU资源。这些进程正是我们的核心服务。
2. JVM层面分析:寻找热点方法
确定了问题出在Java应用上,我开始进行JVM层面的分析。首先使用jstat命令查看GC情况:
$ jstat -gcutil [PID] 1000 10
输出显示Full GC频繁发生,这可能是导致CPU使用率高的原因之一。
接着,我使用jstack命令生成线程转储,查看线程状态:
$ jstack [PID] > thread_dump.txt
分析thread dump文件,我发现大量线程处于RUNNABLE状态,执行着相似的方法调用。
为了进一步定位热点方法,我使用了async-profiler工具:
$ ./profiler.sh -d 30 -f cpu_profile.svg [PID]
生成的火焰图清晰地显示了一个自定义的排序算法占用了大量CPU时间。
3. 应用层面优化:重构算法
找到了罪魁祸首,我立即查看了相关代码。这是一个用于大量数据的自定义排序算法,原本设计用于小规模数据,但随着业务增长,它的性能问题暴露无遗。
我迅速重构了算法,使用Java 8的并行流进行优化:
List<Data> sortedData = data.parallelStream()
.sorted(Comparator.comparing(Data::getKey))
.collect(Collectors.toList());
同时,我添加了缓存机制,避免重复计算:
@Cacheable("sortedData")
public List<Data> getSortedData() {
// 优化后的排序逻辑
}
4. 数据库优化:索引与查询改进
在排查过程中,我还发现了一些低效的数据库查询。使用explain命令分析SQL语句:
EXPLAIN SELECT * FROM large_table WHERE status = 'ACTIVE';
结果显示这个查询导致了全表扫描。我立即添加了合适的索引:
CREATE INDEX idx_status ON large_table(status);
并重写了部分ORM查询,使用更高效的原生SQL:
@Query(value = "SELECT * FROM large_table WHERE status = :status", nativeQuery = true)
List<LargeTable> findByStatus(@Param("status") String status);
5. 部署优化:资源隔离
为了防止单个服务影响整个系统,我决定使用Docker进行资源隔离。创建了如下的Dockerfile:
FROM openjdk:11-jre-slim
COPY target/myapp.jar app.jar
ENTRYPOINT ["java", "-Xmx2g", "-jar", "/app.jar"]
并使用Docker Compose进行服务编排,限制了CPU和内存使用:
version: '3'
services:
myapp:
build: .
deploy:
resources:
limits:
cpus: '0.50'
memory: 512M
6. 监控告警:防患未然
最后,为了避免类似问题再次发生,我升级了监控系统。使用Prometheus和Grafana搭建了全面的监控平台,并设置了更加智能的告警规则:
- alert: HighCPUUsage
expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
for: 5m
labels:
severity: warning
annotations:
summary: "High CPU usage detected"
description: "CPU usage is above 80% for more than 5 minutes"
结语:危机与成长
经过近4小时的奋战,系统终于恢复了正常。CPU使用率降到了30%以下,服务响应时间也恢复到了毫秒级。
这次经历让我深刻意识到,在追求业务快速发展的同时,我们不能忽视技术债务的累积。定期的代码审查、性能测试和压力测试是必不可少的。同时,建立完善的监控和告警机制,能够帮助我们更快地发现和解决问题。
虽然这次事件可能会影响我的年终绩效,但它带给我的经验和教训是无价的。持续学习和改进永远是我们的必修课。
凌晨的阳台上,我望着渐亮的天空,心中暗自庆幸:又一次化险为夷。但我知道,明天将是新的挑战,我们还有很长的路要走。
来源:juejin.cn/post/7424522247791247394
我终于从不想上班又不能裸辞的矛盾中挣扎出来了
最近的状态有一种好像一个泄了气的皮球的感觉一样,就是对生活中很多事情都提不起来兴趣。
我希望自己可以多看一点书,但是我不想动;我希望自己可以练习书法,但是我不想动;我希望自己可以学会一门乐器,但是我不想动。
相比上面三点,我更希望的是我可以早上起来不用上班,但是这只是我的希望而已。
这就是我最近的生活状态。
我有一种我的生活仿佛失去了控制的感觉,每一天我的内心好像都有一个小人在不断呐喊,说我不想上班。因为这个声音,我一度非常非常想要裸辞,但是我为什么没有裸辞呢?
还不是因为我买房买车欠了十几万,我到现在才还了两万而已,再加上我每个月还有房贷要还。
然而,当我经常不情愿地做着跟我心里想法相悖的行为的时候,我发现自己常常会做一些小动作来向自己表达抗议和不满。
比如说,我的工作会变得越来越低效,上班的时候会偷偷地摸鱼,还有就是变得越来越容易拖延。
就好像这样的我,可以让那个不想上班的我,取得了一丢丢的小胜利一样。
一旦开始接受自己没有办法辞职,并且还要上个几十年班这样的结果时,就会让人有一种破罐子破摔的想法。
而且随之而来的是一种对未来,对生活的无力感。
这种无力感渐渐地渗透在我生活的方方面面,以至于让我慢慢地对很多东西都提不起兴趣,我生活中的常态就变成了不想动。
但是有趣的事情发生了,有一天我在和我朋友聊天的时候,我的脑子里面突然出现了一个想法,就是我决定两年之后我要实现我不上班的这个目标。
当有了这个想法之后,我就开始认真思考这件事情的可行度。
通过分析我现在收支情况,我把两年之内改成了2026年之前,因为我觉得这样会让我更加信服这个目标的可行性。
同时我把这个想法也拆分成了两个更为具体的目标,其中一个就是我要在2026年之前还完欠的所有钱。
第二个就是我需要给自己存够20万,这20万是不包括投资理财或者基金股票里面的钱,而是我完全可以自由支配的。
毕竟没有人可以在没有工作的情况下,没有收入的情况下。没有存款的情况下,还能保持一个不焦虑的状态。
当我得出了这两个具体的目标之后,我整个人瞬间被一种兴奋的状态填满,我瞬间找到了工作的意义和动力。
也许你会说,我的这个想法对我现在生活根本起不到任何的改变作用。
我依旧还需要每天七点起床,还是要每天重复地去过我两点一线的生活。
但是于我自己而言,当我给我上班的这件事情加了一个两年的期限之后,我突然觉得我的未来,我的生活都充满了希望。
我整个人从不想动的状态,变成了一种被兴奋的状态填满的感觉。
所以,如果你和我一样有一些类似的困扰,有一些你不想做而又不得不做的事情,让你有一种深陷泥潭,无法前进的感觉,那你不妨试一下这个方法。
结合你自己的实际情况,为你不想做这件事情,设计一个期限,这个期限必须要是你认可,你接受,并且你认为你可以在这个截止时间之前完成的。
我想这个决定应该会对你的生活带来一些改变。
来源:juejin.cn/post/7428154034480906278
35 岁时我改掉的三个习惯
大家好,我是双越老师,wangEditor 作者。
我正在开发一个 Nodejs 全栈 AIGC 知识库项目 划水AI,包括富文本编辑器,多人协同编辑,AI 写作,AI 处理文本等。有兴趣的同学可以围观项目。
开始
虽然标题是 35 岁,但其实本文 2024 年我 36 岁时写的。36 岁总结 35 岁,也没毛病。
35 岁对于我们程序员来说是一个非常敏感的年龄,但我已过 35 ,看看往后会越来越敏感,还是越来越麻木?
本文和大家分享一下我个人在业余生活中的,我觉得很有意义的事情。甚至我觉得这些是事情的意义,都大于工作的成绩。
生活是一个整体,工作只是其中一部分,只是我们养家糊口的一个手段。工作以外的其他部分应该更加重要,例如业余爱好、饮食、休息、娱乐、家庭等。
1. 戒烟
我从大学毕业开始学着吸烟,到 35 岁已经有十几年的烟龄了。虽然每天吸烟量不大,但也断不了,有瘾。
我为什么要戒烟呢?
是觉得吸烟有害健康?—— 对,吸烟肯定有害健康,但人类天生是一种及时行乐的动物,谁会在乎 20 年 30 年以后的健康呢?人类天生是一个心存侥幸的动物(赌徒性质),也不是每个吸烟者都会有 xxx 病,对吧?之前看过一个段子,说某呼吸外科医生做完,累几个小时做完一台手术,先出去吸烟休息一下。
我戒烟,单纯就是想戒。我想摆脱烟草和尼古丁的控制,而且是想了很久了,不是一时突发奇想,之前就有充分的心理准备。
还有,我想在 35+ 的年纪做一点叛逆的事情,叛逆使人年轻,叛逆使人保持活力。年轻时吸烟是叛逆,年龄大了戒烟就是叛逆。年轻时叛逆是狂,年龄大了叛逆是帅。所以,各位大龄程序员,有机会要让自己叛逆起来,做一点帅的事情。
最后,当时 2023 年夏天,我正好不忙,天天闲着,总得找点事儿干。既然工作上不忙,那就在自己身上找点有意义事情做吧 —— 外求不得而向内求,如果没法从外面挣取,那就去提高自身。
烟瘾是什么?就是尼古丁的戒断反应,没有其他理由,没人其他任何事情会让你 1-2 小时就想一次,而且持续想。
关于烟草的本质,烟草的历史,烟草的商业化推广过程,烟草的洗脑广告…… 还有很多内容可以讲,本文就不展开了,有兴趣的可以留言咨询。我很早之前就看过,去学习这些,并且接受这些,才能更好的帮助戒烟。
所以,就这么戒了,到写作本文的时候,正好是戒了一年。我觉得这是我去年做过的最有价值的事情,比我工作挣的钱都有价值。
2. 戒酒
之前我是比较喜欢喝酒的,喜欢一帮人聚餐那种兴奋的状态。但后来喝多了就肚子难受,一躺下就想吐,于是决定不喝了。
有些人说:你可以少喝点。但在中国北方的酒桌上,只有 0 和 1 ,没有中间态。只要你喝了,一开始朋友劝你多喝点,再喝多一点就不用别人劝了,自己就开始主动找酒瓶子了。
我不懂酒,没喝过啥好酒,很少喝红酒。就日常喝的白酒和啤酒而言,我觉得都不好喝。
白酒,度数高,辣(尤其酱味的),容易醉。全世界就中国及其周边人喝白酒,国内几千的白酒没有国际市场。而且单就中国而言,白酒蒸馏技术几百年了,也只有最近这不到 100 年喝白酒。《红楼梦》上层人不喝白酒,《孔乙己》下层人也不喝白酒。
现在喝白酒的人,有两类:1. 被酒桌文化感染而顺从的人; 2. 有酒瘾想快速体验酒精的人。
啤酒,要分好多种,咱日常喝的瓶装的、桶装的,都可以统称为工业啤酒,像水一样,轻薄寡淡 —— 但它有酒精啊!
那种全麦啤酒(忘记名字了,不常喝)还是很好喝的,但价格较高,自己买点喝还行,聚餐喝那个太贵了(普通饭店也没有卖的),很少喝。
我身边也有一些朋友,每周都喝好几次,大部分是为了工作,拉拢客户关系。我觉得我还是比较幸运的,写写代码,改改 bug ,也不用太考虑人际关系。程序员的为数不多的好处。
3. 不看和自己无关的事情
我从不刷抖音(虽然我会发布一些抖音视频),但我之前很喜欢看今日头条 app ,每天闲了就刷一下,吃饭也看,睡前也看。
但是我都看了些啥呢?有一些是娱乐性的小视频,搞笑的,猎奇的,做饭吃饭的,我觉得这些很好,提供情绪价值。
其他的,什么俄 x 战争,什么国外 xxx 冲突,什么体育明星谁比谁厉害,什么传统武术,什么中医 …… 还有这些的评论,各路网友互怼。有时候看的都让人很带情绪,感觉有些人是不是傻,这么简单的道理看不明白?有些人是不是坏,不看事实,只看立场?
这些不仅不能提供情绪价值,反而会增加你的负面情绪。而且,根据《乌合之众》大众心理学研究,你只要参与了其中,你参与了互怼,你就会成为他们其中的一员,也变成傻子或坏人。围观,也是一种参与,你的心里会支持某一方。
更关键的是,这些事情我自己有关系吗?或者我的表态能决定这件事儿的走向吗?哪怕投一票决定一点点呢 —— 答案是显然的,要么没有任何关系,要么自己瞎操心。
所以,我卸载了今日头条 app ,不看了,眼不见心不烦,这些事情我不知道,也不影响我个人工作生活。从此以后,我觉得我的世界瞬间清净了,至少不会被那些负面情绪所打扰。
另外,我睡前也不看手机了,把手机扔在书房充电,直接睡觉。如果偶尔失眠或想事情,那就想,也不用非得拿着手机想,对吧。
总结
35 岁是一个里程碑和转折点,工作上如此,生活中也是如此。程序员是一个相对来说比较“单纯”的群体,我觉得更应该关注个人生活中的成长,共勉,加油~
来源:juejin.cn/post/7417630844100247590
处理异常的13条军规
前言
在我们日常工作中,经常会遇到一些异常,比如:NullPointerException、NumberFormatException、ClassCastException等等。
那么问题来了,我们该如何处理异常,让代码变得更优雅呢?
苏三的免费刷题网站:http://www.susan.net.cn 里面:面试八股文、BAT面试真题、工作内推、工作经验分享、技术专栏等等什么都有,欢迎收藏和转发。
1 不要忽略异常
不知道你有没有遇到过下面这段代码:
反例:
Long id = null;
try {
id = Long.parseLong(keyword);
} catch(NumberFormatException e) {
//忽略异常
}
用户输入的参数,使用Long.parseLong方法转换成Long类型的过程中,如果出现了异常,则使用try/catch直接忽略了异常。
并且也没有打印任何日志。
如果后面线上代码出现了问题,有点不太好排查问题。
建议大家不要忽略异常,在后续的工作中,可能会带来很多麻烦。
正例:
Long id = null;
try {
id = Long.parseLong(keyword);
} catch(NumberFormatException e) {
log.info(String.format("keyword:{} 转换成Long类型失败,原因:{}",keyword , e))
}
后面如果数据转换出现问题,从日志中我们一眼就可以查到具体原因了。
2 使用全局异常处理器
有些小伙伴,经常喜欢在Service代码中捕获异常。
不管是普通异常Exception,还是运行时异常RuntimeException,都使用try/catch把它们捕获。
反例:
try {
checkParam(param);
} catch (BusinessException e) {
return ApiResultUtil.error(1,"参数错误");
}
在每个Controller类中都捕获异常。
在UserController、MenuController、RoleController、JobController等等,都有上面的这段代码。
显然这种做法会造成大量重复的代码。
我们在Controller、Service等业务代码中,尽可能少捕获异常。
这种业务异常处理,应该交给拦截器统一处理。
在SpringBoot中可以使用@RestControllerAdvice注解,定义一个全局的异常处理handler,然后使用@ExceptionHandler注解在方法上处理异常。
例如:
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 统一处理异常
*
* @param e 异常
* @return API请求响应实体
*/
@ExceptionHandler(Exception.class)
public ApiResult handleException(Exception e) {
if (e instanceof BusinessException) {
BusinessException businessException = (BusinessException) e;
log.info("请求出现业务异常:", e);
return ApiResultUtil.error(businessException.getCode(), businessException.getMessage());
}
log.error("请求出现系统异常:", e);
return ApiResultUtil.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务器内部错误,请联系系统管理员!");
}
}
有了这个全局的异常处理器,之前我们在Controller或者Service中的try/catch代码可以去掉。
如果在接口中出现异常,全局的异常处理器会帮我们封装结果,返回给用户。
3 尽可能捕获具体异常
在你的业务逻辑方法中,有可能需要去处理多种不同的异常。
你可能你会觉得比较麻烦,而直接捕获Exception。
反例:
try {
doSomething();
} catch(Exception e) {
log.error("doSomething处理失败,原因:",e);
}
这样捕获异常太笼统了。
其实doSomething方法中,会抛出FileNotFoundException和IOException。
这种情况我们最好捕获具体的异常,然后分别做处理。
正例:
try {
doSomething();
} catch(FileNotFoundException e) {
log.error("doSomething处理失败,文件找不到,原因:",e);
} catch(IOException e) {
log.error("doSomething处理失败,IO出现了异常,原因:",e);
}
这样如果后面出现了上面的异常,我们就非常方便知道是什么原因了。
4 在finally中关闭IO流
我们在使用IO流的时候,用完了之后,一般需要及时关闭,否则会浪费系统资源。
我们需要在try/catch中处理IO流,因为可能会出现IO异常。
反例:
try {
File file = new File("/tmp/1.txt");
FileInputStream fis = new FileInputStream(file);
byte[] data = new byte[(int) file.length()];
fis.read(data);
for (byte b : data) {
System.out.println(b);
}
fis.close();
} catch (IOException e) {
log.error("读取文件失败,原因:",e)
}
上面的代码直接在try的代码块中关闭fis。
假如在调用fis.read方法时,出现了IO异常,则可能会直接抛异常,进入catch代码块中,而此时fis.close方法没办法执行,也就是说这种情况下,无法正确关闭IO流。
正例:
FileInputStream fis = null;
try {
File file = new File("/tmp/1.txt");
fis = new FileInputStream(file);
byte[] data = new byte[(int) file.length()];
fis.read(data);
for (byte b : data) {
System.out.println(b);
}
} catch (IOException e) {
log.error("读取文件失败,原因:",e)
} finally {
if(fis != null) {
try {
fis.close();
fis = null;
} catch (IOException e) {
log.error("读取文件后关闭IO流失败,原因:",e)
}
}
}
在finally代码块中关闭IO流。
但要先判断fis不为空,否则在执行fis.close()方法时,可能会出现NullPointerException异常。
需要注意的地方时,在调用fis.close()方法时,也可能会抛异常,我们还需要进行try/catch处理。
最近就业形势比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。
你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。
添加苏三的私人微信:su_san_java,备注:掘金+所在城市,即可加入。
5 多用try-catch-resource
前面在finally代码块中关闭IO流,还是觉得有点麻烦。
因此在JDK7之后,出现了一种新的语法糖try-with-resource。
上面的代码可以改造成这样的:
File file = new File("/tmp/1.txt");
try (FileInputStream fis = new FileInputStream(file)) {
byte[] data = new byte[(int) file.length()];
fis.read(data);
for (byte b : data) {
System.out.println(b);
}
} catch (IOException e) {
e.printStackTrace();
log.error("读取文件失败,原因:",e)
}
try括号里头的FileInputStream实现了一个AutoCloseable
接口,所以无论这段代码是正常执行完,还是有异常往外抛,还是内部代码块发生异常被截获,最终都会自动关闭IO流。
我们尽量多用try-catch-resource的语法关闭IO流,可以少写一些finally中的代码。
而且在finally代码块中关闭IO流,有顺序的问题,如果有多种IO,关闭的顺序不对,可能会导致部分IO关闭失败。
而try-catch-resource就没有这个问题。
6 不在finally中return
我们在某个方法中,可能会有返回数据。
反例:
public int divide(int dividend, int divisor) {
try {
return dividend / divisor;
} catch (ArithmeticException e) {
// 异常处理
} finally {
return -1;
}
}
上面的这个例子中,我们在finally代码块中返回了数据-1。
这样最后在divide方法返回时,会将dividend / divisor的值覆盖成-1,导致正常的结果也不对。
我们尽量不要在finally代码块中返回数据。
正解:
public int divide(int dividend, int divisor) {
try {
return dividend / divisor;
} catch (ArithmeticException e) {
// 异常处理
return -1;
}
}
如果dividend / divisor出现了异常,则在catch代码块中返回-1。
7 少用e.printStackTrace()
我们在本地开发中,喜欢使用e.printStackTrace()方法,将异常的堆栈跟踪信息输出到标准错误流中。
反例:
try {
doSomething();
} catch(IOException e) {
e.printStackTrace();
}
这种方式在本地确实容易定位问题。
但如果代码部署到了生产环境,可能会带来下面的问题:
- 可能会暴露敏感信息,如文件路径、用户名、密码等。
- 可能会影响程序的性能和稳定性。
正解:
try {
doSomething();
} catch(IOException e) {
log.error("doSomething处理失败,原因:",e);
}
我们要将异常信息记录到日志中,而不是保留给用户。
8 异常打印详细一点
我们在捕获了异常之后,需要把异常的相关信息记录到日志当中。
反例:
try {
double b = 1/0;
} catch(ArithmeticException e) {
log.error("处理失败,原因:",e.getMessage());
}
这个例子中使用e.getMessage()方法返回异常信息。
但执行结果为:
doSomething处理失败,原因:
这种情况异常信息根本没有打印出来。
我们应该把异常信息和堆栈都打印出来。
正例:
try {
double b = 1/0;
} catch(ArithmeticException e) {
log.error("处理失败,原因:",e);
}
执行结果:
doSomething处理失败,原因:
java.lang.ArithmeticException: / by zero
at cn.net.susan.service.Test.main(Test.java:16)
将具体的异常,出现问题的代码和具体行数都打印出来。
9 别捕获了异常又马上抛出
有时候,我们为了记录日志,可能会对异常进行捕获,然后又抛出。
反例:
try {
doSomething();
} catch(ArithmeticException e) {
log.error("doSomething处理失败,原因:",e)
throw e;
}
在调用doSomething方法时,如果出现了ArithmeticException异常,则先使用catch捕获,记录到日志中,然后使用throw关键抛出这个异常。
这个骚操作纯属是为了记录日志。
但最后发现日志记录两次。
因为在后续的处理中,可能会将这个ArithmeticException异常又记录一次。
这样就会导致日志重复记录了。
10 优先使用标准异常
在Java中已经定义了许多比较常用的标准异常,比如下面这张图中列出的这些异常:
反例:
public void checkValue(int value) {
if (value < 0) {
throw new MyIllegalArgumentException("值不能为负");
}
}
自定义了一个异常表示参数错误。
其实,我们可以直接复用已有的标准异常。
正例:
public void checkValue(int value) {
if (value < 0) {
throw new IllegalArgumentException("值不能为负");
}
}
11 对异常进行文档说明
我们在写代码的过程中,有一个好习惯是给方法、参数和返回值,增加文档说明。
反例:
/*
* 处理用户数据
* @param value 用户输入参数
* @return 值
*/
public int doSomething(String value)
throws BusinessException {
//业务逻辑
return 1;
}
这个doSomething方法,把方法、参数、返回值都加了文档说明,但异常没有加。
正解:
/*
* 处理用户数据
* @param value 用户输入参数
* @return 值
* @throws BusinessException 业务异常
*/
public int doSomething(String value)
throws BusinessException {
//业务逻辑
return 1;
}
抛出的异常,也需要增加文档说明。
12 别用异常控制程序的流程
我们有时候,在程序中使用异常来控制了程序的流程,这种做法其实是不对的。
反例:
Long id = null;
try {
id = Long.parseLong(idStr);
} catch(NumberFormatException e) {
id = 1001;
}
如果用户输入的idStr是Long类型,则将它转换成Long,然后赋值给id,否则id给默认值1001。
每次都需要try/catch还是比较影响系统性能的。
正例:
Long id = checkValueType(idStr) ? Long.parseLong(idStr) : 1001;
我们增加了一个checkValueType方法,判断idStr的值,如果是Long类型,则直接转换成Long,否则给默认值1001。
13 自定义异常
如果标准异常无法满足我们的业务需求,我们可以自定义异常。
例如:
/**
* 业务异常
*
* @author 苏三
* @date 2024/1/9 下午1:12
*/
@AllArgsConstructor
@Data
public class BusinessException extends RuntimeException {
public static final long serialVersionUID = -6735897190745766939L;
/**
* 异常码
*/
private int code;
/**
* 具体异常信息
*/
private String message;
public BusinessException() {
super();
}
public BusinessException(String message) {
this.code = HttpStatus.INTERNAL_SERVER_ERROR.value();
this.message = message;
}
}
对于这种自定义的业务异常,我们可以增加code和message这两个字段,code表示异常码,而message表示具体的异常信息。
BusinessException继承了RuntimeException运行时异常,后面处理起来更加灵活。
提供了多种构造方法。
定义了一个序列化ID(serialVersionUID)。
来源:juejin.cn/post/7429267019445387276
我开发的一些开发者小工具
在 2020 年,我辞职在家,每天都有大把时间。于是,我开始开发一些与开发相关的小工具,目的是解决开发中遇到的问题,或者帮助更深入地理解某些技术概念。
每天写写小工具,时间就这样一天天过去,回想起来,这段经历其实挺有意思的。
刚开始时,这些工具的 UI 确实比较简陋。不过随着时间推移,我也在不断改进它们的外观。虽然现在看来可能还是不够精美,但已经有了很大进步。
说实话,这些工具的用户引导和文档都很少,更像是我自己的一个小天地。通过 Google Analytics 的数据,我发现有些工具的使用者可能只有我自己,比如微图床。但正因为我自己在用,即使最近添加新工具的频率减少了,我也一直在维护它们。
令我感到欣慰的是,我把其中一些工具提交到了阮一峰老师的博客,很多小工具都得到了他的推荐。这对我来说是一种莫大的鼓励。
一些与深入原理相关的工具
这些工具旨在帮助开发者更深入地理解一些基础概念和底层原理。
IEEE754 浮点数转换
这个工具可以帮助你理解 IEEE 754 标准中双精度浮点数的内部表示。它能将十进制数转换为对应的二进制表示,并清晰地展示符号位、指数位和尾数位。这对于理解计算机如何处理浮点数非常有帮助。
根据 IEEE754 标准,Infinity
的浮点数转换为:指数位全为 1,尾数位全为 0。
以下是 Infinity 的浮点数转换:
根据 IEEE754 标准,0
的浮点数转换为:符号位为 0,指数位全为 0,尾数位全为 0。
以下是 0 的浮点数转换:
UTF-8 编码转换
UTF-8 是一种可变长度的字符编码,这个工具可以帮助你理解 Unicode 字符是如何被编码成 UTF-8 的。你可以输入任何 Unicode 字符,工具会显示其 UTF-8 编码的二进制表示,让你直观地看到编码过程。
base64 编码转换
Base64 是一种常用的编码方式,特别是在处理二进制数据时。这个工具不仅可以帮助你理解 Base64 编码的原理,还提供了便捷的编码和解码功能。它对于处理需要在文本环境中传输二进制数据的场景特别有用。
文件类型检测
这个工具可以帮助你理解如何通过文件的魔数(magic number)来判断文件类型。你可以上传一个文件,工具会读取文件的二进制数据,并根据魔数判断文件类型。这在处理未知文件或验证文件类型时非常有用。
比如,JPEG
是因为它的 Magic Number 为 FF D8 FF DB
图片相关
图片处理是 Web 开发中的一个重要方面,以下是一些与图片处理相关的工具。
微图
这是一个快速的图片压缩工具,可以帮助你减小图片文件的大小,而不会显著降低图片质量。
它支持多种图片格式,并且没有文件大小或数量的限制。这个工具对于优化网站加载速度特别有帮助。
最主要的是它借助于前端实现,无需服务器成本,所以你不需要担心隐私问题。它的实现方式与 squoosh 类似,都是借助于 WebAssembly 实现。
微图床
这是一个个人图床工具,允许你将 GitHub 仓库用作个人图床。它提供了简单的上传和管理功能,让你可以方便地在文章或网页中引用图片。对于经常需要在线分享图片的开发者来说,这是一个非常实用的工具。
图片分享
这个工具可以帮助你快速生成带有文字的图片,适合用于社交媒体分享或创建简单的海报。它简化了图文组合的过程,让你无需使用复杂的图像编辑软件就能创建吸引人的图片。
图片占位符
这是一个图片占位符生成工具,可以快速创建自定义尺寸和颜色的占位图片,非常适合在开发过程中使用。它可以帮助你在实际图片还未准备好时,保持页面布局的完整性。
编码与加密
在 Web 开发中,我们经常需要处理各种编码和加密。以下是一些相关的工具:
URL 编码
这个工具可以帮助你进行 URL 编码和解码,对于处理包含特殊字符的 URL 非常有用。它可以确保你的 URL 在各种环境中都能正确传输和解析。
HTML 实体编码
HTML 实体编码工具可以帮助你将特殊字符转换为 HTML 实体,确保它们在 HTML 中正确显示。这对于防止 XSS 攻击和确保 HTML 文档的正确渲染都很重要。
哈希生成器
这个工具可以生成多种常用的哈希值,包括 MD5、SHA1、SHA256 等。它在数据完整性验证、密码存储等场景中非常有用。
颜色工具
颜色是 Web 设计中的重要元素,以下是一些与颜色相关的工具:
颜色转换
这个工具可以在 RGB、HSL、CMYK 等不同颜色模型之间进行转换。它可以帮助设计师和开发者在不同的颜色表示方法之间自如切换。
调色板生成器
这个工具可以帮助你生成颜色的色调和阴影,非常适合创建一致的颜色主题。它可以让你快速构建和谐的配色方案,提高设计效率。
对比度计算器
这个工具可以计算两种颜色之间的对比度,帮助你确保文本在背景上的可读性。它对于创建符合可访问性标准的设计非常重要。
结语
虽然有些工具可能只有我自己在用,但正是这种持续的学习和创造过程让我感到充实和快乐。
我会继续维护和改进这些工具,也欢迎大家使用并提供反馈。
来源:juejin.cn/post/7426151241470476298
35 岁时我改掉的三个习惯
大家好,我是双越老师,wangEditor 作者。
我正在开发一个 Nodejs 全栈 AIGC 知识库项目 划水AI,包括富文本编辑器,多人协同编辑,AI 写作,AI 处理文本等。有兴趣的同学可以围观项目。
开始
虽然标题是 35 岁,但其实本文 2024 年我 36 岁时写的。36 岁总结 35 岁,也没毛病。
35 岁对于我们程序员来说是一个非常敏感的年龄,但我已过 35 ,看看往后会越来越敏感,还是越来越麻木?
本文和大家分享一下我个人在业余生活中的,我觉得很有意义的事情。甚至我觉得这些是事情的意义,都大于工作的成绩。
生活是一个整体,工作只是其中一部分,只是我们养家糊口的一个手段。工作以外的其他部分应该更加重要,例如业余爱好、饮食、休息、娱乐、家庭等。
1. 戒烟
我从大学毕业开始学着吸烟,到 35 岁已经有十几年的烟龄了。虽然每天吸烟量不大,但也断不了,有瘾。
我为什么要戒烟呢?
是觉得吸烟有害健康?—— 对,吸烟肯定有害健康,但人类天生是一种及时行乐的动物,谁会在乎 20 年 30 年以后的健康呢?人类天生是一个心存侥幸的动物(赌徒性质),也不是每个吸烟者都会有 xxx 病,对吧?之前看过一个段子,说某呼吸外科医生做完,累几个小时做完一台手术,先出去吸烟休息一下。
我戒烟,单纯就是想戒。我想摆脱烟草和尼古丁的控制,而且是想了很久了,不是一时突发奇想,之前就有充分的心理准备。
还有,我想在 35+ 的年纪做一点叛逆的事情,叛逆使人年轻,叛逆使人保持活力。年轻时吸烟是叛逆,年龄大了戒烟就是叛逆。年轻时叛逆是狂,年龄大了叛逆是帅。所以,各位大龄程序员,有机会要让自己叛逆起来,做一点帅的事情。
最后,当时 2023 年夏天,我正好不忙,天天闲着,总得找点事儿干。既然工作上不忙,那就在自己身上找点有意义事情做吧 —— 外求不得而向内求,如果没法从外面挣取,那就去提高自身。
烟瘾是什么?就是尼古丁的戒断反应,没有其他理由,没人其他任何事情会让你 1-2 小时就想一次,而且持续想。
关于烟草的本质,烟草的历史,烟草的商业化推广过程,烟草的洗脑广告…… 还有很多内容可以讲,本文就不展开了,有兴趣的可以留言咨询。我很早之前就看过,去学习这些,并且接受这些,才能更好的帮助戒烟。
所以,就这么戒了,到写作本文的时候,正好是戒了一年。我觉得这是我去年做过的最有价值的事情,比我工作挣的钱都有价值。
2. 戒酒
之前我是比较喜欢喝酒的,喜欢一帮人聚餐那种兴奋的状态。但后来喝多了就肚子难受,一躺下就想吐,于是决定不喝了。
有些人说:你可以少喝点。但在中国北方的酒桌上,只有 0 和 1 ,没有中间态。只要你喝了,一开始朋友劝你多喝点,再喝多一点就不用别人劝了,自己就开始主动找酒瓶子了。
我不懂酒,没喝过啥好酒,很少喝红酒。就日常喝的白酒和啤酒而言,我觉得都不好喝。
白酒,度数高,辣(尤其酱味的),容易醉。全世界就中国及其周边人喝白酒,国内几千的白酒没有国际市场。而且单就中国而言,白酒蒸馏技术几百年了,也只有最近这不到 100 年喝白酒。《红楼梦》上层人不喝白酒,《孔乙己》下层人也不喝白酒。
现在喝白酒的人,有两类:1. 被酒桌文化感染而顺从的人; 2. 有酒瘾想快速体验酒精的人。
啤酒,要分好多种,咱日常喝的瓶装的、桶装的,都可以统称为工业啤酒,像水一样,轻薄寡淡 —— 但它有酒精啊!
那种全麦啤酒(忘记名字了,不常喝)还是很好喝的,但价格较高,自己买点喝还行,聚餐喝那个太贵了(普通饭店也没有卖的),很少喝。
我身边也有一些朋友,每周都喝好几次,大部分是为了工作,拉拢客户关系。我觉得我还是比较幸运的,写写代码,改改 bug ,也不用太考虑人际关系。程序员的为数不多的好处。
3. 不看和自己无关的事情
我从不刷抖音(虽然我会发布一些抖音视频),但我之前很喜欢看今日头条 app ,每天闲了就刷一下,吃饭也看,睡前也看。
但是我都看了些啥呢?有一些是娱乐性的小视频,搞笑的,猎奇的,做饭吃饭的,我觉得这些很好,提供情绪价值。
其他的,什么俄 x 战争,什么国外 xxx 冲突,什么体育明星谁比谁厉害,什么传统武术,什么中医 …… 还有这些的评论,各路网友互怼。有时候看的都让人很带情绪,感觉有些人是不是傻,这么简单的道理看不明白?有些人是不是坏,不看事实,只看立场?
这些不仅不能提供情绪价值,反而会增加你的负面情绪。而且,根据《乌合之众》大众心理学研究,你只要参与了其中,你参与了互怼,你就会成为他们其中的一员,也变成傻子或坏人。围观,也是一种参与,你的心里会支持某一方。
更关键的是,这些事情我自己有关系吗?或者我的表态能决定这件事儿的走向吗?哪怕投一票决定一点点呢 —— 答案是显然的,要么没有任何关系,要么自己瞎操心。
所以,我卸载了今日头条 app ,不看了,眼不见心不烦,这些事情我不知道,也不影响我个人工作生活。从此以后,我觉得我的世界瞬间清净了,至少不会被那些负面情绪所打扰。
另外,我睡前也不看手机了,把手机扔在书房充电,直接睡觉。如果偶尔失眠或想事情,那就想,也不用非得拿着手机想,对吧。
总结
35 岁是一个里程碑和转折点,工作上如此,生活中也是如此。程序员是一个相对来说比较“单纯”的群体,我觉得更应该关注个人生活中的成长,共勉,加油~
来源:juejin.cn/post/7417630844100247590
尤雨溪成立VoidZero,Rust要一统JavaScript工具链?
尤雨溪在Vite Conf 2024
上宣布成立公司Void Zero,目前已经完成$460万种子轮融资,由Accel
领头,并且有Amplify
以及在dev tools领域有丰富经验的创始人参与。 主要目标是搭建下一代JavaScript
工具链,实现一套工具覆盖从源码到最终产物的中间过程,例如semantic analysis、transformer、linter、formatter、minifier、boundler等。
好的工具链不外乎快
、好用
, 本文将结合尤雨溪在Vite Conf 2024
上分享的内容来介绍什么是下一代JavaScript工具链
, 以及快
、好用
体现在哪些方面, 最后再上手试验下相关工具看是否真的快
。
Vite工具现状
相信现在大多数前端开发人员的构建工具首选一定是Vite
,Vite确实易上手并且快,涵盖了Vue、React、Preact等主流前端框架,也支持TypeScript、Nuxt等。Vite仅需简单的几个指令即可运行起项目:
// 运行create指令,选择前端框架和语言,例如Vue、TypeScript
npm create vite@latest
// Done. Now run:
cd vite-project
npm install
npm run dev
Vite为什么快?
有对比才能体现快,几年前构建前端项目还是使用webpack、Rollup、Parcel等工具,当项目代码指数级增长,这些工具的性能瓶颈愈发明显,动不动需要几分钟才能启动dev server,即使是模块热更新(HMR),文件修改后也需要几秒钟才能反馈到浏览器,这严重影响开发者工作幸福指数
。
浏览器的快速发展造就了Vite
,Vite的优势在两个方面:首先是支持了Native ES Modules,其次是build过程接入了编译型语言(如go、rust)开发的工具。这些优势体现在服务器启动和热更新两个阶段:
- 服务器启动: Vite将应用中的模块区分为依赖、源码两种,改进了开发服务器启动时间。
- 依赖:开发时不会变动的纯JavaScript,或者是较大的依赖(上百个模块的组件库),这些代码的处理代价比较高,Vite会使用
esbuild预构建
这些依赖,由于esbuild使用Go编写,因此比以JavaScript编写的打包器预构建快10-100倍。 - 源码:对于Vue、JSX等频繁变动的代码文件,Vite以原生ESM方式提供源码,让浏览器接管了打包程序的部分工作,Vite只需要在浏览器请求源码时进行转换并安需提供,也就是需安需导入、安需加载。
- 依赖:开发时不会变动的纯JavaScript,或者是较大的依赖(上百个模块的组件库),这些代码的处理代价比较高,Vite会使用
- 热更新(HMR)
在Vite中,HMR是在原生ESM上执行的。当编辑一个文件时,Vite只需要精确地使已编辑Module与其最近的HMR边界之间的链失活,使得无论应用大小如何,HMR能保持快速更新。
Vite同时利用HTTP头来加速整个页面的重新加载:源码模块请求根据
304 Not Modified
协商缓存,而预构建的依赖模块请求则通过Cache-Control:max-age=31536000,immutable
进行强缓存,因此一旦被缓存将不需要再次请求。
Vite也有缺陷
Vite当前架构底层依赖于esbuild
、Rollup
、SWC
,三者的作用如下:
- esbuild: Vite使用esbuild执行依赖项预构建,转化TypeScript、JSX,并且作为生成环境构建的默认minifier。
- Rollup: Rollup直接基于ES6模块格式,因此能够实现除屑优化(Tree Shaking),然后基于插件生态来支持打包过程的扩展。Vite基于Rollup的插件模板实现插件生态,构建生产环境的bundling chunk和静态资源。
- SWC: SWC使用Rust语言实现,号称
super-fast
JavaScript编译器,能够将TypeScript、JSX编译为浏览器支持的JavaScript,编译速度比babel快20倍。Vite主要使用SWC来打包React代码以及实现React代码的HMR。
Vite充分利用esbuild、Rollup、SWC各自的优势来组合成一套打包工具链,虽然对使用者来说是无感的,但对于Vite内部,三套打包框架组合在一起本身就显得比较臃肿。
接下来我们就分析下这一套组合拳会有哪些缺陷:
- 两套bundling
虽然esbuild构建非常快,但它的tree shaking以及代码切分不像rollup的配置化那样灵活,插件系统设计的也不尽如人意,因此Vite仅在DEV环境使用esbuild预构建依赖项。rollup正好拟补了ebuild的缺点,比较好的chunck control,以及易配置的tree shaking,因此适合在生成环境打包代码。
- 生产环境构建速度慢
由于Rollup基于JavaScript实现,虽然比Webpack快很多,但相比于native工具链,速度就相形见绌了。
- 比较大的二进制包
SWC的二进制包有多大?在Mac系统下,达到37MB,比Vite和其依赖项文件总和大了2倍。
- SWC虽然快,但缺少bundler能力
SWC有比较完善的transform和minifier,但没有提供可用的bundler,这也就说明了SWC不能直接作为打包工具。
- 不一致的bundler行为
DEV环境使用esbuild预构建依赖项,而PROD环境使用rollup构建包,在包含ESM、CJS多模块形式场景下,esbuild和rullup的构建行为存在差异,导致一些仅在线上出现的问题。
- 低效率的构建管道
由于混合了JavaScript、Go、Rust三种程序,同一代码片段可能会在不同进程重复执行AST、transform、serialize,并将结果在不同进程间传递。另一方面,在进程间传递大量的代码块本身也会有比较大的开销,特别是传递source map此类文件时开销更大。
总结这些问题,就三点:碎片化、不兼容、低效率。 为了解决这些种种问题,统一的打包工具迫在眉睫,这也是尤雨溪提出Rolldown
的主要原因。
基于Rust的下一代工具链
VoidZero
提出的下一代工具链是什么?下图为 VoieZero
规划蓝图,不管是Vue、React、Nuxt还是其他前端框架,一个Vite
统统给你搞定,测试框架仅需Vitest
即可。Vite
底层依赖Rolldown
打包器,而打包过程完全交由工具链Oxc
负责。实际干活的Rolldown
和Oxc
都基于Rust实现,因此够快。
Oxc
、Rolldown
离正式使用还有一段距离,预计是2025年初投入使用,但这也不妨碍我们先了解下这两个工具让人惊掉下巴的牛,毕竟值460万美金。
Oxc
Oxc
作为统一的语言工具链,将提供包含代码检查Linter
、代码格式化Formatter
、代码打包的组合式NPM包或者Rust包。代码打包过程分为Transformer
、Minifier
、Resolver
、Parser
、Semantic Analysis
。
Oxc
官网地址: oxc.rs/ , 目前已经发布了oxlint v0.9.9
、oxc-transform alpha 版本
。
oxlint v0.9.9
:检查速度比eslint快50-100倍。oxlint已经在Shopify
投入使用,之前使用eslint检查代码需要75分钟/CI,而使用oxlint
仅需要10秒钟,你就说快吧!oxc-transform alpha
:转换速度是SWC的3到5倍, 并且内存占用减少20%, 更小的包体积(2MB vs SWC的37MB). 已实现的主要3个功能:
- 转换TypeScript至ESNext;
- 转换React JSX至ESNext,并支持React Refresh;
- TypeScript DTS 声明;
Oxc
目前已完成Parser
Linter
Resolver
,正在快马加鞭地完善Transformer
。
Rolldown
Rolldown
是基于Rust实现的JavaScript快速打包器,与Rollup API兼容。作为打包器应包含的功能有:
- Bundling:
- 阶段1:使用Rolldown替换esbuild,执行依赖项预生成,主要包括多entry的代码切分、cjs/esm混合打包、基础插件支持;
- 阶段2:Rolldown能够支持生成环境构建,包括命令行和配置文件支持、Treeshaking、Source map、Rollup插件兼容等;
- Transform: 使用
oxc
代替esbuild的Transform,尽可能使用同一套AST
。核心功能模块Typescript/JSX转换、代码缩减(minification)以及语法降级; - 与Vite深度集成: 替换esbuild和rollup,Vite内部核心插件使用Rust实现,提升构建效率。
Rolldown
在性能方面表现如何?官方给出了两个测试。
测试一:打包数量为19k的模块文件,其中10k为React JSX组件,9k为图标js文件。不同打包框架的耗时如下:
- rolldown: 0.63s
- rolldown-vite: 1.49s
- esbuild: 1.23s
- fram: 2.08s
- rsbuild: 3.14s
rolldown打包速度比esbuild快了近2倍。
测试二:打包Vue Core代码,基于TypeScript的多包仓库,包含11个包/62个dist bundles,耗时如下:
Vue版本 | 构建框架 | 构建耗时 |
---|---|---|
Vue3.2 | Rollup + rullup-plugin-typescript2+ terser tsx | 114s |
Vue3.5(main branch) | Rollup + rollup-plugin-esbuild + swc minify tsc | 8.5s |
Vue3.5(rolldown branch) | Rolldown(tranform+minify) + oxc-transform | 1.11s |
基于
rolldown
的Vue3.5源代码比Rollup
构建快了近8倍。
单从测试数据来看,基于Rust开发的rolldown
,打包速度确实带来惊人的提升。以下为下一代Vite的架构概览,预计2025年初发布。
总结
VoidZero
宣称的下一代JavaScript工具链,价值460万美金,其商业价值可见一斑,对于研发个体来说没有明显的感受,但对于大型企业来说,VoidZero
能实打实的为企业节省每年几百万的CI构建成本。
VoidZero
将清一色的使用Rust来搭建底层构建逻辑,如果能够成型,也证明了Rust在前端构建领域的地位。这也让我们反思,借助于Rust独特的性能和安全性优势,它还能够为前端带来哪些价值?例如WASM支持,基于Tauri、Electon.rs框架的桌面应用,支持Flutter和Dart语言的移动端应用。
究竟VoidZero
会为前端领域带来怎样的变革,Vite能不能一统JavaScript工具链,让我们拭目以待吧。
我是
前端下饭菜
,原创不易,各位看官动动手,帮忙关注、点赞、收藏、评轮!
来源:juejin.cn/post/7422404598360948748