注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

12306全球最大票务系统与Gemfire介绍

全球最大票务系统 自2019年12月12日发售春运首日车票,截至2020年1月9日,12306全渠道共发售车票4.12亿张,日均售票能力达到了2000万张,平均一年售出30亿张火车票,也就是说12306已经发展成全球交易量最大的实时票务系统。 12306发布数...
继续阅读 »

全球最大票务系统


自2019年12月12日发售春运首日车票,截至2020年1月9日,12306全渠道共发售车票4.12亿张,日均售票能力达到了2000万张,平均一年售出30亿张火车票,也就是说12306已经发展成全球交易量最大的实时票务系统。


12306发布数据显示,2020年春运期间,40天的春运期间,12306最高峰日网站点击量为1495亿次,这相当于每个中国人一天在12306上点击了100次,平均每秒点击量为170多万次。而全球访问量最大的搜索引擎网站, 谷歌日访问量也不过是56亿次,一个12306的零头。 再看一下大家习惯性做对比的淘宝,2019年双十一当天,淘宝的日活跃用户为4.76亿,相当于每个人也在淘宝上点击300多次,才能赶上12306的峰值点击量。


上亿人口,40天时间,30亿次出行,12306之前,全球没有任何一家公司和产品接手过类似的任务。这个网站是在数亿人苛刻的目光中,做一件史无前例却又必须成功的事情。


历史发展


10年前铁道部顶着重重压力决心要解决买车票这个全民难题,2010年春运首日12306网站开通并试运行,2011年12月23日网站正式上线,铁道部兑现了让网络售票覆盖所有车次的承诺,不料上线第一天,全民蜂拥而入,流量暴增,网站宕机,除此之外,支付流程繁琐支付渠道单一,各种问题不断涌现,宕机可能会迟到,但永远不会缺席,12306上线的第二年,网站仍然难以支撑春运的巨大流量,很多人因为网站的各种问题导致抢票失败,甚至耽误了去线下买票的最佳时机,铁道部马不解鞍听着批评,一次又一次给12306改版升级,这个出生的婴儿几乎是在骂声中长大的。2012年9月,中秋国庆双节来临之前,12306又一次全站崩溃,本来大家习以为常的操作,却被另一个消息彻底出炉,这次崩溃之前,铁道部曾花了3.3亿对系统进行升级,中标的不是IBM惠普EMC等大牌厂商,而是拥有国字号背景的太极股份和同方股份,铁道部解释说3.3亿已经是最低价了,但没人能听进去,大家只关心他长成了什么样,没人关心他累不累,从此之后,铁道部就很少再发声明了。


2013年左右,各种互联网公司表示我行我上,开发了各种抢票网站插件。当时360浏览器靠免费抢票创下国内浏览器使用率的最高纪录,百度猎豹搜狗UC也纷纷加入,如今各类生活服务APP,管他干啥的,都得植入购票抢票功能和服务,12306就这样被抢票软件围捕了。不同的是,过去抢票是免费,现在由命运馈赠的火车票,都在明面上标好了价格,比如抢票助力包,一般花钱买10元5份,也可以邀请好友砍一刀,抢票速度上,分为低快高级光速VIP等等速度,等级越高就越考验钱包。


2017年12306上线了选座和接续换乘功能,从此爱人可以自由抢靠窗座,而且夹在两人之间坐立不安,换乘购票也变得简单。2019年上线官方捡漏神器候补购票功能,可以代替科技黄牛,自动免费为旅客购买退票余票。......


阿里云当时主要是给他们提供虚拟机服务,主要是做IaaS这一层,就是基础设施服务这一层,2012年熟悉阿里云历史的应该都知道,那个时候阿里云其实还是很小的一个厂商,所以不要盲目夸大阿里云在里面起的作用。


技术难点


1、巨大流量,高请求高并发。


2、抢票流量。每天放出无数个爬虫机器人,模拟真人登陆12306,不间断的刷新网站余票,这会滋生很多的灰色流量,也会给12306本身的话造成非常大的压力。


3、动态库存。电商的任务是购物结算,库存是唯一且稳定的,而12306每卖出一张车票,不仅要减少首末站的库存,还要同时减少一趟列车所有过路站的。



以北京西到深圳福田的G335次高铁为例,表面上看起来中间有16个车站及16个SKU,但实际上不同的起始站都会产生新的SKU。我们将所有起始和终点的可能性相加,就是16+15+14一直加到一,一共136个SKU,而每种票对应三种座位,所以一共是408个商品。然后更复杂的是用户每买一张票会影响其他商品的库存,假如用户买了一张北京西的高碑店东的票,那北京始发的16个SKU库存都要减一,但是它并不影响非北京始发车票的库存,
更关键的是这些SKU间有的互斥,有的不互斥,优先卖长的还是优先卖短程的呢,每一次火车票的出售都会引发连锁变化,让计算量大大增加,如果再叠加当前的选座功能,计算数量可能还要再翻倍,而这些计算数据需要在大量购票者抢票的数秒,甚至数毫秒内完成,难度可想而知有多多大。



4、随机性。你永远都不知道哪一个人会在哪一天,去到哪一个地点,而双十一的预售和发货,其实已经提前准备了一个月,甚至几个月,并不是集中在双十一那天爆发的那一天。所以必须要有必须要有动态扩容的能力。


读扩散和写扩散


上面说的动态库存,就比如 A -> B -> C -> D 共 4 个车站,假如乘客买了 B -> C 的车票,那么同时会影响到 A->C,A->D,B->C,B->D,涉及了多个车站的排列组合,这里计算是比较耗费性能的。


那么这里就涉及到了 “读扩散” 和 “写扩散” 的问题,在 12 年的时候,12306 使用的就是读扩散,也就是在扣减余票库存的时候,直接扣减对应车站,而在查询的时候,进行动态计算。而写扩散就是在写的时候,就动态计算每个车站应该扣除多少余票库存,在查询的时候直接查即可。


12306本身他其实是读的流量远远大于写的流量,我个人是认为写扩散其实会更好一点。


Pivotal Gemfire


Redis 在互联网公司中使用的是比较多的,而在银行、12306 很多实时交易的系统中,很多采用 Pivotal Gemfire作为解决方案。Redis 是开源的缓存解决方案,而 Pivotal Gemfire 是商用的,我们在互联网项目中为什么使用 Redis 比较多呢,就是因为 Redis 是开源的,不要钱,开源对应的也就是稳定性不是那么的强,并且开源社区也不会给你提供解决方案,毕竟你是白嫖的,而在银行以及 12306 这些系统中,它们对可靠性要求非常的高,因此会选择商用的 Pivotal Gemfire,不仅性能强、高可用,而且 Gemfire 还会提供一系列的解决方案,据说做到了分布式系统中的 CAP


12306 的性能瓶颈就在于余票的查询操作上,上边已经说了,12306 是采用读扩散,也就是客户买票之后,扣减库存只扣减对应车站之间的余票库存,在读的时候,再来动态的计算每个站点应该有多少余票,因此读性能是 12306 的性能瓶颈


当时 12306 也尝试了许多其他的解决方案,比如 cassandra 和 mamcached,都扛不住查询的流量,而使用 Gemfire 之后扛住了流量,因此就使用了 Gemfire。2012年6月一期先改造12306的主要瓶颈——余票查询系统。 9月份完成代码改造,系统上线。2012年国庆,又是网上订票高峰期间,大家可以显著发现,可以登录12306,虽然还是很难订票,但是查询余票很快。2012年10月份,二期用GemFire改造订单查询系统(客户查询自己的订单记录)2013年春节,又是网上订票高峰期间,大家可以显著发现,可以登录12306,虽然还是很难订票,但是查询余票很快,而且查询自己的订票和下订单也很快。


技术改造之后,在只采用10几台X86服务器实现了以前数十台小型机的余票计算和查询能力,单次查询的最长时间从之前的15秒左右下降到0.2秒以下,缩短了75倍以上。 2012年春运的极端高流量并发情况下,系统几近瘫痪。而在改造之后,支持每秒上万次的并发查询,高峰期间达到2.6万个查询/秒吞吐量,整个系统效率显著提高;订单查询系统改造,在改造之前的系统运行模式下,每秒只能支持300-400个查询/秒的吞吐量,高流量的并发查询只能通过分库来实现。改造之后,可以实现高达上万个查询/秒的吞吐量,而且查询速度可以保障在20毫秒左右。新的技术架构可以按需弹性动态扩展,并发量增加时,还可以通过动态增加X86服务器来应对,保持毫秒级的响应时间。


通过云计算平台虚拟化技术,将若干X86服务器的内存集中起来,组成最高可达数十TB的内存资源池,将全部数据加载到内存中,进行内存计算。计算过程本身不需要读写磁盘,只是定期将数据同步或异步方式写到磁盘。GemFire在分布式集群中保存了多份数据,任何一台机器故障,其它机器上还有备份数据,因此通常不用担心数据丢失,而且有磁盘数据作为备份。GemFire支持把内存数据持久化到各种传统的关系数据库、Hadoop库和其它文件系统中。大家知道,当前计算架构的瓶颈在存储,处理器的速度按照摩尔定律翻番增长,而磁盘存储的速度增长很缓慢,由此造成巨大高达10万倍的差距。这样就很好理解GemFire为什么能够大幅提高系统性能了。Gemfire 的存储和计算都在一个地方,它的存储和实时计算的性能目前还没有其他中间件可以取代。


但是 Gemfire 也存在不足的地方,对于扩容的支持不太友好的,因为它里边有一个 Bucket 类似于 Topic 的概念,定好 Bucket 之后,扩容是比较难的,在 12306 中,也有过测试,需要几十个T的内存就可以将业务数据全部放到内存中来,因此直接将内存给加够,也就不需要很频繁的扩容。


12306业务解决方案


当然在优化中,我们靠改变架构加机器可以提升速度效率,剩下的也需要业务上的优化。


1、验证码。如果说是淘宝啊这种网站,他用这种验证码,用12306的验证码,可能大家都不会用了,对不对,但是12306他比较特殊,因为铁路全国就他一家,所以说他可以去做这个事情,他不用把用户体验放在第一位
。他最高的优先级是怎么把票给需要的人手上。


当然这个利益的确是比较大,所以也会采用这种人工打码的方式,可以雇一批大学生去做这个验证码识别。


2、候补。候补车票其实相当于整个系统上,它是一个异步的过程,你可以在这里排队,后面的话也没有抢到票,后面再通知你。


3、分时段售票。对于抢票来说,瞬时抢票会导致对服务器有瞬间很大的压力,因此从业务设计上来说需要将抢票的压力给分散开,比如今天才开启抢15天之后的车票。2点抢票,3点抢票等等。


总结


只有程序员才知道,一个每天完成超过1500万个订单,承受近1500亿次点击的系统到底有多难,在高峰阶段的时候,平均每秒就要承受170多万次的点击,面对铁路运输这种特殊的运算模式,也能够保证全国人民在短时间内抢到回家的票,12306就是在无数国人的苛责和质疑中,创造了一个世界的奇迹。


12306除了技术牛,还有着自己的人情关怀,系统会自动识别购票者的基本信息,如果识别出订单里有老人会优先给老人安排下铺儿童和家长会尽量安排在邻近的位置,12306 在保证所有人都能顺利抢到回家的票的同时,还在不断地增加更多的便利,不仅在乎技术问题,更在乎人情异味,12306可能还不够完美,但他一直在努力变得更好,为我们顺利回家提供保障,这是背后无数程序员日夜坚守的结果,我们也应该感谢总设计师单杏花女士,所以你可以调侃,可以批评,但不能否认12306背后所做出的所有努力!


作者:jack_xu
来源:juejin.cn/post/7381747852831653929
收起阅读 »

秒懂双亲委派机制

前言 最近有位小伙伴问了我一个问题:JDBC为什么会破坏双亲委派机制? 这个问题挺有代表性的。 双亲委派机制是Java中非常重要的类加载机制,它保证了类加载的完整性和安全性,避免了类的重复加载。 这篇文章就跟大家一起聊聊,Java中类加载的双亲委派机制到底是怎...
继续阅读 »

前言


最近有位小伙伴问了我一个问题:JDBC为什么会破坏双亲委派机制?


这个问题挺有代表性的。


双亲委派机制是Java中非常重要的类加载机制,它保证了类加载的完整性和安全性,避免了类的重复加载。


这篇文章就跟大家一起聊聊,Java中类加载的双亲委派机制到底是怎么回事,有哪些破坏双亲委派机制的案例,为什么要破坏双亲委派机制,希望对你会有所帮助。


1 为什么要双亲委派机制?


我们的Java在运行之前,首先需要把Java代码转换成字节码,即class文件。


然后JVM需要把字节码通过一定的方式加载到内存中的运行时数据区


这种方式就是类加载器(ClassLoader)。


再通过加载、验证、准备、解析、初始化这几个步骤完成类加载过程,然后再由jvm执行引擎的解释器和JIT即时编译器去将字节码指令转换为本地机器指令进行执行。


我们在使用类加载器加载类的时候,会面临下面几个问题:



  1. 如何保证类不会被重复加载?类重复加载会出现很多问题。

  2. 类加载器是否允许用户自定义?

  3. 如果允许用户自定义,如何保证类文件的安全性?

  4. 如何保证加载的类的完整性?


为了解决上面的这一系列的问题,我们必须要引入某一套机制,这套机制就是:双亲委派机制


2 什么是双亲委派机制?


接下来,我们看看什么是双亲委派机制。


双亲委派机制的基本思想是:当一个类加载器试图加载某个类时,它会先委托给其父类加载器,如果父类加载器无法加载,再由当前类加载器自己进行加载。


这种层层委派的方式有助于保障类的唯一性,避免类的重复加载,并提高系统的安全性和稳定性。


在Java中默认的类加载器有3层:



  1. 启动类加载器(Bootstrap Class Loader):负责加载 %JAVA_HOME%/jre/lib 目录下的核心Java类库,比如:rt.jar、charsets.jar等。它是最顶层的类加载器,通常由C++编写。

  2. 扩展类加载器(Extension Class Loader):负责加载Java的扩展库,一般位于/lib/ext目录下。

  3. 应用程序类加载器(Application Class Loader):也称为系统类加载器,负责加载用户类路径(ClassPath)下的应用程序类。


用一张图梳理一下,双亲委派机制中的3种类加载器的层次关系:图片


但这样不够灵活,用户没法控制,加载自己想要的一些类。


于是,Java中引入了自定义类加载器。


创建一个新的类并继承ClassLoader类,然后重写findClass方法。


该方法主要是实现从那个路径读取 ar包或者.class文件,将读取到的文件用字节数组来存储,然后可以使用父类的defineClass来转换成字节码。


如果想破坏双亲委派的话,就重写loadClass方法,否则不用重写。


类加载器的层次关系改成:图片


双亲委派机制流程图如下:图片


具体流程大概是这样的:



  1. 需要加载某个类时,先检查自定义类加载器是否加载过,如果已经加载过,则直接返回。

  2. 如果自定义类加载器没有加载过,则检查应用程序类加载器是否加载过,如果已经加载过,则直接返回。

  3. 如果应用程序类加载器没有加载过,则检查扩展类加载器是否加载过,如果已经加载过,则直接返回。

  4. 如果扩展类加载器没有加载过,则检查启动类加载器是否加载过,如果已经加载过,则直接返回。

  5. 如果启动类加载器没有加载过,则判断当前类加载器能否加载这个类,如果能加载,则加载该类,然后返回。

  6. 如果启动类加载器不能加载该类,则交给扩展类加载器。扩展类加载器判断能否加载这个类,如果能加载,则加载该类,然后返回。

  7. 如果扩展类加载器不能加载该类,则交给应用程序类加载器。应用程序类加载器判断能否加载这个类,如果能加载,则加载该类,然后返回。

  8. 如果应用程序类加载器不能加载该类,则交给自定义类加载器。自定义类加载器判断能否加载这个类,如果能加载,则加载该类,然后返回。

  9. 如果自定义类加载器,也无法加载这个类,则直接抛ClassNotFoundException异常。


这样做的好处是:



  1. 保证类不会重复加载。加载类的过程中,会向上问一下是否加载过,如果已经加载了,则不会再加载,这样可以保证一个类只会被加载一次。

  2. 保证类的安全性。核心的类已经被启动类加载器加载了,后面即使有人篡改了该类,也不会再加载了,防止了一些有危害的代码的植入。


3 破坏双亲委派机制的场景


既然Java中引入了双亲委派机制,为什么要破坏它呢?


答:因为它有一些缺点。


下面给大家列举一下,破坏双亲委派机制最常见的场景。


3.1 JNDI


JNDI是Java中的标准服务,它的代码由启动类加载器去加载。


但JNDI要对资源进行集中管理和查找,它需要调用由独立厂商在应用程序的ClassPath下的实现了JNDI接口的代码,但启动类加载器不可能“认识”这些外部代码。


为了解决这个问题,Java后来引入了线程上下文类加载器(Thread Context ClassLoader)。


这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置。


如果创建线程时没有设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。


有了线程上下文加载器,JNDI服务就可以使用它去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这样就打破了双亲委派机制。


3.2 JDBC


原生的JDBC中Driver驱动本身只是一个接口,并没有具体的实现,具体的实现是由不同数据库类型去实现的。


例如,MySQL的mysql-connector.jar中的Driver类具体实现的。


原生的JDBC中的类是放在rt.jar包,是由启动类加载器进行类加载的。


在JDBC中需要动态去加载不同数据库类型的Driver实现类,而mysql-connector.jar中的Driver实现类是用户自己写的代码,启动类加载器肯定是不能加载的,那就需要由应用程序启动类去进行类加载。


为了解决这个问题,也可以使用线程上下文类加载器(Thread Context ClassLoader)。


3.3  Tomcat容器


Tomcat是Servlet容器,它负责加载Servlet相关的jar包。


此外,Tomcat本身也是Java程序,也需要加载自身的类和一些依赖jar包。


这样就会带来下面的问题:



  1. 一个Tomcat容器下面,可以部署多个基于Servlet的Web应用,但如果这些Web应用下有同名的Servlet类,又不能产生冲突,需要相互独立加载和运行才行。

  2. 但如果多个Web应用,使用了相同的依赖,比如:SpringBoot、Mybatis等。这些依赖包所涉及的文件非常多,如果全部都独立,可能会导致JVM内存不足。也就是说,有些公共的依赖包,最好能够只加载一次。

  3. 我们还需要将Tomcat本身的类,跟Web应用的类隔离开。


这些原因导致,Tomcat没有办法使用传统的双亲委派机制加载类了。


那么,Tomcat加载类的机制是怎么样的?


图片



  • CommonClassLoader:是Tomcat最基本的类加载器,它加载的类可以被Tomcat容器和Web应用访问。

  • CatalinaClassLoader:是Tomcat容器私有的类加载器,加载类对于Web应用不可见。

  • SharedClassLoader:各个Web应用共享的类加载器,加载的类对于所有Web应用可见,但是对于Tomcat容器不可见。

  • WebAppClassLoader:各个Web应用私有的类加载器,加载类只对当前Web应用可见。比如不同war包应用引入了不同的Spring版本,这样能加载各自的Spring版本,相互隔离。


3.4 热部署


由于用户对程序动态性的追求,比如:代码热部署、代码热替换等功能,引入了OSGi(Open Service Gateway Initiative)。


OSGi中的每一个模块(称为Bundle)。


当程序升级或者更新时,可以只停用、重新安装然后启动程序的其中一部分,对企业来说这是一个非常诱人的功能。


OSGi的Bundle类加载器之间只有规则,没有固定的委派关系。


各个Bundle加载器是平级关系。


不是双亲委派关系。




作者:苏三说技术
来源:juejin.cn/post/7383894631312769074
收起阅读 »

ThreadLocal不香了,ScopedValue才是王道

ThreadLocal的缺点在Java中,当多个方法要共享一个变量时,我们会选择使用ThreadLocal来进行共享,比如:  以上代码将字符串“dadudu”通过设置到ThreadLocal中,从而可以做到在main()方法中赋值,在a(...
继续阅读 »

ThreadLocal的缺点

在Java中,当多个方法要共享一个变量时,我们会选择使用ThreadLocal来进行共享,比如:  以上代码将字符串“dadudu”通过设置到ThreadLocal中,从而可以做到在main()方法中赋值,在a()b()方法中获取值,从而共享值。

生命在于思考,我们来想想ThreadLocal有什么缺点:

  1. 第一个就是权限问题,也许我们只需要在main()方法中给ThreadLocal赋值,在其他方法中获取值就可以了,而上述代码中a()b()方法都有权限给ThreadLocal赋值,ThreadLocal不能做权限控制。
  2. 第二个就是内存问题,ThreadLocal需要手动强制remove,也就是在用完ThreadLocal之后,比如b()方法中,应该调用其remove()方法,但是我们很容易忘记调用remove(),从而造成内存浪费

ScopedValue

而JDK21中的新特性ScopedValue能不能解决这两个缺点呢?我们先来看一个ScopedValue的Demo: 

首先需要通过ScopedValue.newInstance()生成一个ScopedValue对象,然后通过ScopedValue.runWhere()方法给ScopedValue对象赋值,runWhere()的第三个参数是一个lambda表达式,表示作用域,比如上面代码就表示:给NAME绑定值为"dadudu",但是仅在调用a()方法时才生效,并且在执行runWhere()方法时就会执行lambda表达式。

比如上面代码的输出结果为: 

从结果可以看出在执行runWhere()时会执行a()a()方法中执行b()b()执行完之后返回到main()方法执行runWhere()之后的代码,所以,在a()方法和b()方法中可以拿到ScopedValue对象所设置的值,但是在main()方法中是拿不到的(报错了),b()方法之所以能够拿到,是因为属于a()方法调用栈中。

所以在给ScopedValue绑定值时都需要指定一个方法,这个方法就是所绑定值的作用域,只有在这个作用域中的方法才能拿到所绑定的值。

ScopedValue也支持在某个方法中重新开启新的作用域并绑定值,比如: 

以上代码中,在a()方法中重新给ScopedValue绑定了一个新值“xiaodudu”,并指定了作用域为c()方法,所以c()方法中拿到的值为“xiaodudu”,但是b()中仍然拿到的是“dadudu”,并不会受到影响,以上代码的输出结果为: 

甚至如果把代码改成: 

以上代码在a()方法中有两处调用了c()方法,我想大家能思考出c1c2输出结果分别是什么: 

所以,从以上分析可以看到,ScopedValue有一定的权限控制:就算在同一个线程中也不能任意修改ScopedValue的值,就算修改了对当前作用域(方法)也是无效的。另外ScopedValue也不需要手动remove,关于这块就需要分析它的实现原理了。

实现原理

大家先看下面代码,注意看下注释: 

执行main()方法时,main线程执行过程中会执行runWhere()方法三次,而每次执行runWhere()时都会生成一个Snapshot对象,Snapshot对象中记录了所绑定的值,而Snapshot对象有一个prev属性指向上一次所生成的Snapshot对象,并且在Thread类中新增了一个属性scopedValueBindings,专门用来记录当前线程对应的Snapshot对象。

比如在执行main()方法中的runWhere()时:

  1. 会先生成Snapshot对象1,其prev为null,并将Snapshot对象1赋值给当前线程的scopedValueBindings属性,然后执行a()方法
  2. 在执行a()方法中的runWhere()时,会先生成Snapshot对象2,其prevSnapshot对象1,并将Snapshot对象2赋值给当前线程的scopedValueBindings属性,使得在执行b()方法时能从当前线程拿到Snapshot对象2从而拿到所绑定的值,runWhere()内部在执行完b()方法后会取prev,从而取出Snapshot对象1,并将Snapshot对象1赋值给当前线程的scopedValueBindings属性,然后继续执行a()方法后续的逻辑,如果后续逻辑调用了get()方法,则会取当前线程的scopedValueBindings属性拿到Snapshot对象1,从Snapshot对象1中拿到所绑定的值就可以了,而对于Snapshot对象2由于没有引用则会被垃圾回收掉。

所以,在用ScopedValue时不需要手动remove。

好了,关于ScopedValue就介绍到这啦,下次继续分享JDK21新特性,欢迎大家关注我的公众号:Hoeller,第一时间接收我的原创技术文章,谢谢大家的阅读。


作者:IT周瑜
来源:juejin.cn/post/7287241480770928655
收起阅读 »

开发经理:谁在项目里面用Stream. paraller()直接gun

大家好,我是小玺,今天给大家分享一下项目中关于Stream.parallel() 碰到的坑。 Stream.parallel() 是Java 8及以上版本引入的Stream API中的一个方法,它用于将一个串行流转换为并行流。并行流可以在多个处理器上同时执行操...
继续阅读 »

大家好,我是小玺,今天给大家分享一下项目中关于Stream.parallel() 碰到的坑。


Stream.parallel() 是Java 8及以上版本引入的Stream API中的一个方法,它用于将一个串行流转换为并行流。并行流可以在多个处理器上同时执行操作,从而显著提高对大量数据进行处理的性能。


踩坑日记


某个大型项目,晚上十一点多有个用户对小部分数据进行某项批量操作后,接口大半天没有反应最后返回超时报错,但是过了一段时间后,出现了部分数据被修改成功,部分数据则没有反应。用户立马跳起来,打电话投诉到公司领导层,于是乎领导层对上至开发经理和PM,下至小开发进行会议批斗,要求马上排查并解决问题,毕竟项目这么大,当初也是要求测试做过压测的,怎么出现这么大的生产事故。


1712648893920.png


于是乎开发和实施运维分头行事,开发人员排查问题,实施人员先把问题数据维护好,不能应该用户使用。一群开发也是很疑惑,开发和测试环境都没法复现出问题,简单过一下代码也没看出个所以然,由于时间问题,不得不呼叫一手开发经理帮忙看看,开发经理后台接口看完Stream.parallel()进行的操作代码立马就炸了,git看了下提交人【会笑】,把这个开发从头到脚喷了一遍。


在对会笑单独进行了长达半小时的“耐心教育”后(ps:问题安排另一名开发同事修复),开发经理给团队的所有后端开发人员又都教育了一遍。原来会笑在用并行流的时候,没有考虑线程池配置和事务问题,把一堆数据进行了批量更新,Stream.parallel()并行流默认使用的是ForkJoinPool.commonPool()作为线程池,该线程池默认最大线程数就是CPU核数。


1712648957687.png


雀食对于一些初中级开发来说,开发过程中往往喜欢用一些比较新颖的写法来实现但是对新语法又是一知半解的,Stream.parallel()作为Java的新特性,也就成了其中一个反面教材。如果操作数据量不大的情况,其实没有必要用到Stream.parallel(),效率反而会变差。


注意事项



  1. 线程安全:并行流并不能保证线程安全性,因此,如果流中的元素是共享资源或操作本身不是线程安全的,你需要确保正确同步或使用线程安全的数据结构。

  2. 数据分区:Java的并行流机制会自动对数据进行分区,但在某些情况下,数据分区的开销可能大于并行带来的收益,特别是对于小规模数据集。

  3. 效率考量:并非所有的流操作都能从并行化中受益,有些操作(如短流操作或依赖于顺序的操作)并行执行反而可能导致性能下降。而且,过多的上下文切换也可能抵消并行带来的优势。

  4. 资源消耗:并行流默认使用的线程池大小可能与机器的实际物理核心数相适应,但也可能与其他并发任务争夺系统资源。

  5. 结果一致性:并行流并不保证执行的顺序性,也就是说,如果流操作的结果依赖于元素的处理顺序,则不应该使用并行流。

  6. 事务处理:在涉及到事务操作时,通常需要避免在并行流中直接处理,如上述例子所示,应当将事务边界放在单独的服务方法内,确保每个线程内的事务独立完成。


Tips:线程数可以通JVM启动参数-Djava.util.concurrent.ForkJoinPool.common.parallelism=20进行修改


作者:小玺
来源:juejin.cn/post/7355431482687864883
收起阅读 »

记一次难忘的json反序列化问题排查经历

前言 最近我在做知识星球中的商品秒杀系统,昨天遇到了一个诡异的json反序列化问题,感觉挺有意思的,现在拿出来跟大家一起分享一下,希望对你会有所帮助。 案发现场 我最近在做知识星球中的商品秒杀系统,写了一个filter,获取用户请求的header中获取JWT的...
继续阅读 »

前言


最近我在做知识星球中的商品秒杀系统,昨天遇到了一个诡异的json反序列化问题,感觉挺有意思的,现在拿出来跟大家一起分享一下,希望对你会有所帮助。


案发现场


我最近在做知识星球中的商品秒杀系统,写了一个filter,获取用户请求的header中获取JWT的token信息。


然后根据token信息,获取到用户信息。


在转发到业务接口之前,将用户信息设置到用户上下文当中。


这样接口中的业务代码,就能通过用户上下文,获取到当前登录的用户信息了。


我们的token和用户信息,为了性能考虑都保存到了Redis当中。


用户信息是一个json字符串。


当时在用户登录接口中,将用户实体,使用fastjson工具,转换成了字符串:


JSON.toJSONString(userDetails);

保存到了Redis当中。


然后在filter中,通过一定的key,获取Redis中的字符串,反序列化成用户实体。


使用的同样是fastjson工具:


JSON.parseObject(json, UserEntity.class);

但在反序列化的过程中,filter抛异常了:com.alibaba.fastjson.JSONException: illegal identifier : \pos 1, line 1, column 2{"accountNonExpired":true,"accountNonLocked":true,"authorities":[{"authority":"admin"}],"credentialsNonExpired":true,"enabled":true,"id":13,"password":"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe","roles":["admin"],"username":"admin"}


2 分析问题


我刚开始以为是json数据格式有问题。


将json字符串复制到在线json工具:http://www.sojson.com,先去掉化之后,再格式数据,发现json格式没有问题:![图片](p3-juejin.byteimg.com/tos-cn-i-k3…)


然后写了一个专门的测试类,将日志中打印的json字符串复制到json变量那里,使用JSON.parseObject方法,将json字符串转换成Map对象:


public class Test {

    public static void main(String[] args) {
        String json = "{"accountNonExpired":true,"accountNonLocked":true,"authorities":[{"authority":"admin"}],"credentialsNonExpired":true,"enabled":true,"id":13,"password":"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe","roles":["admin"],"username":"admin"}";
        Map map = JSON.parseObject(json, Map.class);
        // 输出解析后的 JSON 对象
        System.out.println(map);
    }
}

执行结果:


{password=$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe, credentialsNonExpired=true, roles=["admin"], accountNonExpired=true, id=13, authorities=[{"authority":"admin"}], enabled=true, accountNonLocked=true, username=admin}

竟然转换成功了。


这就让我有点懵逼了。。。


为什么相同的json字符串,在Test类中能够正常解析,而在filter当中却不行?


当时怕搞错了,debug了一下filter,发现获取到的json数据,跟Test类中的一模一样:图片


带着一脸的疑惑,我做了下面的测试。


8000页BAT大佬写的刷题笔记,让我offer拿到手软


莫非是反序列化工具有bug?


3 改成gson工具


我尝试了一下将json的反序列化工具改成google的gson,代码如下:


 Map map = new Gson().fromJson(userJson, Map.class);

运行之后,报了一个新的异常:com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 2 path $


这里提示json字符串中包含了:$


$是特殊字符,password是做了加密处理的,里面包含$.,这两种特殊字符。


为了快速解决问题,我先将这两个特字符替换成空字符串:


json = json.replace("$","").replace(".","");

日志中打印出的json中的password,已经不包含这两个特殊字符了:


2a10o3XfeGr0SHStAwLuJRW6ykE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe

但调整之后代码报了下面的异常:com.google.gson.JsonSyntaxException: com.google.gson.stream.MalformedJsonException: Expected name at line 1 column 2 path $.


跟刚刚有点区别,但还是有问题。


4 改成jackson工具


我又尝试了一下json的反序列化工具,改成Spring自带的的jackson工具,代码如下:


ObjectMapper objectMapper = new ObjectMapper();
try {
    Map map = objectMapper.readValue(json, Map.class);
catch (JsonProcessingException e) {
    e.printStackTrace();
}

调整之后,反序列化还是报错:com.fasterxml.jackson.core.JsonParseException: Unexpected character ('' (code 92)): was expecting double-quote to start field name


3种反序列化工具都不行,说明应该不是fastjson的bug导致的当前json字符串,反序列化失败。


到底是什么问题呢?


5 转义


之前的数据,我在仔细看了看。


里面是对双引号,是使用了转义的,具体是这样做的:"


莫非还是这个转义的问题?


其实我之前已经注意到了转义的问题,但使用Test类测试过,没有问题。


当时的代码是这样的:


public class Test {

    public static void main(String[] args) {
        String json = "{"accountNonExpired":true,"accountNonLocked":true,"authorities":[{"authority":"admin"}],"credentialsNonExpired":true,"enabled":true,"id":13,"password":"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe","roles":["admin"],"username":"admin"}";
        Map map = JSON.parseObject(json, Map.class);
        // 输出解析后的 JSON 对象
        System.out.println(map);
    }
}

里面也包含了一些转义字符。


我带着试一试的心态,接下来,打算将转义字符去掉。


看看原始的json字符串,解析有没有问题。


怎么去掉转义字符呢?


手写工具类,感觉不太好,可能会写漏一些特殊字符的场景。


8000页BAT大佬写的刷题笔记,让我offer拿到手软


我想到了org.apache.commons包下的StringEscapeUtils类,它里面的unescapeJava方法,可以轻松去掉Java代码中的转义字符。


于是,我调整了一下代码:


json = StringEscapeUtils.unescapeJava(json);
JSON.parseObject(json, UserEntity.class);

这样处理之后,发现反序列化成功了。


总结


这个问题最终发现还是转义的问题。


那么,之前Test类中json字符串,也使用了转义,为什么没有问题?


当时的代码是这样的:


public class Test {

    public static void main(String[] args) {
        String json = "{"accountNonExpired":true,"accountNonLocked":true,"authorities":[{"authority":"admin"}],"credentialsNonExpired":true,"enabled":true,"id":13,"password":"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe","roles":["admin"],"username":"admin"}";
        Map map = JSON.parseObject(json, Map.class);
        System.out.println(map);
    }
}

但在filter中的程序,在读取到这个json字符串之后,发现该字符串中包含了``转义符号,程序自动把它变成了\


调整一下Test类的main方法,改成三个斜杠的json字符串:


public static void main(String[] args) {
    String json = "{\"accountNonExpired\":true,\"accountNonLocked\":true,\"authorities\":[{\"authority\":\"admin\"}],\"credentialsNonExpired\":true,\"enabled\":true,\"id\":13,\"password\":\"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe\",\"roles\":[\"admin\"],\"username\":\"admin\"}";
    Map map = JSON.parseObject(json, Map.class);
    System.out.println(map);
}

执行结果:Exception in thread "main" com.alibaba.fastjson.JSONException: illegal identifier : \pos 1, line 1, column 2{"accountNonExpired":true,"accountNonLocked":true,"authorities":[{"authority":"admin"}],"credentialsNonExpired":true,"enabled":true,"id":13,"password":"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe","roles":["admin"],"username":"admin"}抛出了跟文章最开始一样的异常。


说明其实就是转义的问题。


之前,我将项目的日志中的json字符串,复制到idea的Test的json变量中,当时将最外层的双引号一起复制过来了,保存的是1个斜杠的数据。


这个操作把我误导了。


而后面从在线的json工具中,把相同的json字符串,复制到idea的Test的json变量中,在双引号当中粘贴数据,保存的却是3个斜杠的数据,它会自动转义。


让我意识到了问题。


好了,下次如果遇到类似的问题,可以直接使用org.apache.commons包下的StringEscapeUtils类,先去掉转义,再反序列化,这样可以快速解决问题。


此外,这次使用了3种不同的反序列化工具,也看到了其中的一些差异。


作者:苏三说技术
来源:juejin.cn/post/7385081262871003175
收起阅读 »

Flutter桌面应用开发:深入Flutter for Desktop

Flutter 是一个开源的 UI 工具包,用于构建高性能、高保真、多平台的应用程序,包括移动、Web 和桌面。 安装和环境配置 安装Prerequisites: Java Development Kit (JDK): 安装JDK 8或更高版本,因为Flutt...
继续阅读 »

Flutter 是一个开源的 UI 工具包,用于构建高性能、高保真、多平台的应用程序,包括移动、Web 和桌面。


安装和环境配置


安装Prerequisites:


Java Development Kit (JDK): 安装JDK 8或更高版本,因为Flutter要求JDK 1.8或更高。配置环境变量JAVA_HOME指向JDK的安装路径。
Flutter SDK:


下载Flutter SDK:


访问Flutter官方网站下载适用于Windows的Flutter SDK压缩包。
解压并选择一个合适的目录安装,例如 C:\src\flutter
将Flutter SDK的bin目录添加到系统PATH环境变量中。例如,添加 C:\src\flutter\bin


Git:


如果还没有安装Git,可以从Git官网下载并安装。
在安装过程中,确保勾选 "Run Git from the Windows Command Prompt" 选项。


Flutter Doctor:


打开命令提示符或PowerShell,运行 flutter doctor 命令。这将检查你的环境是否完整,并列出任何缺失的组件,如Android Studio、Android SDK等。


Android Studio (如果计划开发Android应用):


下载并安装Android Studio,它包含了Android SDK和AVD Manager。
安装后,通过Android Studio设置向导配置Android SDK和AVD。
确保在系统环境变量中配置了ANDROID_HOME指向Android SDK的路径,通常是\Sdk


iOS Development (如果计划开发iOS应用):


你需要安装Xcode和Command Line Tools,这些只适用于macOS。
在终端中运行xcode-select --install以安装必要的命令行工具。


验证安装:


运行 flutter doctor --android-licenses 并接受所有许可证(如果需要)。
再次运行 flutter doctor,确保所有必需的组件都已安装并配置正确。


开始开发:


创建你的第一个Flutter项目:flutter create my_first_app
使用IDE(如VS Code或Android Studio)打开项目,开始编写和运行代码。


基础知识


在Flutter桌面应用开发中,Dart语言是核心。基础Flutter应用展示来学习Dart语言魅力:


import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);

final String title;

@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;

void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State object, which causes it to re-build the widget.
_counter++;
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}

导入库:



  • import 'package:flutter/material.dart';: 导入Flutter的Material库,包含了许多常用的UI组件。


主入口点:


void main() => runApp(MyApp());: 应用的主入口点,启动MaterialApp。


MyApp StatelessWidget:


MyApp是一个无状态的Widget,用于配置应用的全局属性。


MyHomePage StatefulWidget:



  • MyHomePage是一个有状态的Widget,它有一个状态类_MyHomePageState,用于管理状态。

  • title参数在构造函数中传递,用于初始化AppBar的标题。


_MyHomePageState:



  • _counter变量用于存储按钮点击次数。

  • _incrementCounter方法更新状态,setState通知Flutter需要重建Widget。

  • build方法构建Widget树,根据状态_counter更新UI。


UI组件:



  • Scaffold提供基本的布局结构,包括AppBar、body和floatingActionButton。

  • FloatingActionButton是一个浮动按钮,点击时调用_incrementCounter。

  • Text组件显示文本,AppBar标题和按钮点击次数。

  • ColumnCenter用于布局管理。


Flutter应用


创建项目目录:


选择一个合适的位置创建一个新的文件夹,例如,你可以命名为my_flutter_app。


初始化Flutter项目:


打开终端或命令提示符,导航到你的项目目录,然后运行以下命令来初始化Flutter应用:


   cd my_flutter_app
flutter create .

这个命令会在当前目录下创建一个新的Flutter应用。


检查项目:


初始化完成后,你应该会看到以下文件和文件夹:



  • lib/:包含你的Dart代码,主要是main.dart文件。

  • pubspec.yaml:应用的配置文件,包括依赖项。

  • android/ios/:分别用于Android和iOS的原生项目配置。


运行应用:


为了运行应用,首先确保你的模拟器或物理设备已经连接并准备好。然后在终端中运行:


   flutter run

这将构建你的应用并启动它在默认的设备上。


编辑代码:


打开lib/main.dart文件,这是你的应用的入口点。你可以在这里修改代码以自定义你的应用。例如,你可以修改MaterialApphome属性来指定应用的初始屏幕。


热重载:


当你修改代码并保存时,可以使用flutter pub get获取新依赖,然后按r键(或在终端中输入flutter reload)进行热重载,快速查看代码更改的效果。


布局和组件


Flutter提供了丰富的Widget库来构建复杂的布局。下面是一个使用Row, Column, Expanded, 和 ListView的简单布局示例,展示如何组织UI组件。


import 'package:flutter/material.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Desktop Layout Example',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(title: Text("Desktop App Layout")),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
RaisedButton(onPressed: () {}, child: Text('Button 1')),
RaisedButton(onPressed: () {}, child: Text('Button 2')),
],
),
SizedBox(height: 20),
Expanded(
child: ListView.builder(
itemCount: 20,
itemBuilder: (context, index) {
return ListTile(title: Text('Item $index'));
},
),
),
],
),
),
);
}
}

Column和Row是基础的布局Widget,Expanded用于占据剩余空间,ListView.builder动态构建列表项,展示了如何灵活地组织UI元素。


状态管理和数据流


在Flutter中,状态管理是通过Widget树中的状态传递和更新来实现的。最基础的是使用StatefulWidgetsetState方法,但复杂应用通常会采用更高级的状态管理方案,如ProviderRiverpodBloc


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class Counter with ChangeNotifier {
int _count = 0;

int get count => _count;

void increment() {
_count++;
notifyListeners();
}
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => Counter(),
child: MaterialApp(
home: Scaffold(
body: Center(
child: Consumer(
builder: (context, counter, child) {
return Text(
'${counter.count}',
style: TextStyle(fontSize: 24),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Provider.of(context, listen: false).increment();
},
child: Icon(Icons.add),
),
),
),
);
}
}

状态管理示例引入了Provider库,ChangeNotifier用于定义状态,ChangeNotifierProvider在树中提供状态,Consumer用于消费状态并根据状态更新UI,Provider.of用于获取状态并在按钮按下时调用increment方法更新状态。这种方式解耦了状态和UI,便于维护和测试。


路由和导航


Flutter使用Navigator进行页面间的导航。


import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Navigation Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: '/',
routes: {
'/': (context) => HomePage(),
'/details': (context) => DetailsPage(),
},
);
}
}

class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home Page')),
body: Center(
child: ElevatedButton(
child: Text('Go to Details'),
onPressed: () {
Navigator.pushNamed(context, '/details');
},
),
),
);
}
}

class DetailsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Details Page')),
body: Center(child: Text('This is the details page')),
);
}
}

MaterialApproutes属性定义了应用的路由表,initialRoute指定了初始页面,Navigator.pushNamed用于在路由表中根据名称导航到新页面。这展示了如何在Flutter中实现基本的页面跳转逻辑。


响应式编程


Flutter的UI是完全响应式的,意味着当状态改变时,相关的UI部分会自动重建。使用StatefulWidgetsetState方法是最直接的实现方式。


import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CounterPage(),
);
}
}

class CounterPage extends StatefulWidget {
@override
_CounterPageState createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++;
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}

StatefulWidgetsetState的使用体现了Flutter的响应式特性。当调用_incrementCounter方法更新_counter状态时,Flutter框架会自动调用build方法,仅重绘受影响的部分,实现了高效的UI更新。这种模式确保了UI始终与最新的状态保持一致,无需手动管理UI更新逻辑。


平台交互


Flutter提供了Platform类来与原生平台进行交互。


import 'package:flutter/foundation.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Platform.isAndroid ? AndroidScreen() : DesktopScreen(),
);
}
}

class AndroidScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text('This is an Android screen');
}
}

class DesktopScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text('This is a Desktop screen');
}
}

性能优化


优化主要包括减少不必要的渲染、使用高效的Widget和数据结构、压缩资源等。例如,使用const关键字创建常量Widget以避免不必要的重建:


class MyWidget extends StatelessWidget {
const MyWidget({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Container(
color: Colors.blue,
child: const Text('Optimized Widget', style: TextStyle(fontSize: 24)),
);
}
}

调试和测试


Flutter提供了强大的调试工具,如热重载、断点、日志输出等。测试方面,可以使用test包进行单元测试和集成测试:


import 'package:flutter_test/flutter_test.dart';

void main() {
test('Counter increments correctly', () {
final counter = Counter(0);
expect(counter.value, equals(0));
counter.increment();
expect(counter.value, equals(1));
});
}

打包和发布


发布Flutter应用需要构建不同平台的特定版本。在桌面环境下,例如Windows,可以使用以下命令:


flutter build windows

这将生成一个.exe文件,可以分发给用户。确保在pubspec.yaml中配置好应用的元数据,如版本号和描述。


Flutter工作原理分析


Flutter Engine:



  • Flutter引擎是Flutter的基础,它负责渲染、事件处理、文本布局、图像解码等功能。引擎是用C++编写的,部分用Java或Objective-C/Swift实现原生平台的接口。

  • Skia是Google的2D图形库,用于绘制UI。在桌面应用中,Skia直接与操作系统交互,提供图形渲染。

  • Dart VM运行Dart代码,提供垃圾回收和即时编译(JIT)或提前编译(AOT)。


Flutter Framework:



  • Flutter框架是用Dart编写的,它定义了Widget、State和Layout等概念,以及动画、手势识别和数据绑定等机制。

  • WidgetsFlutterBinding是框架与引擎的桥梁,它实现了将Widget树转换为可绘制的命令,这些命令由引擎执行。


Widgets:



  • Flutter中的Widget是UI的构建块,它们是不可变的。StatefulWidget和State类用于管理可变状态。

  • 当状态改变时,setState方法被调用,导致Widget树重新构建,进而触发渲染。


Plugins:



  • 插件是Flutter与原生平台交互的方式,它们封装了原生API,使得Dart代码可以访问操作系统服务,如文件系统、网络、传感器等。

  • 桌面应用的插件需要针对每个目标平台(Windows、macOS、Linux)进行实现。


编译和运行流程:



  • 使用flutter build命令,Dart代码会被编译成原生代码(AOT编译),生成可执行文件。

  • 运行时,Flutter引擎加载并执行编译后的代码,同时初始化插件和设置渲染管线。


调试和热重载:



  • Flutter支持热重载,允许开发者在运行时快速更新代码,无需重新编译整个应用。

  • 调试工具如DevTools提供了对应用性能、内存、CPU使用率的监控,以及源代码级别的调试。


性能优化:



  • Flutter通过AOT编译和Dart的垃圾回收机制来提高性能。

  • 使用const关键字创建Widget可以避免不必要的重建,减少渲染开销。


作者:天涯学馆
来源:juejin.cn/post/7378015213347913791
收起阅读 »

一条SQL 最多能查询出来多少条记录?

问题 一条这样的 SQL 语句能查询出多少条记录? select * from user 表中有 100 条记录的时候能全部查询出来返回给客户端吗? 如果记录数是 1w 呢? 10w 呢? 100w 、1000w 呢? 虽然在实际业务操作中我们不会这么干,...
继续阅读 »

问题


一条这样的 SQL 语句能查询出多少条记录?


select * from user 

表中有 100 条记录的时候能全部查询出来返回给客户端吗?


如果记录数是 1w 呢? 10w 呢? 100w 、1000w 呢?


虽然在实际业务操作中我们不会这么干,尤其对于数据量大的表不会这样干,但这是个值得想一想的问题。


寻找答案


前提:以下所涉及资料全部基于 MySQL 8


max_allowed_packet


在查询资料的过程中发现了这个参数 max_allowed_packet



上图参考了 MySQL 的官方文档,根据文档我们知道:



  • MySQL 客户端 max_allowed_packet 值的默认大小为 16M(不同的客户端可能有不同的默认值,但最大不能超过 1G)

  • MySQL 服务端 max_allowed_packet 值的默认大小为 64M

  • max_allowed_packet 值最大可以设置为 1G(1024 的倍数)


然而 根据上图的文档中所述



The maximum size of one packet or any generated/intermediate string,or any parameter sent by the mysql_smt_send_long_data() C API function




  • one packet

  • generated/intermediate string

  • any parameter sent by the mysql_smt_send_long_data() C API function


这三个东东具体都是什么呢? packet 到底是结果集大小,还是网络包大小还是什么? 于是 google 了一下,搜索排名第一的是这个:



根据 “Packet Too Large” 的说明, 通信包 (communication packet) 是



  • 一个被发送到 MySQL 服务器的单个 SQL 语句

  • 或者是一个被发送到客户端的单行记录

  • 或者是一个从主服务器 (replication source server) 被发送到从属服务器 (replica) 的二进制日志事件。


1、3 点好理解,这也同时解释了,如果你发送的一条 SQL 语句特别大可能会执行不成功的原因,尤其是insert update 这种,单个 SQL 语句不是没有上限的,不过这种情况一般不是因为 SQL 语句写的太长,主要是由于某个字段的值过大,比如有 BLOB 字段。


那么第 2 点呢,单行记录,默认值是 64M,会不会太大了啊,一行记录有可能这么大的吗? 有必要设置这么大吗? 单行最大存储空间限制又是多少呢?


单行最大存储空间


MySQL 单行最大宽度是 65535 个字节,也就是 64KB 。无论是 InnoDB 引擎还是 MyISAM 引擎。



通过上图可以看到 超过 65535 不行,不过请注意其中的错误提示:“Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535” ,如果字段是变长类型的如 BLOB 和 TEXT 就不包括了,那么我们试一下用和上图一样的字段长度,只把最后一个字段的类型改成 BLOB 和 TEXT


mysql> CREATE TABLE t (a VARCHAR(10000), b VARCHAR(10000),
c VARCHAR(10000), d VARCHAR(10000), e VARCHAR(10000),
f VARCHAR(10000), g TEXT(6000)) ENGINE=InnoDB CHARACTER SET latin1;
Query OK, 0 rows affected (0.02 sec)

可见无论 是改成 BLOB 还是 TEXT 都可以成功。但这里请注意,字符集是 latin1 可以成功,如果换成 utf8mb4 或者 utf8mb3 就不行了,会报错,仍然是 :“Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535.” 为什么呢?


因为虽然不包括 TEXT 和 BLOB, 但总长度还是超了!


我们先看一下这个熟悉的 VARCHAR(255) , 你有没有想过为什么用 255,不用 256?



在 4.0 版本以下,varchar(255) 指的是 255 个字节,使用 1 个字节存储长度即可。当大于等于 256 时,要使用 2 个字节存储长度。所以定义 varchar(255) 比 varchar(256) 更好。


但是在 5.0 版本以上,varchar(255) 指的是 255 个字符,每个字符可能占用多个字节,例如使用 UTF8 编码时每个汉字占用 3 字节,使用 GBK 编码时每个汉字占 2 字节。



例子中我们用的是 MySQL8 ,由于字符集是 utf8mb3 ,存储一个字要用三个字节, 长度为 255 的话(列宽),总长度要 765 字节 ,再加上用 2 个字节存储长度,那么这个列的总长度就是 767 字节。所以用 latin1 可以成功,是因为一个字符对应一个字节,而 utf8mb3 或 utf8mb4 一个字符对应三个或四个字节,VARCHAR(10000) 就可能等于要占用 30000 多 40000 多字节,比原来大了 3、4 倍,肯定放不下了。


另外,还有一个要求,列的宽度不要超过 MySQL 页大小 (默认 16K)的一半,要比一半小一点儿。 例如,对于默认的 16KB InnoDB 页面大小,最大行大小略小于 8KB。


下面这个例子就是超过了一半,所以报错,当然解决办法也在提示中给出了。


mysql> CREATE TABLE t4 (
c1 CHAR(255),c2 CHAR(255),c3 CHAR(255),
c4 CHAR(255),c5 CHAR(255),c6 CHAR(255),
c7 CHAR(255),c8 CHAR(255),c9 CHAR(255),
c10 CHAR(255),c11 CHAR(255),c12 CHAR(255),
c13 CHAR(255),c14 CHAR(255),c15 CHAR(255),
c16 CHAR(255),c17 CHAR(255),c18 CHAR(255),
c19 CHAR(255),c20 CHAR(255),c21 CHAR(255),
c22 CHAR(255),c23 CHAR(255),c24 CHAR(255),
c25 CHAR(255),c26 CHAR(255),c27 CHAR(255),
c28 CHAR(255),c29 CHAR(255),c30 CHAR(255),
c31 CHAR(255),c32 CHAR(255),c33 CHAR(255)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET latin1;
ERROR 1118 (42000): Row size too large (> 8126). Changing some columns to TEXT or BLOB may help.
In current row format, BLOB prefix of 0 bytes is stored inline.

那么为什么是 8K,不是 7K,也不是 9K 呢? 这么设计的原因可能是:MySQL 想让一个数据页中能存放更多的数据行,至少也得要存放两行数据(16K)。否则就失去了 B+Tree 的意义。B+Tree 会退化成一个低效的链表。


你可能还会奇怪,不超过 8K ?你前面的例子明明都快 64K 也能存下,那 8K 到 64K 中间这部分怎么解释?


答:如果包含可变长度列的行超过 InnoDB 最大行大小, InnoDB 会选择可变长度列进行页外存储,直到该行适合 InnoDB ,这也就是为什么前面有超过 8K 的也能成功,那是因为用的是VARCHAR这种可变长度类型。



当你往这个数据页中写入一行数据时,即使它很大将达到了数据页的极限,但是通过行溢出机制。依然能保证你的下一条数据还能写入到这个数据页中。


我们通过 Compact 格式,简单了解一下什么是 页外存储行溢出


MySQL8 InnoDB 引擎目前有 4 种 行记录格式:



  • REDUNDANT

  • COMPACT

  • DYNAMIC(默认 default 是这个)

  • COMPRESSED


行记录格式 决定了其行的物理存储方式,这反过来又会影响查询和 DML 操作的性能。



Compact 格式的实现思路是:当列的类型为 VARCHAR、 VARBINARY、 BLOB、TEXT 时,该列超过 768byte 的数据放到其他数据页中去。



在 MySQL 设定中,当 varchar 列长度达到 768byte 后,会将该列的前 768byte 当作当作 prefix 存放在行中,多出来的数据溢出存放到溢出页中,然后通过一个偏移量指针将两者关联起来,这就是 行溢出机制



假如你要存储的数据行很大超过了 65532byte 那么你是写入不进去的。假如你要存储的单行数据小于 65535byte 但是大于 16384byte,这时你可以成功 insert,但是一个数据页又存储不了你插入的数据。这时肯定会行溢出!



MySQL 这样做,有效的防止了单个 varchar 列或者 Text 列太大导致单个数据页中存放的行记录过少的情况,避免了 IO 飙升的窘境。


单行最大列数限制


mysql 单表最大列数也是有限制的,是 4096 ,但 InnoDB 是 1017



实验


前文中我们疑惑 max_allowed_packet 在 MySQL8 的默认值是 64M,又说这是限制单行数据的,单行数据有这么大吗? 在前文我们介绍了行溢出, 由于有了 行溢出 ,单行数据确实有可能比较大。


那么还剩下一个问题,max_allowed_packet 限制的确定是单行数据吗,难道不是查询结果集的大小吗 ? 下面我们做个实验,验证一下。


建表


CREATE TABLE t1 (
c1 CHAR(255),c2 CHAR(255),c3 CHAR(255),
c4 CHAR(255),c5 CHAR(255),c6 CHAR(255),
c7 CHAR(255),c8 CHAR(255),c9 CHAR(255),
c10 CHAR(255),c11 CHAR(255),c12 CHAR(255),
c13 CHAR(255),c14 CHAR(255),c15 CHAR(255),
c16 CHAR(255),c17 CHAR(255),c18 CHAR(255),
c19 CHAR(255),c20 CHAR(255),c21 CHAR(255),
c22 CHAR(255),c23 CHAR(255),c24 CHAR(255),
c25 CHAR(255),c26 CHAR(255),c27 CHAR(255),
c28 CHAR(255),c29 CHAR(255),c30 CHAR(255),
c31 CHAR(255),c32 CHAR(192)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET latin1;


经过测试虽然提示的是 Row size too large (> 8126) 但如果全部长度加起来是 8126 建表不成功,最终我试到 8097 是能建表成功的。为什么不是 8126 呢 ?可能是还需要存储一些其他的东西占了一些字节吧,比如隐藏字段什么的。


用存储过程造一些测试数据,把表中的所有列填满


create
definer = root@`%` procedure generate_test_data()
BEGIN
DECLARE i INT DEFAULT 0;
DECLARE col_value TEXT DEFAULT REPEAT('a', 255);
WHILE i < 5 DO
INSERT INTO t1 VALUES
(
col_value, col_value, col_value,
col_value, REPEAT('b', 192)
);
SET i = i + 1;
END WHILE;
END;


max_allowed_packet 设置的小一些,先用 show VARIABLES like '%max_allowed_packet%'; 看一下当前的大小,我的是 67108864 这个单位是字节,等于 64M,然后用 set global max_allowed_packet =1024 将它设置成允许的最小值 1024 byte。 设置好后,关闭当前查询窗口再新建一个,然后再查看:



这时我用 select * from t1; 查询表数据时就会报错:



因为我们一条记录的大小就是 8K 多了,所以肯定超过 1024byte。可见文档的说明是对的, max_allowed_packet 确实是可以约束单行记录大小的。


答案


文章写到这里,我有点儿写不下去了,一是因为懒,另外一个原因是关于这个问题:“一条 SQL 最多能查询出来多少条记录?” 肯定没有标准答案


目前我们可以知道的是:



  • 你的单行记录大小不能超过 max_allowed_packet

  • 一个表最多可以创建 1017 列 (InnoDB)

  • 建表时定义列的固定长度不能超过 页的一半(8k,16k...)

  • 建表时定义列的总长度不能超过 65535 个字节


如果这些条件我们都满足了,然后发出了一个没有 where 条件的全表查询 select * 那么.....


首先,你我都知道,这种情况不会发生在生产环境的,如果真发生了,一定是你写错了,忘了加条件。因为几乎没有这种要查询出所有数据的需求。如果有,也不能开发,因为这不合理。


我考虑的也就是个理论情况,从理论上讲能查询出多少数据不是一个确定的值,除了前文提到的一些条件外,它肯定与以下几项有直接的关系



  • 数据库的可用内存

  • 数据库内部的缓存机制,比如缓存区的大小

  • 数据库的查询超时机制

  • 应用的可用物理内存

  • ......


说到这儿,我确实可以再做个实验验证一下,但因为懒就不做了,大家有兴趣可以自己设定一些条件做个实验试一下,比如在特定内存和特定参数的情况下,到底能查询出多少数据,就能看得出来了。


虽然我没能给出文章开头问题的答案,但通过寻找答案也弄清楚了 MySQL 的一些限制条件,并加以了验证,也算是有所收获了。


参考



作者:xiaohezi
来源:juejin.cn/post/7255478273652834360
收起阅读 »

前任开发在代码里下毒了,支付下单居然没加幂等

故事又是一个风和日丽没好的一天,小猫戴着耳机,安逸地听着音乐,撸着代码,这种没有会议的日子真的是巴适得板。不料祸从天降,组长火急火燎地跑过来找到了小猫。“快排查一下,目前有A公司用户反馈积分被多扣了”。小猫回忆了一下“不对啊,这接口我也没动过啊,前几天对外平台...
继续阅读 »

故事

又是一个风和日丽没好的一天,小猫戴着耳机,安逸地听着音乐,撸着代码,这种没有会议的日子真的是巴适得板。

不料祸从天降,组长火急火燎地跑过来找到了小猫。“快排查一下,目前有A公司用户反馈积分被多扣了”。

小猫回忆了一下“不对啊,这接口我也没动过啊,前几天对外平台的老六直接找我要个支付接口,我就给他了的,以前的代码,我都没有动过的......”。

于是小猫一边疑惑一边翻看着以前的代码,越看脸色越差......

42175B273A64E95B1B5B66D392256552.jpg

小猫做的是一个标准的积分兑换商城,以前和客户合作的时候,客户直接用的是小猫单位自己定制的h5页面。这次合作了一家公司有点特殊,由于公司想要定制化自己个性化的H5,加上本身A公司自己有开发能力,所以经过讨论就以接口的方式直接将相关接口给出去,A客户H5开发完成之后自己来对接。

慢慢地,原因也水落石出,之前好好的业务一直没有问题是因为商城的本身H5页面做了防重复提交,由于量小,并且一般对接方式用的都是纯H5,所以都没有什么问题,然后这次是直接将接口给出去了,完了接口居然没有加幂等......

小猫躺枪,数据订正当然是少不了了,事故报告当然也少不了了。

正所谓前人挖坑,后人遭殃,前人锅后人背。

聊聊幂等

接口幂等梗概

这个案例其实就是一个典型的接口幂等案例。那么老猫就和大家从以下几个方面好好剖析一下接口幂等吧。

interfacemd.png

什么是接口幂等

比较专业的术语:其任意多次执行所产生的影响均与第一次执行的影响相同。 大白话:多次调用的情况下,接口最终得到的结果是一致的。

那么为什么需要幂等呢?

  1. 用户进行提交动作的时候,由于网络波动等原因导致后端同步响应不及时,这样用户就会一直点点点,这样机会发生重复提交的情况。
  2. 分布式系统之间调用的情况下,例如RPC调用,为了防止网络波动超时等造成的请求失败,都会添加重试机制,导致一个请求提交多次。
  3. 分布式系统经常会用到消息中间件,当由于网络原因,mq没有收到ack的情况下,就会导致消息的重复投递,从而就会导致重复提交行为。
  4. 还有就是恶意攻击了,有些业务接口做的比较粗糙,黑客找到漏洞之后会发起重复提交,这样就会导致业务出现问题。打个比方,老猫曾经干过,邻居小孩报名了一个画画比赛,估计是机构培训发起的,功能做的也差,需要靠投票赢得某些礼品,然后老猫抓到接口信息之后就模拟投票进行重复刷了投票。

那么哪些接口需要做幂等呢?

首先我们说是不是所有的接口都需要幂等?是不是加了幂等就好呢?显然不是。 因为接口幂等的实现某种意义上是要消耗系统性能的,我们没有必要针对所有业务接口都加上幂等。

这个其实并不能做一个完全的定义说哪个就不用幂等,因为很多时候其实还是得结合业务逻辑一起看。但是其中也是有规律可循的。

既然我们说幂等就是多次调用,接口最终得到结果一致,那么很显然,查询接口肯定是不要加幂等的,另外一些简单删除数据的接口,无论是逻辑删除还是物理删除,看场景的情况下其实也不用加幂等。

但是大部分涉及到多表更新行为的接口,咱们最好还是得加上幂等。

接口幂等实战方案

前端防抖处理

前端防抖主要可以有两种方案,一种是技术层面的,一种是产品层面的:

  1. 技术层面:例如提交控制在100ms内,同一个用户最多只能做一次订单提交的操作。
  2. 产品层面:当然用户点击提交之后,按钮直接置灰。

基于数据库唯一索引

  1. 利用数据库唯一索引。我们具体来看一下流程,咱们就用小猫遇到的例子。如下:

unique-key.png

过程描述:

  • 建立一张去重表,其中某个字段需要建立唯一索引,例如小猫这个场景中,咱们就可以将订单提交流水单号作为唯一索引存储到我们的数据库中,就模型上而言,可以将其定义为支付请求流水表。
  • 客户端携带相关流水信息到后端,如果发现编号重复,那么此时就会插入失败,报主键冲突的错误,此时我们针对该错误做一下业务报错的二次封装给到客户另一个友好的提示即可。

数据库乐观锁实现

什么是乐观锁,它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。 说得直白一点乐观锁就是一个马大哈。总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,只在更新的时候会判断一下在此期间别人有没有去更新这个数据。

例如提交订单的进行支付扣款的时候,本来可能更新账户金额扣款的动作是这样的:

update Account set balance = balance-#{payAmount} where accountCode = #{accountCode}

加上版本号之后,咱们的代码就是这样的。

update Account set balance = balance-#{payAmount},version=version +1 where accountCode = #{accountCode} and version = #{currVersion}

这种情况下其实就要求客户端每次在请求支付下单的时候都需要上层客户端指定好当前的版本信息。 不过这种幂等的处理方式,老猫用的比较少。

数据库悲观锁实现

悲观锁的话具有强烈的独占和排他特性。大白话谁都不信的主。所以我们就用select ... for update这样的语法进行行锁,当然老猫觉得单纯的select ... for update只能解决同一时刻大并发的幂等,所以要保证单号重试这样非并发的幂等请求还是得去校验当前数据的状态才行。就拿当前的小猫遇到的场景来说,流程如下:

pessimistic.png

begin;  # 1.开始事务
select * from order where order_code='666' for update # 查询订单,判断状态,锁住这条记录
if(status !=处理中){
//非处理中状态,直接返回;
return ;
}
## 处理业务逻辑
update order set status='完成' where order_code='666' # 更新完成
update stock set num = num - 1 where spu='xxx' # 库存更新
commit; # 5.提交事务

这里老猫一再想要强调的是在校验的时候还是得带上本身的业务状态去做校验,select ... for update并非万能幂等。

后端生成token

这个方案的本质其实是引入了令牌桶的机制,当提交订单的时候,前端优先会调用后端接口获取一个token,token是由后端发放的。当然token的生成方式有很多种,例如定时刷新令牌桶,或者定时生成令牌并放到令牌池中,当然目的只有一个就是保住token的唯一性即可。

生成token之后将token放到redis中,当然需要给token设置一个失效时间,超时的token也会被删除。

当后端接收到订单提交的请求的时候,会先判断token在缓存中是否存在,第一次请求的时候,token一定存在,也会正常返回结果,但是第二次携带同一个token的时候被拒绝了。

流程如下:

token.png

有个注意点大家可以思考一下: 如果用户用程序恶意刷单,同一个token发起了多次请求怎么办? 想要实现这个功能,就需要借助分布式锁以及Lua脚本了,分布式锁可以保证同一个token不能有多个请求同时过来访问,lua脚本保证从redis中获取令牌->比对令牌->生成单号->删除令牌这一系列行为的原子性。

分布式锁+状态机(订单状态)

现在很多的业务服务都是分布式系统,所以就拿分布式锁来说,关于分布式锁,老猫在此不做赘述,之前老猫写过redis的分布式锁和实现,还有zk锁和实现,具体可见链接:

当然和上述的数据库悲观锁类似,咱们的分布式锁也只能保证同一个订单在同一时间的处理。其次也是要去校订单的状态,防止其重复支付的,也就是说,只要支付的订单进入后端,都要将原先的订单修改为支付中,防止后续支付中断之后的重复支付。

在上述小猫的流程中还没有涉及到现金补充,如果涉及到现金补充的话,例如对接了微信或者支付宝的情况,还需要根据最终的支付回调结果来最终将订单状态进行流转成支付完成或者是支付失败。

总结

在我们日常的开发中,一些重要的接口还是需要大家谨慎对待,即使是前任开发留下的接口,没有任何改动,当有人咨询的时候,其实就要好好去了解一下里面的实现,看看方案有没有问题,看看技术实现有没有问题,这应该也是每一个程序员的基本素养。

另外的,在一些重要的接口上,尤其是资金相关的接口上,幂等真的是相当的重要。小伙伴们,你们觉得呢?如果大家还有好的解决方案,或者有其他思考或者意见也欢迎大家的留言。


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

Stream很好,Map很酷,但答应我别用toMap()

在 JDK 8 中 Java 引入了让人欲罢不能的 stream 流处理,可以说已经成为了我日常开发中不可或缺的一部分。 当完成一次流处理之后需要返回一个集成对象时,已经肌肉记忆的敲下 collect(Collectors.toList()) 或者 colle...
继续阅读 »

JDK 8Java 引入了让人欲罢不能的 stream 流处理,可以说已经成为了我日常开发中不可或缺的一部分。


当完成一次流处理之后需要返回一个集成对象时,已经肌肉记忆的敲下 collect(Collectors.toList()) 或者 collect(Collectors.toSet())。你可能会想,toListtoSet 都这么便捷顺手了,当又怎么能少得了 toMap() 呢。


答应我,一定打消你的这个想法,否则这将成为你噩梦的开端。


image.png


什么?你不信,没有什么比代码让人更痛彻心扉,让我们直接上代码。


让我们先准备一个用户实体类。


@Data
@AllArgsConstructor
public class User {

private int id;

private String name;
}

假设有这么一个场景,你从数据库读取 User 集合,你需要将其转为 Map 结构数据,keyvalue 分别为 useridname


很快,你啪的一下就写出了下面的代码:


public class UserTest {
@Test
public void demo() {
List<User> userList = new ArrayList<>();
// 模拟数据
userList.add(new User(1, "Alex"));
userList.add(new User(1, "Beth"));

Map<Integer, String> map = userList.stream()
.collect(Collectors.toMap(User::getId, User::getName));
System.out.println(map);
}
}

运行程序,你已经想好了开始怎么摸鱼,结果啪的一下 IllegalStateException 报错就拍你脸上,你定睛一看怎么提示 Key 值重复。


image.png


作为优秀的八股文选手,你清楚的记得 HashMap 对象 Key 重复是进行替换。你不信邪,断点一打,堆栈一看,硕大的 uniqKeys 摆在了面前,凭借四级 424 分的优秀战绩你顿时菊花一紧,点开一看,谁家好人 map key 还要去重判断啊。


image.png


好好好,这么玩是吧,你转身打开浏览器一搜,原来需要自己手动处理重复场景,啪的一下你又重新改了一下代码:


public class UserTest {
@Test
public void demo() {
List<User> userList = new ArrayList<>();
// 模拟数据
userList.add(new User(1, "Alex"));
userList.add(new User(2, null));

Map<Integer, String> map = userList.stream()
.collect(Collectors.toMap(User::getId, User::getName, (oldData, newData) -> newData));
System.out.println(map);
}
}

再次执行程序,你似乎已经看到知乎的摸鱼贴在向你招手了,结果啪的一下 NPE 又拍在你那笑容渐渐消失的脸上。



静下心来,本着什么大风大浪我没见过的心态,断点堆栈一气呵成,而下一秒你又望着代码陷入了沉思,我是谁?我在干什么?




鼓起勇气,你还不信今天就过不去这个坎了,大手一挥,又一段优雅的代码孕育而生。


public class UserTest {
@Test
public void demo() {
List<User> userList = new ArrayList<>();
// 模拟数据
userList.add(new User(1, "Alex"));
userList.add(new User(1, "Beth"));
userList.add(new User(2, null));

Map<Integer, String> map = userList.stream()
.collect(Collectors.toMap(
User::getId,
it -> Optional.ofNullable(it.getName()).orElse(""),
(oldData, newData) -> newData)
);
System.out.println(map);
}
}

优雅,真是太优雅了,又是 Stream 又是 Optional,可谓是狠狠拿捏技术博文的 G 点了。


image.png


这时候你回头一看,我需要是什么来着?这 TM 不是一个循环就万事大吉了吗,不信邪的你回归初心,回归了 for 循环的怀抱,又写了一版。


public class UserTest {
@Test
public void demo() {
List<User> userList = new ArrayList<>();
// 模拟数据
userList.add(new User(1, "Alex"));
userList.add(new User(1, "Beth"));
userList.add(new User(2, null));

Map<Integer, String> map = new HashMap<>();
userList.forEach(it -> {
map.put(it.getId(), it.getName());
});
System.out.println(map);
}
}

看着运行完美无缺的代码,你一时陷入了沉思,数分钟过去了,你删除了 for 循环,换上 StreamOptional 不羁的外衣,安心的提交了代码,这口细糠一定也要让好同事去尝一尝。


image.png


作者:烽火戏诸诸诸侯
来源:juejin.cn/post/7383643463534018579
收起阅读 »

写了一个责任链模式,bug 无数...

责任链模式是一种行为设计模式, 允许你将请求沿着处理者链进行发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。 使用场景 责任链的使用场景还是比较多的: 多条件流程判断:权限控制 ERP 系统流程审批:总经理、人事经理、项...
继续阅读 »

责任链模式是一种行为设计模式, 允许你将请求沿着处理者链进行发送。


收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。


图片


使用场景


责任链的使用场景还是比较多的:



  • 多条件流程判断:权限控制

  • ERP 系统流程审批:总经理、人事经理、项目经理

  • Java 过滤器的底层实现 Filter


如果不使用该设计模式,那么当需求有所改变时,就会使得代码臃肿或者难以维护,例如下面的例子。


| 反例


假设现在有一个闯关游戏,进入下一关的条件是上一关的分数要高于 xx:



  • 游戏一共 3 个关卡

  • 进入第二关需要第一关的游戏得分大于等于 80

  • 进入第三关需要第二关的游戏得分大于等于 90


那么代码可以这样写:


//第一关
public class FirstPassHandler {
    public int handler(){
        System.out.println("第一关-->FirstPassHandler");
        return 80;
    }
}

//第二关
public class SecondPassHandler {
    public int handler(){
        System.out.println("第二关-->SecondPassHandler");
        return 90;
    }
}

//第三关
public class ThirdPassHandler {
    public int handler(){
        System.out.println("第三关-->ThirdPassHandler,这是最后一关啦");
        return 95;
    }
}

//客户端
public class HandlerClient {
    public static void main(String[] args) {

        FirstPassHandler firstPassHandler = new FirstPassHandler();//第一关
        SecondPassHandler secondPassHandler = new SecondPassHandler();//第二关
        ThirdPassHandler thirdPassHandler = new ThirdPassHandler();//第三关

        int firstScore = firstPassHandler.handler();
        //第一关的分数大于等于80则进入第二关
        if(firstScore >= 80){
            int secondScore = secondPassHandler.handler();
            //第二关的分数大于等于90则进入第二关
            if(secondScore >= 90){
                thirdPassHandler.handler();
            }
        }
    }
}

那么如果这个游戏有 100 关,我们的代码很可能就会写成这个样子:


if(第1关通过){
    // 第2关 游戏
    if(第2关通过){
        // 第3关 游戏
        if(第3关通过){
           // 第4关 游戏
            if(第4关通过){
                // 第5关 游戏
                if(第5关通过){
                    // 第6关 游戏
                    if(第6关通过){
                        //...
                    }
                }
            }
        }
    }
}

这种代码不仅冗余,并且当我们要将某两关进行调整时会对代码非常大的改动,这种操作的风险是很高的,因此,该写法非常糟糕。


| 初步改造


如何解决这个问题,我们可以通过链表将每一关连接起来,形成责任链的方式,第一关通过后是第二关,第二关通过后是第三关....


这样客户端就不需要进行多重 if 的判断了:


public class FirstPassHandler {
    /**
     * 第一关的下一关是 第二关
     */

    private SecondPassHandler secondPassHandler;

    public void setSecondPassHandler(SecondPassHandler secondPassHandler) {
        this.secondPassHandler = secondPassHandler;
    }

    //本关卡游戏得分
    private int play(){
        return 80;
    }

    public int handler(){
        System.out.println("第一关-->FirstPassHandler");
        if(play() >= 80){
            //分数>=80 并且存在下一关才进入下一关
            if(this.secondPassHandler != null){
                return this.secondPassHandler.handler();
            }
        }

        return 80;
    }
}

public class SecondPassHandler {

    /**
     * 第二关的下一关是 第三关
     */

    private ThirdPassHandler thirdPassHandler;

    public void setThirdPassHandler(ThirdPassHandler thirdPassHandler) {
        this.thirdPassHandler = thirdPassHandler;
    }

    //本关卡游戏得分
    private int play(){
        return 90;
    }

    public int handler(){
        System.out.println("第二关-->SecondPassHandler");

        if(play() >= 90){
            //分数>=90 并且存在下一关才进入下一关
            if(this.thirdPassHandler != null){
                return this.thirdPassHandler.handler();
            }
        }

        return 90;
    }
}

public class ThirdPassHandler {

    //本关卡游戏得分
    private int play(){
        return 95;
    }

    /**
     * 这是最后一关,因此没有下一关
     */

    public int handler(){
        System.out.println("第三关-->ThirdPassHandler,这是最后一关啦");
        return play();
    }
}

public class HandlerClient {
    public static void main(String[] args) {

        FirstPassHandler firstPassHandler = new FirstPassHandler();//第一关
        SecondPassHandler secondPassHandler = new SecondPassHandler();//第二关
        ThirdPassHandler thirdPassHandler = new ThirdPassHandler();//第三关

        firstPassHandler.setSecondPassHandler(secondPassHandler);//第一关的下一关是第二关
        secondPassHandler.setThirdPassHandler(thirdPassHandler);//第二关的下一关是第三关

        //说明:因为第三关是最后一关,因此没有下一关
        //开始调用第一关 每一个关卡是否进入下一关卡 在每个关卡中判断
        firstPassHandler.handler();

    }
}

| 缺点


现有模式的缺点:



  • 每个关卡中都有下一关的成员变量并且是不一样的,形成链很不方便

  • 代码的扩展性非常不好,最新设计模式面试题整理好了


| 责任链改造


既然每个关卡中都有下一关的成员变量并且是不一样的,那么我们可以在关卡上抽象出一个父类或者接口,然后每个具体的关卡去继承或者实现。


有了思路,我们先来简单介绍一下责任链设计模式的基本组成:



  • 抽象处理者(Handler)角色: 定义一个处理请求的接口,包含抽象处理方法和一个后继连接。

  • 具体处理者(Concrete Handler)角色: 实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。

  • 客户类(Client)角色: 创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。


图片


public abstract class AbstractHandler {

    /**
     * 下一关用当前抽象类来接收
     */

    protected AbstractHandler next;

    public void setNext(AbstractHandler next) {
        this.next = next;
    }

    public abstract int handler();
}

public class FirstPassHandler extends AbstractHandler{

    private int play(){
        return 80;
    }

    @Override
    public int handler(){
        System.out.println("第一关-->FirstPassHandler");
        int score = play();
        if(score >= 80){
            //分数>=80 并且存在下一关才进入下一关
            if(this.next != null){
                return this.next.handler();
            }
        }
        return score;
    }
}

public class SecondPassHandler extends AbstractHandler{

    private int play(){
        return 90;
    }

    public int handler(){
        System.out.println("第二关-->SecondPassHandler");

        int score = play();
        if(score >= 90){
            //分数>=90 并且存在下一关才进入下一关
            if(this.next != null){
                return this.next.handler();
            }
        }

        return score;
    }
}

public class ThirdPassHandler extends AbstractHandler{

    private int play(){
        return 95;
    }

    public int handler(){
        System.out.println("第三关-->ThirdPassHandler");
        int score = play();
        if(score >= 95){
            //分数>=95 并且存在下一关才进入下一关
            if(this.next != null){
                return this.next.handler();
            }
        }
        return score;
    }
}

public class HandlerClient {
    public static void main(String[] args) {

        FirstPassHandler firstPassHandler = new FirstPassHandler();//第一关
        SecondPassHandler secondPassHandler = new SecondPassHandler();//第二关
        ThirdPassHandler thirdPassHandler = new ThirdPassHandler();//第三关

        // 和上面没有更改的客户端代码相比,只有这里的set方法发生变化,其他都是一样的
        firstPassHandler.setNext(secondPassHandler);//第一关的下一关是第二关
        secondPassHandler.setNext(thirdPassHandler);//第二关的下一关是第三关

        //说明:因为第三关是最后一关,因此没有下一关

        //从第一个关卡开始
        firstPassHandler.handler();

    }
}

| 责任链工厂改造


对于上面的请求链,我们也可以把这个关系维护到配置文件中或者一个枚举中。我将使用枚举来教会大家怎么动态的配置请求链并且将每个请求者形成一条调用链。


图片


public enum GatewayEnum {
    // handlerId, 拦截者名称,全限定类名,preHandlerId,nextHandlerId
    API_HANDLER(new GatewayEntity(1"api接口限流""cn.dgut.design.chain_of_responsibility.GateWay.impl.ApiLimitGatewayHandler"null2)),
    BLACKLIST_HANDLER(new GatewayEntity(2"黑名单拦截""cn.dgut.design.chain_of_responsibility.GateWay.impl.BlacklistGatewayHandler"13)),
    SESSION_HANDLER(new GatewayEntity(3"用户会话拦截""cn.dgut.design.chain_of_responsibility.GateWay.impl.SessionGatewayHandler"2null)),
    ;

    GatewayEntity gatewayEntity;

    public GatewayEntity getGatewayEntity() {
        return gatewayEntity;
    }

    GatewayEnum(GatewayEntity gatewayEntity) {
        this.gatewayEntity = gatewayEntity;
    }
}

public class GatewayEntity {

    private String name;

    private String conference;

    private Integer handlerId;

    private Integer preHandlerId;

    private Integer nextHandlerId;
}

public interface GatewayDao {

    /**
     * 根据 handlerId 获取配置项
     * 
@param handlerId
     * 
@return
     */

    GatewayEntity getGatewayEntity(Integer handlerId);

    /**
     * 获取第一个处理者
     * 
@return
     */

    GatewayEntity getFirstGatewayEntity();
}

public class GatewayImpl implements GatewayDao {

    /**
     * 初始化,将枚举中配置的handler初始化到map中,方便获取
     */

    private static Map gatewayEntityMap = new HashMap<>();

    static {
        GatewayEnum[] values = GatewayEnum.values();
        for (GatewayEnum value : values) {
            GatewayEntity gatewayEntity = value.getGatewayEntity();
            gatewayEntityMap.put(gatewayEntity.getHandlerId(), gatewayEntity);
        }
    }

    @Override
    
public GatewayEntity getGatewayEntity(Integer handlerId) {
        return gatewayEntityMap.get(handlerId);
    }

    @Override
    
public GatewayEntity getFirstGatewayEntity() {
        for (Map.Entry entry : gatewayEntityMap.entrySet()) {
            GatewayEntity value = entry.getValue();
            //  没有上一个handler的就是第一个
            if (value.getPreHandlerId() == null) {
                return value;
            }
        }
        return null;
    }
}

public class GatewayHandlerEnumFactory {

    private static GatewayDao gatewayDao = new GatewayImpl();

    // 提供静态方法,获取第一个handler
    public static GatewayHandler getFirstGatewayHandler() {

        GatewayEntity firstGatewayEntity = gatewayDao.getFirstGatewayEntity();
        GatewayHandler firstGatewayHandler = newGatewayHandler(firstGatewayEntity);
        if (firstGatewayHandler == null) {
            return null;
        }

        GatewayEntity tempGatewayEntity = firstGatewayEntity;
        Integer nextHandlerId = null;
        GatewayHandler tempGatewayHandler = firstGatewayHandler;
        // 迭代遍历所有handler,以及将它们链接起来
        while ((nextHandlerId = tempGatewayEntity.getNextHandlerId()) != null) {
            GatewayEntity gatewayEntity = gatewayDao.getGatewayEntity(nextHandlerId);
            GatewayHandler gatewayHandler = newGatewayHandler(gatewayEntity);
            tempGatewayHandler.setNext(gatewayHandler);
            tempGatewayHandler = gatewayHandler;
            tempGatewayEntity = gatewayEntity;
        }
    // 返回第一个handler
        return firstGatewayHandler;
    }

    /**
     * 反射实体化具体的处理者
     * 
@param firstGatewayEntity
     * 
@return
     */

    private static GatewayHandler newGatewayHandler(GatewayEntity firstGatewayEntity) {
        // 获取全限定类名
        String className = firstGatewayEntity.getConference();
        try {
            // 根据全限定类名,加载并初始化该类,即会初始化该类的静态段
            Class clazz = Class.forName(className);
            return (GatewayHandler) clazz.newInstance();
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }
        return null;
    }

}

public class GetewayClient {
    public static void main(String[] args) {
        GetewayHandler firstGetewayHandler = GetewayHandlerEnumFactory.getFirstGetewayHandler();
        firstGetewayHandler.service();
    }
}

设计模式有很多,责任链只是其中的一种,我觉得很有意思,非常值得一学。设计模式确实是一门艺术,仍需努力呀!




作者:程序员蜗牛
来源:juejin.cn/post/7383643463534067731
收起阅读 »

HTTP3为什么抛弃了经典的TCP,转而拥抱 QUIC 呢

我们在看一些关于计算机网络的数据或文章的时候,最常听到的就是 TCP、UDP、HTTP 这些,除此之外,我们或多或少可能听过 QUIC这个东西,一般跟这个词一起出现的是 HTTP3,也就是HTTP协议的3.0版本,未来2.x 版本的升级方案。QUIC...
继续阅读 »

我们在看一些关于计算机网络的数据或文章的时候,最常听到的就是 TCP、UDP、HTTP 这些,除此之外,我们或多或少可能听过 QUIC这个东西,一般跟这个词一起出现的是 HTTP3,也就是HTTP协议的3.0版本,未来2.x 版本的升级方案。

QUIC 由 Google 主导设计研发。我们都知道 HTTP 协议是应用层协议,在传输层它使用的是 TCP 作为传输协议,当然这仅仅是对于 HTTP/1 和 HTTP/2 而言的。而 QUIC 的设计的对标协议就是 TCP ,也就是说将来只要能使用 TCP 的地方,都可以用 QUIC 代替。

Google 最开始的目的就是为了替换 HTTP 协议使用的 TCP 协议,所以最开始的名字叫做 HTTP over QUIC,后来由 IETF 接管后更名为 HTTP/3。所以,现在说起 HTTP/3 ,实际指的就是利用 QUIC 协议的版本。

TCP 不好吗,为什么还要 QUIC

TCP 协议作为传输层最负盛名的协议,可谓是久经考验。只要一说到 TCP ,我们都能说出来它是一种面向连接的、可靠的、基于字节流的传输层通信协议。

TCP 通过三次握手的方式建立连接,并且通过序号、ACK确认、丢包重传以及流量控制、拥塞控制等各种繁琐又细致的方式来保证它的可靠性。

关于 TCP 的更多细节,有兴趣的可以读读我之前写的《轻解计算机网络》里的 一个网管的自我修养之TCP协议

看上去很完美了,那为什么还要重新搞个 QUIC 出来呢,而且还被作为下一代 HTTP 的实现协议。确实不存在完美的协议,TCP 协议在整个发展过程中经过了多次改进,但是由于牵扯到互联网世界浩如烟海的各种设备,每次更新、修改都要考虑兼容问题,历史包袱太重,以至于尾大不掉。

所以为了弥补 TCP 的不足,在 TCP 上直接修改不太可能,那最好的方案就是重新开发一套协议。这种协议要吸收 TCP 的精华,又要解决 TCP 的不足,这就是 QUIC 出现的意义。

TCP 的问题-队头阻塞

时至今日,互联网上大多数网站都已经支持 HTTP/2 协议了,你可以在浏览器开发者工具中看一下网络请求,其中的 Protocol 表示网络请求采用的协议。

image.png

HTTP/2的一个主要特性是使用多路复用(multiplexing),因而它可以通过同一个TCP连接发送多个逻辑数据流。复用使得很多事情变得更快更好,它带来更好的拥塞控制、更充分的带宽利用、更长久的TCP连接————这些都比以前更好了,链路能更容易实现全速传输。标头压缩技术也减少了带宽的用量。

采用HTTP/2后,浏览器对每个主机一般只需要 一个 TCP连接,而不是以前常见的六个连接。

如下图所示,HTTP/2 在使用 TCP 传输数据的时候,可以在一个连接上传输两个不同的流,红色是一个流,绿色是另外一个流,但是仍然是按顺序传输的,假设其中有一个包丢了,那整个链路上这个包后面的部分都要等待。

image.png

这就造成了阻塞,虽然一个连接可传多个流,但仍然存在单点问题。这个问题就叫做队头阻塞。

QUIC 如何解决的

TCP 这个问题是无解的,QUIC 就是为了彻底解决这个问题。

如下图所示,两台设备之间建立的是一个 QUIC 连接,但是可以同时传输多个相互隔离的数据流。例如黄色是一个数据流,蓝色是一个数据流,它俩互不影响,即便其中一个数据流有丢包的问题,也完全不会影响到其他的数据流传输。

这样一来,也就解决了 TCP 的队头阻塞问题。

image.png

为什么要基于 UDP 协议

QUIC 虽然是和TCP 平行的传输协议,工作在传输层,但是其并不是完全采用全新设计的,而是对 UDP 协议进行了包装。

UDP 是无连接的,相对于 TCP 来说,无连接就是不可靠的,没有三次握手,没有丢包重传,没有各种各样的复杂算法,但这带来了一个好处,那就是速度快。

而 QUIC 为了达到 TCP 的可靠性,所以在 UDP 的基础上增加了序号机制、丢包重传等等 UDP 没有而 TCP 具有的特性。

既然这么多功能都做了,还差一个 UDP 吗,完全全新设计一个不好吗,那样更彻底呀。

之所以不重新设计应该有两个原因:

  1. UDP 本身就是非常经典的传输层协议,对于快速传输来说,其功能完全没有问题。
  2. 还有一个重要的原因,前面也说到了,互联网上的设备太多,而很多设备只认 TCP 和 UDP 协议,如果设计一个完全全新的协议,很难实施。

QUIC 协议

不需要三次握手

QUIC 建立连接的速度是非常快的,不需要 TCP 那样的三次握手,称之为 0-RTT(零往返时间)及 1-RTT(1次往返时间)。

QUIC 使用了TLS 1.3传输层安全协议,所以 QUIC 传输的数据都是加密的,也就是说 HTTP/3 直接就是 HTTPS 的,不存在 HTTP 的非加密版本。

正是因为这样,所以,QUIC 建立连接的过程就是 TLS 建立连接的过程,如下图这样,首次建立连接只需要 1-RTT。

image.png

而在首次连接建立之后,QUIC 客户端就缓存了服务端发来的 Server Hello,也就是加密中所需要的一些内容。在之后重新建立连接时,只需要根据缓存内容直接加密数据,所以可以在客户端向服务端发送连接请求的同时将数据也一并带过去,这就是 0-RTT 。

连接不依靠 IP

QUIC 在建立连接后,会为这个连接分配一个连接 ID,用这个 ID 可以识别出具体的连接。

假设我正在家里用 WIFI 发送请求,但是突然有事儿出去了,马上切换到了蜂窝网络,那对于 QUIC 来说就没有什么影响。因为这个连接没有变,所以仍然可以继续执行请求,数据该怎么传还怎么传。

而如果使用的是 TCP 协议的话,那只能重新建立连接,重传之前的数据,因为 TCP 的寻址依靠的是 IP 和 端口。

未来展望

随着 QUIC 协议的不断完善和推广,其应用场景将更加广泛,对互联网传输技术产生深远的影响。未来的互联网,将是 QUIC 和 HTTP3 主导的时代。

要知道,HTTP/1 到 HTTP/2,中间用了整整 16 年才完成,这还只是针对协议做改进和优化,而 QUIC 可谓是颠覆性的修改,可想而知,其过程只会更加漫长。



作者:古时的风筝
来源:juejin.cn/post/7384266820466180148
收起阅读 »

Moka Ascend 2024|势在·人为,技术创新,激发企业管理内在效能

2024年6月19日,Moka 在深圳举办的 Moka Ascend 2024 产品发布会圆满落幕。此次大会以「势在·人为」为主题,汇聚了来自不同企业的管理者、人力资源专家和技术创新者,共同探讨和分享他们在人力资源管理、组织效能提升以及全球化招聘等方面的最新实...
继续阅读 »

2024年6月19日,Moka 在深圳举办的 Moka Ascend 2024 产品发布会圆满落幕。此次大会以「势在·人为」为主题,汇聚了来自不同企业的管理者、人力资源专家和技术创新者,共同探讨和分享他们在人力资源管理、组织效能提升以及全球化招聘等方面的最新实践和深刻见解。

Moka 联合创始人兼 CEO 李国兴在题为「势在必行:激发管理效率的内在势能」的主题演讲中,正式发布「Moka 人效管理解决方案」,以回应当前企业对经营和管理的诉求。要看清楚「人效」,管理者不应该只是得到事后汇报和对补救过程做出决策,而是真的需要一个内嵌于HR业务中,涵盖企业人力资源管理全流程,具备人力资源全面效能的工具。「Moka 人效管理解决方案」从成本到人效,从规划到落地,让决策更有依据,让管理更有保障,为企业健康发展[ 控本增效 ],帮助企业更清晰而全面地看到人力资源效能的运作情况,也让过程中的管理价值不断重复循环起来,「看得清」才能更好的「管得住」。

今年1月初,Moka 携手钉钉,推出「钉钉人事旗舰版」,致力于解决中大型企业在 HR 软件的使用过程中面临的系统割裂、数据孤岛等问题。此产品一经推出,对于整个行业来说,都是让人眼前一亮的「HR SaaS 新物种」。

钉钉生态业务部总经理陈霆旭与李国兴一起就合作后的进展和成绩进行了分享。在阐述钉钉开放生态战略合作的构想时,陈霆旭提到,在人事场景方面,Moka 是理想的合作伙伴,我们双方通过深度融合的方式,创新性地满足了客户需求。「钉钉人事旗舰版」之所以能被市场快速验证和接纳,本质还为了更好满足客户价值,大家在价值链上找到了更精细的分工和更清晰的边界,干自己更擅长的事,钉钉有非常丰富的IM、OA、文档、音视频等协同办公能力,也让企业的组织数字化实现零门槛,Moka在人事领域也有非常深的积淀,有了这种强强融合共建的产品能给员工和HR带来前所未有的创新体验,为客户带来「1+1>2」的价值和效果。

除了「看得清」,「看得远」成为企业不断进步与更新的动力。新一代大模型时代到来,AI 技术越来越多的可能性呈现在大众面前。Moka 合伙人兼 CTO 刘洪泽在大会上分享了 Moka 在 AI 领域的技术创新和应用实践。以 AI 原生为理念,深入业务场景,Moka Eva 的能力不断进化,不仅完成了从智能面试到智能招聘解决方案的升级,同时也赋能于全新功能「SmartPractice」,集成多国家和地区招聘最佳实践,智能辅助HR全球招聘,全流程提升招聘效率与招聘质量。

同时,AI 亦全面赋能 Moka 技术平台,构建新一代底层平台能力。伴随着新加坡数据中心的投入使用,以及一直以来与合作伙伴的生态共融,Moka 将与更多客户实现合作共赢,不断向打造世界级HR产品的愿景靠近。

企业成长是个超长周期的动态过程,过程中企业会面临市场内卷、业务难管等挑战,针对企业发展难题,各行业有成功实践经验的管理者提出新解法。

本次大会亦邀请了多位企业管理者分享了他们在人效管理、本地化人才战略和全球化招聘方面的深刻见解和实践经验。来自卓尔数科的CHO屠俊强调了在不确定性环境下,"1+3"模型发挥着重要作用,在应用SOTE模型的同时,构建组织力、业务力和影响力,实现业务与组织管理的深度融合。KLOOK 客路旅行的中国区人力资源总监戴良辰则分享了如何通过业务共创和团队成长两大维度,落地本地招聘,推动业务快速发展。

此外,本次大会还举办了题为”无界之帆:全球化招聘的挑战和实践“的圆桌论坛,由李国兴主持,邀请了携程集团招聘运营负责人梁昊耘、AfterShip Global HRD Flora Zeng和海辰储能招聘总监金一凡,共同探讨海外团队组建的挑战与经验,如何在全球范围内构建雇主品牌,以及如何利用系统和工具优化招聘流程。这些分享不仅展现了中国企业在全球化浪潮中的积极作为,也为其他企业提供了宝贵的参考和启示。

势在人为,破局而出。Moka 未来仍会继续坚持「全员体验更好」的产品理念,响应客户需求,持续优化产品与服务,助力企业实现更高效、更智能的人事管理。同时,Moka 也将积极与各方共同探索人事管理新机遇,在动态的变化中寻求可期待的攀升,保持始终向上的能量。

收起阅读 »

打脸了,rust确实很快

正文: 原标题:不要盲目迷信rust,rust或许没有你想象中的那么快 之前的文章内容是关于rust速度不快的问题。但是我遗留的问题是,计算出来的哈希值不一样。经过老哥的指点,原来是我js代码的实现有点问题,让大家见笑了。 于是我修改代码,重新测试了一遍,这...
继续阅读 »

正文:


原标题:不要盲目迷信rust,rust或许没有你想象中的那么快


之前的文章内容是关于rust速度不快的问题。但是我遗留的问题是,计算出来的哈希值不一样。经过老哥的指点,原来是我js代码的实现有点问题,让大家见笑了。


image.png


于是我修改代码,重新测试了一遍,这时哈希值就一样了,并且计算的结果确实是rust要快。刚好拿来佐证rust快的事实。


image.png


以后一定经过仔细验证,再发表文章。原文不删,留作警醒。


以下是原文:


原文:


先说结论:抛开应用场景,单说语言速度都是耍流氓。因为js调度rust,会有时间损耗。所以rust在一定应用场景下,比js还慢。


# 使用 wasm 提高前端20倍的 md5 计算速度


前两天看了一篇文件,是使用rust和wasm来加快md5的计算时间。跑了他的demo,发现只有rust的demo,而没有js的对比,于是我fork项目后,补充了一个js的对比。


image.png


测试下来,发现rust并没有比js快多少,由于浏览器限制,我只能用2GB文件来测,不知道是不是这个原因。还是我使用的js的原因。至少在2GB的边界时,js比rust要快。


他rust部分我没有动,只是添加了一个js的对比,如果大家觉得我的js写得有问题,欢迎pr重新比较。


在线对比地址:minori-ty.github.io/digest-wasm…


项目地址: github.com/Minori-ty/d…


作者:天平
来源:juejin.cn/post/7359757993732734991
收起阅读 »

手把手带你开发一套用户权限系统,精确到按钮级

在实际的软件项目开发过程中,用户权限控制可以说是所有运营系统中必不可少的一个重点功能,根据业务的复杂度,设计的时候可深可浅,但无论怎么变化,设计的思路基本都是围绕着用户、角色、菜单这三个部分展开。 如何设计一套可以精确到按钮级别的用户权限功能呢? 今天通过这篇...
继续阅读 »

在实际的软件项目开发过程中,用户权限控制可以说是所有运营系统中必不可少的一个重点功能,根据业务的复杂度,设计的时候可深可浅,但无论怎么变化,设计的思路基本都是围绕着用户、角色、菜单这三个部分展开


如何设计一套可以精确到按钮级别的用户权限功能呢?


今天通过这篇文章一起来了解一下相关的实现逻辑,不多说了,直接上案例代码!


01、数据库设计


在进入项目开发之前,首先我们需要进行相关的数据库设计,以便能存储相关的业务数据。


对于【用户权限控制】功能,通常5张表基本就可以搞定,分别是:用户表、角色表、用户角色表、菜单表、角色菜单表,相关表结构示例如下。



其中,用户和角色是多对多的关系角色与菜单也是多对多的关系用户通过角色来关联到菜单,当然也有的用户权限控制模型中,直接通过用户关联到菜单,实现用户对某个菜单权限独有控制,这都不是问题,可以自由灵活扩展。


用户、角色表的结构设计,比较简单。下面,我们重点来解读一下菜单表的设计,如下:



可以看到,整个菜单表就是一个父子表结构,关键字段如下



  • name:菜单名称

  • menu_code:菜单编码,用于后端权限控制

  • parent_id:菜单父节点ID,方便递归遍历菜单

  • node_type:菜单节点类型,可以是文件夹、页面或者按钮类型

  • link_url:菜单对应的地址,如果是文件夹或者按钮类型,可以为空

  • level:菜单树的层次,以便于查询指定层级的菜单

  • path:树id的路径,主要用于存放从根节点到当前树的父节点的路径,想要找父节点时会特别快


为了方便项目后续开发,在此我们创建一个名为menu_auth_db的数据库,SQL 初始脚本如下:


CREATE DATABASE IF NOT EXISTS `menu_auth_db` default charset utf8mb4 COLLATE utf8mb4_unicode_ci;

CREATE TABLE `menu_auth_db`.`tb_user` (
`id` bigint(20) unsigned NOT NULL COMMENT '用户ID',
`mobile` varchar(20) NOT NULL DEFAULT '' COMMENT '用户手机号',
`name` varchar(100) NOT NULL DEFAULT '' COMMENT '用户姓名',
`password` varchar(128) NOT NULL DEFAULT '' COMMENT '用户密码',
`is_delete` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除 1:已删除;0:未删除',
PRIMARY KEY (`id`),
) ENGINE=InnoDB COMMENT='用户表';

CREATE TABLE `menu_auth_db`.`tb_user_role` (
`id` bigint(20) unsigned NOT NULL COMMENT '主键',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`role_id` bigint(20) NOT NULL COMMENT '角色ID',
PRIMARY KEY (`id`),
) ENGINE=InnoDB COMMENT='用户角色表';

CREATE TABLE `menu_auth_db`.`tb_role` (
`id` bigint(20) unsigned NOT NULL COMMENT '角色ID',
`name` varchar(100) NOT NULL DEFAULT '' COMMENT '角色名称',
`code` varchar(100) NOT NULL DEFAULT '' COMMENT '角色编码',
`is_delete` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除 1:已删除;0:未删除',
PRIMARY KEY (`id`),
) ENGINE=InnoDB COMMENT='角色表';


CREATE TABLE `menu_auth_db`.`tb_role_menu` (
`id` bigint(20) unsigned NOT NULL COMMENT '主键',
`role_id` bigint(20) NOT NULL COMMENT '角色ID',
`menu_id` bigint(20) NOT NULL COMMENT '菜单ID',
PRIMARY KEY (`id`),
) ENGINE=InnoDB COMMENT='角色菜单表';


CREATE TABLE `menu_auth_db`.`tb_menu` (
`id` bigint(20) NOT NULL COMMENT '菜单ID',
`name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '菜单名称',
`menu_code` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '菜单编码',
`parent_id` bigint(20) DEFAULT NULL COMMENT '父节点',
`node_type` tinyint(4) NOT NULL DEFAULT '1' COMMENT '节点类型,1文件夹,2页面,3按钮',
`icon_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '菜单图标地址',
`sort` int(11) NOT NULL DEFAULT '1' COMMENT '排序号',
`link_url` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '菜单对应的地址',
`level` int(11) NOT NULL DEFAULT '0' COMMENT '菜单层次',
`path` varchar(2500) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '树id的路径,主要用于存放从根节点到当前树的父节点的路径',
`is_delete` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除 1:已删除;0:未删除',
PRIMARY KEY (`id`) USING BTREE,
KEY idx_parent_id (`parent_id`) USING BTREE
) ENGINE=InnoDB COMMENT='菜单表';

02、项目构建


菜单权限模块的数据库设计搞定之后,就可以正式进入系统开发阶段了。


2.1、创建项目


为了快速构建项目,这里采用的是springboot+mybatisPlus框架来快速开发,借助mybatisPlus提供的生成代码器,可以一键生成所需的daoserviceweb层的服务代码,以便帮助我们剩去 CRUD 中重复编程的工作量,内容如下:



CRUD 代码生成完成之后,此时我们就可以编写业务逻辑代码了,相关示例如下!


2.2、菜单功能开发


2.2.1、菜单新增逻辑示例

@Override
public void addMenu(Menu menu) {
//如果插入的当前节点为根节点,parentId指定为0
if(menu.getParentId().longValue() == 0){
menu.setLevel(1);//默认根节点层级为1
menu.setPath(null);//默认根节点路径为空
}else{
Menu parentMenu = baseMapper.selectById(menu.getParentId());
if(parentMenu == null){
throw new CommonException("未查询到对应的父菜单节点");
}
menu.setLevel(parentMenu.getLevel().intValue() + 1);
// 重新设置菜单节点路径,多个用【,】隔开
if(StringUtils.isNotEmpty(parentMenu.getPath())){
menu.setPath(parentMenu.getPath() + "," + parentMenu.getId());
}else{
menu.setPath(parentMenu.getId().toString());
}
}
// 设置菜单ID,可以用发号器来生成
menu.setId(System.currentTimeMillis());
// 将菜单信息插入到数据库
super.save(menu);
}

2.2.2、菜单查询逻辑示例

首先,编写一个视图对象,用于数据展示。


public class MenuVo {

/**
* 主键
*/

private Long id;

/**
* 名称
*/

private String name;

/**
* 菜单编码
*/

private String menuCode;

/**
* 父节点
*/

private Long parentId;

/**
* 节点类型,1文件夹,2页面,3按钮
*/

private Integer nodeType;

/**
* 图标地址
*/

private String iconUrl;

/**
* 排序号
*/

private Integer sort;

/**
* 页面对应的地址
*/

private String linkUrl;

/**
* 层次
*/

private Integer level;

/**
* 树id的路径 整个层次上的路径id,逗号分隔,想要找父节点特别快
*/

private String path;

/**
* 子菜单集合
*/

List childMenu;

// set、get方法等...
}

接着编写菜单查询逻辑,这里需要用到递归算法来封装菜单视图。


@Override
public List queryMenuTree() {
Wrapper queryObj = new QueryWrapper<>().orderByAsc("level","sort");
List
allMenu = super.list(queryObj);
// 0L:表示根节点的父ID
List resultList = transferMenuVo(allMenu, 0L);
return resultList;
}

递归算法,方法实现逻辑如下!


/**
* 封装菜单视图
*
@param allMenu
*
@param parentId
*
@return
*/

private List transferMenuVo(List
allMenu, Long parentId){
List resultList = new ArrayList<>();
if(!CollectionUtils.isEmpty(allMenu)){
for (Menu source : allMenu) {
if(parentId.longValue() == source.getParentId().longValue()){
MenuVo menuVo = new MenuVo();
BeanUtils.copyProperties(source, menuVo);
//递归查询子菜单,并封装信息
List childList = transferMenuVo(allMenu, source.getId());
if(!CollectionUtils.isEmpty(childList)){
menuVo.setChildMenu(childList);
}
resultList.add(menuVo);
}
}
}
return resultList;
}

最后编写一个菜单查询接口,将其响应给客户端。


@RestController
@RequestMapping("/menu")
public class MenuController {

@Autowired
private MenuService menuService;

@PostMapping(value = "/queryMenuTree")
public List queryTreeMenu(){
return menuService.queryMenuTree();
}
}

为了便于演示,这里我们先在数据库中初始化几条数据,最后三条数据指的是按钮类型的菜单,用户真正请求的时候,实际上请求的是这三个功能,内容如下:



queryMenuTree接口发起请求,返回的数据结果如下图:



将返回的数据,通过页面进行渲染之后,结果类似如下图:



2.3、用户权限开发


在上文,我们提到了用户通过角色来关联菜单,因此,很容易想到,用户控制菜单的流程如下:



  • 第一步:用户登陆系统之后,查询当前用户拥有哪些角色;

  • 第二步:再通过角色查询关联的菜单权限点;

  • 第三步:最后将用户拥有的角色名下所有的菜单权限点,封装起来返回给用户;


带着这个思路,我们一起来看看具体的实现过程。


2.3.1、用户权限点查询逻辑示例

首先,编写一个通过用户ID查询菜单的服务,代码示例如下!


@Override
public List queryMenus(Long userId) {
// 第一步:先查询当前用户对应的角色
Wrapper queryUserRoleObj = new QueryWrapper<>().eq("user_id", userId);
List userRoles = userRoleService.list(queryUserRoleObj);
if(!CollectionUtils.isEmpty(userRoles)){
// 第二步:通过角色查询菜单(默认取第一个角色)
Wrapper queryRoleMenuObj = new QueryWrapper<>().eq("role_id", userRoles.get(0).getRoleId());
List roleMenus = roleMenuService.list(queryRoleMenuObj);
if(!CollectionUtils.isEmpty(roleMenus)){
Set menuIds = new HashSet<>();
for (RoleMenu roleMenu : roleMenus) {
menuIds.add(roleMenu.getMenuId());
}
//查询对应的菜单
Wrapper queryMenuObj = new QueryWrapper<>().in("id", new ArrayList<>(menuIds));
List
menus = super.list(queryMenuObj);
if(!CollectionUtils.isEmpty(menus)){
//将菜单下对应的父节点也一并全部查询出来
Set allMenuIds = new HashSet<>();
for (Menu menu : menus) {
allMenuIds.add(menu.getId());
if(StringUtils.isNotEmpty(menu.getPath())){
String[] pathIds = StringUtils.split(",", menu.getPath());
for (String pathId : pathIds) {
allMenuIds.add(Long.valueOf(pathId));
}
}
}
// 第三步:查询对应的所有菜单,并进行封装展示
List
allMenus = super.list(new QueryWrapper().in("id", new ArrayList<>(allMenuIds)));
List resultList = transferMenuVo(allMenus, 0L);
return resultList;
}
}

}
return null;
}

然后,编写一个通过用户ID查询菜单的接口,将数据结果返回给用户,代码示例如下!


@PostMapping(value = "/queryMenus")
public List queryMenus(Long userId){
//查询当前用户下的菜单权限
return menuService.queryMenus(userId);
}

2.4、用户鉴权开发


完成以上的逻辑开发之后,可以实现哪些用户拥有哪些菜单权限点的操作,比如用户【张三】,拥有【用户管理】菜单,那么他只能看到【用户管理】的界面;用户【李四】,用于【角色管理】菜单,同样的,他只能看到【角色管理】的界面,无法看到其他的界面。


但是某些技术人员发生漏洞之后,可能会绕过页面展示逻辑,直接对接口服务发起请求,依然能正常操作,例如利用用户【张三】的账户,操作【角色管理】的数据,这个时候就会发生数据安全隐患的问题。


为此,我们还需要一套用户鉴权的功能,对接口请求进行验证,只有满足要求的才能获取数据。


其中上文提到的菜单编码menuCode就是一个前、后端联系的桥梁。其实所有后端的接口,与前端对应的都是按钮操作,因此我们可以以按钮为基准,实现前后端双向权限控制


以【角色管理-查询】这个为例,前端可以通过菜单编码实现是否展示这个查询按钮,后端可以通过菜单编码来鉴权当前用户是否具备请求接口的权限,实现过程如下!


2.4.1、权限控制逻辑示例

在此,我们采用权限注解+代理拦截器的方式,来实现接口权限的安全验证。


首先,编写一个权限注解CheckPermissions


@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckPermissions {

String value() default "";
}

然后,编写一个代理拦截器,拦截所有被@CheckPermissions注解标注的方法


@Aspect
@Component
public class CheckPermissionsAspect {

@Autowired
private MenuMapper menuMapper;

@Pointcut("@annotation(com.company.project.core.annotation.CheckPermissions)")
public void checkPermissions() {}

@Before("checkPermissions()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
Long userId = null;
// 获取请求参数
Object[] args = joinPoint.getArgs();
Object requestParam = args[0];
// 用户请求参数实体类中的用户ID
if(!Objects.isNull(requestParam)){
// 获取请求对象中属性为【userId】的值
Field field = requestParam.getClass().getDeclaredField("userId");
field.setAccessible(true);
userId = (Long) field.get(parobj);
}
if(!Objects.isNull(userId)){
// 获取方法上有CheckPermissions注解的参数
Class clazz = joinPoint.getTarget().getClass();
String methodName = joinPoint.getSignature().getName();
Class[] parameterTypes = ((MethodSignature)joinPoint.getSignature()).getMethod().getParameterTypes();
// 寻找目标方法
Method method = clazz.getMethod(methodName, parameterTypes);
if(method.getAnnotation(CheckPermissions.class) != null){
// 获取注解上的参数值
CheckPermissions annotation = method.getAnnotation(CheckPermissions.class);
String menuCode = annotation.value();
if (StringUtils.isNotBlank(menuCode)) {
// 通过用户ID、菜单编码查询是否有关联
int count = menuMapper.selectAuthByUserIdAndMenuCode(userId, menuCode);
if(count == 0){
throw new CommonException("接口无访问权限");
}
}
}
}
}
}

2.4.2、鉴权逻辑验证

我们以上文说到的【角色管理-查询】为例,编写一个服务接口来验证一下逻辑的正确性。


首先,编写一个请求实体类RoleDTO,添加userId属性


public class RoleDTO extends Role {

//添加用户ID
private Long userId;

// set、get方法等...
}

其次,编写一个角色查询接口,并在方法上添加@CheckPermissions注解,表示此方法需要鉴权,满足条件的用户才能请求通过。


@RestController
@RequestMapping("/role")
public class RoleController {

private RoleService roleService;

@CheckPermissions(value="roleMgr:list")
@PostMapping(value = "/queryRole")
public List queryRole(RoleDTO roleDTO){
return roleService.list();
}
}

最后,在数据库中初始化相关的数据。例如给用户【张三】分配一个【访客人员】角色,同时这个角色只有【系统配置】、【用户管理】菜单权限。






启动项目,在postman中传入用户【张三】的ID,查询用户具备的菜单权限,只有两个,结果如下:



同时,利用用户【张三】发起【角色管理-查询】操作,提示:接口无访问权限,结果如下:



与预期结果一致!因为没有配置角色查询接口,所以无权访问!


03、小结


最后总结一下,【用户权限控制】功能在实际的软件系统中非常常见,希望本篇的知识能帮助到大家。




作者:潘志的研发笔记
来源:juejin.cn/post/7380283378153914383
收起阅读 »

25k大专前端外包从深圳回武汉能拿多少?

2023 年 08 月我正式从深圳公司离职,从 7 月初开始投武汉的公司,截止 8 月底,2 个月时间有 5 个面试 3 个 offer:一个自研的 22k、两个外包一个 17k,一个 18k。最终选了一家离我比较近的 18k offer,但遗憾的是刚去第一天...
继续阅读 »

2023 年 08 月我正式从深圳公司离职,从 7 月初开始投武汉的公司,截止 8 月底,2 个月时间有 5 个面试 3 个 offer:一个自研的 22k、两个外包一个 17k,一个 18k。最终选了一家离我比较近的 18k offer,但遗憾的是刚去第一天就发现坑太深,还是决定放弃这家公司,目前失业在家,这里和大家聊一聊最近从深圳回武汉找工作的经历。

基本情况

先大致介绍下我的基本情况:大专学历,30+,从 17 年开始做全职前端开发,到现在有 6-7 年了,属于一年工作经验用 5 年的那种,Vue 一把梭,技术一般。进不了中、大厂,只能在外包混混日子。

时间回到两年前,2021 年 8 月前端行情好的时候,我凭运气找到了一家深圳二线互联网公司的前端外包岗位,offer 是 25k * 12,到今年 8 月正好呆了两年左右,为了说明真实性,下面附上我 2022 年度个税 App 收入纳税截图。

2-2022.png

声明

本人在网上冲浪时从未公开过这家我呆了两年的公司,出于薪资保密原则,如果有人认出我,知道我所在的这家公司,还请不要透露公司名字,万分感谢!

另外,为了避免纠纷,后面面试的公司,我都会进行匿名处理,如果有人猜到公司名称,评论还请使用化名,希望大家能理解。

深圳回武汉

从 21 年入职这家公司开始,这两年前端行情越来越差,目前我这个学历、技术水平比较难找到 25k 以上的工作。我有考虑过要不要先苟在这家公司,毕竟这个工作工资还可以,leader、同事、工作氛围都不错。但由于各方面原因,最终还是决定回武汉。

  1. 工作方面:我做的大多是技术需求,做的比较无聊,成就感较低;另外,新需求越来越少,蛋糕就这么大,僧多粥少,发挥空间较小。
  2. 生活方面:我时常在反思,我是不是一个精致的利己主义者?这些年基本就过年回家,回家也呆不了几天,在照顾父母、关心家人这方面我是做的比较差的,如果我只想着自己能不能拿高工资,自己过的是否惬意,我觉得这是很自私的,回武汉离家近可以很好的解决这个问题。

在业务需求少后,部门也有了裁员的消息,我正好在这边快两年了,也想回武汉换个环境。

为了变被动为主动,就在 7 月初开始投武汉的公司了,计划拿到 offer 就离职回去。因为在行业下行周期,越想往上挣扎越累,还不一定有好的结果,不如顺势躺平,好好享受生活。

简历投递面试数据

23 年 6 月 20 号左右,将简历开放,状态修改为在职-看机会。过了一段时间,发现没 hr 联系我,行情确实差了很多,之前简历一开放,一堆 hr 主动找你,这个时候还没主动投。

一直到 23 年 7 月 3 号,我终于修改好了简历,开始投简历。但如下图,简历比较难投出去,需要双方回复才能投。

3-boss-huifu.png

于是我又下载了拉钩、猎聘。拉钩猎聘大部分都可以直接投,但拉钩 20k+ 武汉的岗位很少,猎聘投了很多也没回复,整体还是 BOSS 上面试机会最多,下面是具体数据

App 类型沟通投递面试机会面试通过/Offer
BOSS13186142
猎聘-29411
拉钩-62(投递反馈)00

卡学历问题

我基本把武汉的 20k+ 前端岗位都投了一遍,但基本没有中、大厂都能通过简历筛选。分三种情况

  1. 没有任何回复(最多)
  2. 回复看了简历不合适(个别)
  3. 直接指出学历不符合(个别)

4-xueli.png

虽然我有自考本科+学士学位也没啥用,一般还是至少要统招本科及以上。当然也有可能会是年龄、技术菜、要的工资高等其他因素。

4-2-xueli.png

面试记录

某电商小公司 - 自研 22k(过)

来源:猎聘 App,岗位:中高级前端开发工程师(自研)(14-22k)

2023 年 7 月 10 号,在投了一个星期后,终于有了第一个面试,晚上 19:00 腾讯会议远程面,大概面了一个小时,问的问题不难,比如

  • 先自我介绍
  • 垂直居中有几种方式?
  • flex: 2 有用过吗?多列布局怎么实现?
  • 怎么判断对象为空?
  • 寻找字符串中出现最多的字符怎么实现?
  • 知不知道最新的 url 参数获取的 API?
  • 实现深拷贝
  • 实现 Promise
  • 新版本发布后,怎么用技术手段通知用户刷新页面?
  • 性能优化数据怎么上报、分析?
  • Vue 组件通信方式有哪些,各有什么特点?
  • Vue 项目怎么提高项目性能?举一些例子
  • element ui table 吸顶怎么做,滚动怎么处理等
  • 你有什么想问我的?

然后还问了一些项目问题,能不能加班,因为虽然双休,但周一到周五会有 3 天加班等。基本没有问啥原理性的问题,就是看基础怎么样,能不能干活。

面试第二天,没有消息,我以为挂了,但隔了一天,7 月 12 号,HR 电话二面,我问了我的一些基本情况后,表示可以直接发 offer,确定薪资为 22k,但其中 2.2k 要当做季度绩效发放,说的是一般不犯啥错误都可以拿到。下面是 offer 截图

5-offer-1.png

沟通入职时间定的 8 月 1 号,比较坑的是甲方都同意 7 月底可以走,外包公司这边不同意,要到 8 月中才放我走,合同确实是这样写的,我也不好说啥。

这家公司比较着急,觉得等的时间有点长了,1个月+,风险有点高。我也不能说让别人一直等,只能说,让他们可以先考虑其他候选人,这家公司过了段时间招到人了,这个 offer 就黄了。

(后面回想起来,我可能有点傻,规定是死的,人是活的,应该直接按甲方允许的 7 月底时间来,这样 offer 就没问题了。如果我们公司不让我走,我可以直接走人,就当旷工,直接被开除就行,只是没有离职证明,但工资流水是有的)

武汉某小公司 - 自研 (12-20k)x

来源:BOSS,岗位:前端开发工程师 - 自研(12-20k)14薪

在上面的 22k 这个 offer 时间有冲突的时候,我就意识到这个 offer 有风险,就开始继续投了。

到 23 年 8 月 2 号终于又有了面试机会,一面是笔试,如下图

6-hema.png

有 4 题,最后一题最简单,第 1、2 题忘记了,1、2、3 我都是用递归实现的,3、4 题如下

  1. _.flatten() 实现一个数组打平方法,支持第二个参数(可指定打平层级)
const array = [[0, 1], [2, [3, 4]], [5, 6]];
const result = _.flatten(array);
  1. 菜单数组转换为嵌套树形结构,但示例只有两级
[
{ id: 1, menu: '水果', level: 1 },
{ id: 2, menu: '橘子', level: 2, parentId: 1 }
// ...
]
// 转换为
[
{
id: 1, menu: '水果', level: 1, children: [{ id: 2, menu: '橘子', level: 2, parentId: 1 }]
},
// ...
]

笔试难度一般,主要靠思维,难度比 leetcode 算法题低,算是过了。

二面是 8 月 7 号电话面,19:00 - 20:00 一个小时左右,大部分问题都忘记了,模糊记得部分问题

  • 先自我介绍
  • 把之前的笔试题一题一题拿出来讲实现思路。
  • 对象的继承有哪几种?
  • TS 用的多吗?
  • 工作中解决的最有成就感的事?
  • vue3 在某些场景比 vue2 性能更低,为什么会这样?
  • 在团队协作时,有遇到过什么问题吗,如果有冲突你会怎么做
  • 你有什么想问我的?

另外面试小哥对我之前有两家半年左右的工作经历比较在意,问了很多之前公司的细节,因为他说之前有面试过的最后背调没通过,所以要问清楚。我的简历写的很真实,基本没有水分,是什么就是什么。

他最后透露,可能就算他可以过,但 HR 那边可能过不了,不知道是我跳槽太频繁还是啥,总之后面基本没消息了,这个算是挂了。

某上海武汉分公司 - 自研(18-23k)x

来源:BOSS,岗位:前端开发 自研(18-23k)

上次面试的挂了之后,继续投,但没面试机会,后面又忙搬家、邮寄东西,回武汉,找房子等,中间大概用了一个多星期。

在 8 月 18 号终于又有了一个自研的面试, 15:40 腾讯会议线上一面 - 技术面,上海那边的开发负责面试,问了一些问题,比较普通,我现在毫无印象。

一面过了,在 8 月 22 日,13:00 二面(现场面),公司办公地点在武昌火车站地铁口,刚开始觉得还不错,但一进去,一个开发都没有,就 1 个人,直接无语...... 武汉算是分部,那个人还不懂技术,和我吹了一下公司怎么怎么厉害,先是做了一份笔试题(比较基础)比如

  • 3 种方式实现顶部导航+左侧菜单+右侧主内容区域布局
  • jwt 鉴权逻辑
  • vue 数组下标改值,响应式丢失、为什么

7-hangshu.png

然后那个人拍了我写的笔试题,让上海那边的人看,说是做的不错。再视频连线进行面试,大致问了一些基础问题,然后坑的地方来了。我之前待过的公司,一个一个问我离职原因。。。。。。

然后就是副总面,问我有没有做过异形屏的适配,有没有写过绘制、渲染逻辑,我。。。。。。然后又问了我好几个假大空的问题,我一脸懵逼,比如一个公司呆 8 年和 8 年每年换一家公司你觉得哪种好。

后面就是回去等消息了,然后就没有然后了。。。。。

某金融公司 - 外包 17k(过)

来源:BOSS,岗位:前端开发 - 外包 17k

和上面那个公司同一时间段,在 8 月 18 号也进行了这家公司的腾讯会议一面

一面比较简单,大致为了下工作经历,重点问了下低代码、怎么动态加载渲染一个组件,底层怎么实现?面试时间比较短,有点仓促

8 月 21 号二面,大致问了一些问题后,还是追问低代码方面的问题,组件级别、可以内嵌到其他指定页面的这种低代码 sdk 封装怎么做?他们是想招个会低代码,有过 sdk 封装经验的。我之前工作中有做过组件库,封装过百万用户级别的小程序 sdk、也做过功能引导、错误上报等 sdk,还自己实现过多个 npm 包轮子,算是勉强符合他们的要求。

二面过了后,开始谈薪资,17k,基本不加班,8 月 23 号三面笔试(类似走过场),有题库,刷一下就没问题,通过就发邮件 offer 了。

8-zhengquan.png

这家公司过了,但我没有接轻易接 offer,而是让 HR 等第二天中午我的反馈,我不想接了别人 offer 又不去。这家公司的 HR 比较好、很热心积极。

主要有以下几个原因

  1. 后面还有一个 18k 的也是同一天二面,且面试体验好,大概率过了,只等确定 offer。
  2. 这家比较远,在花山,而后面一家离我比较近
  3. 这家试用期打折,下面一家不打折。

最终拒了这家 offer,因为下面要讲的这家 offer 下来了,前方高能预警,后面这家公司巨坑、后悔拒了这家。。。。

某互联网公司 - 外包 18k(过)

来源:BOSS,岗位:前端开发(外包)18k

和上面那家几乎同一时间,这家公司也进行了两轮面试

一面,腾讯会议,从 3-4 个 UI 中,选一个题来实现,30 分钟,就是平常干活画 UI,难度不大,面试官是个声音好听的妹子。

二面,腾讯会议,结对编程,面试官出题,我描述实现,面试官写代码,包括

  1. 一个简单的需要使用 Promise 应用题
  2. 运行一个 vue 项目,vue2 写法改 vue3 写法,封装一个计时器组件,组件加 props,组件加插槽等

面试体验真的很好,18k offer 下来后,果断选择了这家离我近的公司。

9-offer-3.png

但没想到的是,入职第一天发现这家公司管理问题很大

  1. 开发环境差,只能用网页版的 vscode,除了要配置 host 外,还有配置端口映射,配置稍微有问题就运行不起来,体验较差。
  2. 沟通太依赖线上,武汉这边基本是xx一线城市那边的产品、UI、开发分配任务给这边开发,沟通成本非常高。
  3. 加班问题,说的是早 9 晚 6,但他们自研一般下班这个点可能会去吃个饭, 然后回来加班,git log 看了下提交记录,不少是 20:00 之后的,还有 21 点、22 点之后的.... 如果真融入这个团队,不加班我是不信的。

从面试体验、沟通来看,这里的开发人员是优秀的,但实际入职却发现环境、氛围差的情况,我只能把这种问题归纳到管理上了。

第一天基本没干活就是配置环境,但这个氛围,我真的接受不了,后面就果断放弃这家公司了。

武汉找工作经验总结

上面我大致描述了从 7 月初到 8 月底的简历投递、面试经历。主要是面试少,实际面试通过率为 60%。下面是一些总结

  • 投递简历时段最好是周一到周三上午 8-9 点,回复、面试机会较多,周五到周天基本没反应。
  • 武汉原理性问的不多,主要还是能干活,比较需要多面手,就是什么都会的,比如 WebGL, Three.js,uni-app 等
  • 一定要问清楚、开发环境、加班问题,不要不好意思,能找自研就尽量找自研。
  • 不要听 HR 或者面试官怎么说,而是自己通过行业、所做的业务去判断是否有坑。

完结撒花,如果觉得内容对您有帮助,那就点个免费的赞吧~~

另外最近有和我一样在找工作的小伙伴吗?你们有遇到过什么坑吗?欢迎在评论区讨论~~~


作者:dev_zuo
来源:juejin.cn/post/7275225948453568552
收起阅读 »

Alpha系统联结大数据、GPT两大功能,助力律所管理降本增效

如何通过AI工具实现法律服务的提质增效,是每一位法律人都积极关注和学习的课题。但从AI技术火爆一下,法律人一直缺乏系统、实用的学习资料,来掌握在法律场景下AI的使用技巧。今年5月,iCourt携手贵阳律协大数据与人工智能专业委员会,联合举办了《人工智能助力律师...
继续阅读 »

如何通过AI工具实现法律服务的提质增效,是每一位法律人都积极关注和学习的课题。但从AI技术火爆一下,法律人一直缺乏系统、实用的学习资料,来掌握在法律场景下AI的使用技巧。

alphagpt

今年5月,iCourt携手贵阳律协大数据与人工智能专业委员会,联合举办了《人工智能助力律师行业高质量发展巡回讲座》,超过100家律所的律师参与活动。

讲座上, iCourt AIGC 研究员、AlphaGPT产品研发负责人兰洋,为贵州律协的律师们讲解了AI技术在法律领域的应用原理、AI技术赋能法律服务发展九大典型场景和业务实操、AI赋能法律服务的发展趋势,并强调了AI时代下,律师需要掌握的关键技能,以期帮助律师们更好地抓住技术变革带来的发展机遇。

iCourt很早就开始了法律发数据与法律AI工具的研发应用,旗下智能法律操作系统Alpha、法律人专属AI助手AlphaGPT,在法律科技日益繁荣的市场上始终走在前列。

alpha

Alpha系统的大数据功能整合了超过1.6亿的案例和超过410万的法规,通过与司法观点库、类案同判库、实务文章库等多数据库协同穿透检索,为律师办案提供精确实用的法律信息。大数据功能不仅支持最高26维度的检索,还支持标签选取、模糊检索、AI检索等多种检索方式,解决律师在任意工作场景下的检索需求。 

alphagpt

值得一提的是,Alpha系统还是仅有的一个集法律大数据、人工智能工具、数智化办公工具、律所管理与团队协同工具于一体的法律智能操作系统。Alpha系统不仅可以帮助律师实现工作效率、工作效果的双重提升,还可以助力律所推动一体化改革,真正地实现降本增效。Alpha系统中涵盖的利冲管理、知识管理、客户管理、财务管理、审批管理和人事行政管理等功能模块,涵盖律所管理与发展的方方面面,是律所发展、转型、升级的的必备工具。

而法律人专属AI助手AlphaGPT,可以帮助律师在案情分析、合同审查、文书写作、法律咨询、文件阅读、法律检索六大场景实现提质增效。例如,依托Alpha系统的法律大数据,AlphaGPT可以在短时间内实现案情智能分析,像律师一样思考,输出客观的法律服务意见书,还可以分钟级时间内实现合同风险与主体风险的一建审查。此外,AlphaGPT可以在短时间内实现文本的主题概括和核心提炼,还可以提供文本校对、润色、翻译等处理操作,大大节省了时间成本。

AI时代下,法律人应当抓住科技发展带来的机遇风口,拥抱AI等新兴技术,积极应用到法律服务领域当中,以实现自身的提质增效和推动行业的快速发展。

AlphaGPT官网入口:https://icourt.cc/product/alphaGPT

收起阅读 »

律所管理OA系统推荐,Alpha法律智能操作系统荣登榜首

随着“一体化”日渐成为律所改革的重要抓手,一个好的线上一体化管理工具(律所管理OA系统)就成为了几乎所有律所改革的必需品。Alpha法律智能操作系统集律所利冲管理、知识管理、客户管理、财务管理、审批管理和人事行政管理于一体,涵盖律所管理的方方面面,是律所管理O...
继续阅读 »

随着“一体化”日渐成为律所改革的重要抓手,一个好的线上一体化管理工具(律所管理OA系统)就成为了几乎所有律所改革的必需品。Alpha法律智能操作系统集律所利冲管理、知识管理、客户管理、财务管理、审批管理和人事行政管理于一体,涵盖律所管理的方方面面,是律所管理OA系统中的代表性产品。目前,Alpha系统已帮助到1.5万家律所提高办案能力与质效。

经过调研与精确设计,Alpha系统以科学的工作流程为基准,以解决律所管理痛点问题为目标,进一步完善功能设置。其中,案件管理、知识管理与行政事务管理广受好评,这是律所一体化建设中不可缺少的三大环节。

alpha

据调研,律所管理的痛点之一是办案过程难管理,从而导致人员调配、案件跟进等徒增风险。对此,Alpha系统的办案件进程管理功能将律师办案过程线上化,通过项目模板将一个案件的立项、签约、办案、开庭、结案等全流程进行统追踪记录,极大地帮助管理人更好地对案件进行监察跟进。Alpha系统的律所OA打通了企业微信、钉钉、飞书等办公软件,可以自动创建工作群,助力开展团队协作,实时提醒律师待办任务,降低工作出错率。

此外,律所一体化的过程中,知识管理能否做好直接关系到律所能否长期高效地运行,决定着管理模式的成败。好的知识管理方案可以促进律所内部的优秀经验高效复制、知识成果快速共享,进而整体提升律师的业务能力。Alpha系统为律所打造了电子化、协同式的知识管理功能,提供超1.5亿案例数据,提供全面、精细的数据检索库;同时,基于多项数据库的组合应用,系统还支持5秒内自动导出客户尽调报告与法律风险分析报告,帮助律师快速了解企业客户基本信息与可能存在的法律风险,高效置顶办案策略。

alpha

针对行政事务管理,Alpha系统集成了律所OA、财务统计分析、绩效管理、审批管理等方面,促进行政统一管理,有力帮助律所降本增效。Alpha系统的自定义审批引擎支持内务外勤分条件高效审批,同时提供全自动利冲检索,做好案涉合同、发票、收款等信息的记录与审批,保障项目信息高效同步流转,实现业务数据和财务数据的统一管理,真正做到了省时省力。

Alpha是目前唯一一个将大数据,律所OA,人工智能相结合的律所管理软件,在律所一体化改革与建设上发挥巨大作用,助力律所实现整体创收提升。

Alpha系统官网入口:https://www.icourt.cc/product/alpha

收起阅读 »

Go 再次讨论 catch error 模型,官方回应现状

大家好,我是煎鱼。 最近社区的同学和 Go 官方又因为错误处理的提案屡屡被否,发生了一些小的摩擦。也非常难得的看到核心团队成员首次表达了目前的态度和情况。 基于此,我们今天进行该内容分享。紧跟 Go 官方最新进展。 快速背景 Go 的错误处理机制,主要是依赖于...
继续阅读 »

大家好,我是煎鱼。


最近社区的同学和 Go 官方又因为错误处理的提案屡屡被否,发生了一些小的摩擦。也非常难得的看到核心团队成员首次表达了目前的态度和情况。


基于此,我们今天进行该内容分享。紧跟 Go 官方最新进展。


快速背景


Go 的错误处理机制,主要是依赖于 if err != nil 的方式。因此在对函数做一定的封装后。


代码会呈现出以下样子:



jy1, err := Foo1()
if err != nil {
return err
}
jy2, err := Foo2()
if err != nil {
return err
}
err := Foo3()
if err != nil {
return err
}
...

有部分开发者会认为这比较的丑陋、混乱且难以阅读。因此 Go 错误处理的优化,也是社区里一直反复提及和提出的领域。饱受各类争议。


新提案:追求类似 try-catch


最近一位国内的同学 @xiaokentrl 提了个类似 try catch error 的新提案,试图用魔法打败魔法。



原作者给出的提案内容是:


1、新增环境变量做开关:


ERROR_SINGLE = TRUE   //error_single = true

2、使用特定标识符来做 try-catch:


Demo1 单行错误处理:


//Single-line error handling
file, err := os.Create("abc.txt") @ return nil , err
defer file.Close()

Demo2 多行错误处理:


func main() {
//Multiline error handling
:@

file, err:= os.Open("abc.txt")
defer file.Close()

buf := make([]byte, 1024)
_, err2 := file.Read(buf)

@ err | err2 return ...
}

主要的变化内容是:利用标签 @ 添加一个类似 try-catch 的代码区块,并添加运算符和相关错误处理的联动规则。


这个提案本身,其实就是以往讲到的 goto error 和 check/with 这种类似 try-catch 的模式。


当然非常快的就遭到了 Go 核心团队的反对:



@Ian Lance Taylor 表示:由于很难处理声明和应用,如果一个标签的作用域中还有其他变量,就不能使用 goto。


新的争端:官方你行你上


社区中有同学看到这一次次被否的错误处理和关联提案们,深感无奈和无语。他发出了以下的质疑:


“为什么不让 Ian Lance Taylor 和/或 Go 核心团队的其他成员提出改进的错误处理框架的初始原型,然后让 Go 社区参与进来,为其最终形式做出贡献呢?Go 中的泛型正是这样发展到现在的。


如果我们等待 Go 社区提出最初的原型,我认为我们将永远不会有改进的 Go 错误处理框架,至少在未来几年内不会。”


但其实很可惜,因为人家真干过。


Go 核心团队是有主动提出过错误处理的优化提案的,提案名为《Proposal: A built-in Go error check function, try》,快速讲一下。


以前的代码:


f, err := os.Open(filename)
if err != nil {
return …, err // zero values for other results, if any
}

应用该提案后的新代码:


f := try(os.Open(filename))

try 只能在本身返回错误结果的函数中使用,且该结果必须是外层函数的最后一个结果参数。


不过很遗憾,该官方提案,喜提有史以来被否决最多的提案 TOP1:



最终该提案也由于形形色色的意见,最终凉了。感觉也给 Go 核心团队泼了一盆凉水,因为这是继 check/handle 后的 try,到目前也没有新的官方提案了。


Go 官方回应


本次提及的新提案下,大家的交流愈演愈烈,有种认为就是 Go 核心团队故意不让错误处理得到更好的改善。


此时 Go 核心团队的元老之一 @Ian Lance Taylor 站出来发声,诠释了目前 Go 团队对此的态度。这也是首次。


具体内容如下:



“我们愿意考虑一个有良好社区支持的好的错误处理提案。


不幸的是,我很遗憾地说,基本上所有新的错误处理提案都不好,也没有社区支持。例如,这个提案有 10 个反对票,没有赞成票。我当然会鼓励人们在广泛使用这门语言之前,避免提交错误处理提案。


我还鼓励人们审查早期的提案。它们在这里:github.com/golang/go/i… 。目前已有 183 个并在不断增加。


我自己阅读了每一个。重要的是,请记住,对已被否决提案的微调的新提案也几乎肯定也会被否决。


并且请记住,我们只会接受一个与现有语言契合良好的提案。例如:这个提案中使用了一个神奇的 @ 符号,这完全不像现有语言中的任何其他东西。


Go 团队可能会在适当的时候提出一个新的错误处理提案。然而,正如其他人所说,我们最好的想法被社区认为是不可接受的。而且有大量的 Go 程序员对现状表示满意。”


总结


目前 Go 错误处理的情况和困境是比较明确的,很多社区同学会基于以往已经被否决的旧提案上进行不断的微改,再不断提交。


现阶段都是被全面否定的,因为即使做了微调,也无法改变提案本身的核心思想。


而 Go 官方自己提出的 check/handle 和 try 提案,在社区中也被广大的网友否决了。还获得了史上最多人否决的提案的位置。


现阶段来看,未来 1-3 年内在错误处理的优化上仍然会继续僵持。



作者:煎鱼eddycjy
来源:juejin.cn/post/7381741857708752905
收起阅读 »

进程还在,JSF接口不干活了,这你敢信?

1、问题背景: 应用在配合R2m升级redis版本的过程中,上游反馈调用接口报错,RpcException:[Biz thread pool of provider has been exhausted],通过监控系统和日志系统定位到现象只出现在一两个节点,并...
继续阅读 »

1、问题背景:


应用在配合R2m升级redis版本的过程中,上游反馈调用接口报错,RpcException:[Biz thread pool of provider has been exhausted],通过监控系统和日志系统定位到现象只出现在一两个节点,并持续出现。第一时间通过JSF将有问题的节点下线,保留现场,业务恢复。


报错日志如下:


24-03-13 02:21:20.188 [JSF-SEV-WORKER-57-T-5] ERROR BaseServerHandler - handlerRequest error msg:[JSF-23003]Biz thread pool of provider has been exhausted, the server port is 22003
24-03-13 02:21:20.658 [JSF-SEV-WORKER-57-T-5] WARN BusinessPool - [JSF-23002]Task:com.alibaba.ttl.TtlRunnable - com.jd.jsf.gd.server.JSFTask@0 has been reject for ThreadPool exhausted! pool:80, active:80, queue:300, taskcnt: 1067777

2、排查步骤:


从现象开始推测原因,系统启动时,会给JSF线程池分配固定的大小,当线程都在工作的时,外部流量又打进来,那么会没有线程去处理请求,此时会有上述的异常。那么JSF线程在干什么呢?


1)借助SGM打印栈信息


2)分析栈信息


可以用在线分析工具:spotify.github.io/threaddump-…


2.1)分析线程状态


通过工具可以定位到JSF线程大部分卡在JedisClusterInfoCache#getSlaveOfSlotFromDc方法,如图:






























2.2)分析线程夯住的方法


getSlaveOfSlotFromDc在方法入口就需要获取读锁,同时在全局变量声明了读锁和写锁:
















此时对问题有一个大体的了解,大概推测:getSlaveOfSlotFromDc是获取redis连接池,该方法入口处需要获取读锁,由于读锁之间不会互斥,所以猜测有业务获取到写锁后没有释放。同时读锁没有设置超时时间,所以导致杰夫线程处理业务时卡在获取读锁处,无法释放。


2.3)从业务的角度分析持有写锁的逻辑


向中间件研发寻求帮助,经过排查,定位到有个更新拓扑的定时任务,执行时会先获取写锁,根据该消息,定位到任务的栈信息:









代码截图:









图1









图2









图3


从日志验证:日志只打印更新拓扑的日志,没有打印更新成功的日志,且02:20分以后r2m-topo-updater就不在打印日志









2.4)深入挖掘原因


虽然现象已经可以推测出来,但是对问题的原因还是百思不得其解,难道parallelStream().forEach存在bug?难道有远程请求,没有设置超时时间?...


经过查找资料确认,如果没有指定,那么parallelStream().forEach会使用ForkJoinPool.commonPool这个默认的线程池去处理任务,该线程池默认设置(容器核心数-1)个活跃线程。同时caffeine数据过期后会异步刷新数据,如果没有指定线程池,它默认也会使用ForkJoinPool.commonPool()来执行异步线程。那么就有概率出现获取到写锁的线程无法获取执行权,获取执行权的线程无法获取到读锁。









2.5)验证


3个ForkJoinPool.commonPool-worker的确都夯在获取redis连接处,线程池的活跃线程都在等待读锁。









本地caffeine缓存没有设置自定义线程池









topo-updater夯在foreach业务处理逻辑中









3.复盘


1)此问题在特定的使用场景下才会小概率出现,非常感谢中间件团队一起协助定位问题,后续也将异步更新拓扑改为同步处理。


2)Java提供了很多异步处理的能力,但是异常处理也代表需要开启线程或者使用共用的线程池,也需要注意。


3)做好监控,能第一时间发现问题并处理问题。


作者:京东科技 田蒙


来源:京东云开发者社区


作者:京东云开发者
来源:juejin.cn/post/7379831020496715813
收起阅读 »

通过代码实现 pdf 文件自动盖章

序言在数字化时代,电子文档的安全性和真实性越来越受到重视。电子印章作为一种数字化的身份验证工具,已经成为确保文档合法性和不可篡改性的重要手段。然而,传统的电子印章往往需要人工操作,不仅效率低下,而且在处理大量文件时容易出错。为了解决这一问题,自动化地给PDF文...
继续阅读 »

序言

在数字化时代,电子文档的安全性和真实性越来越受到重视。电子印章作为一种数字化的身份验证工具,已经成为确保文档合法性和不可篡改性的重要手段。然而,传统的电子印章往往需要人工操作,不仅效率低下,而且在处理大量文件时容易出错。为了解决这一问题,自动化地给PDF文件盖电子章成为了一个迫切的需求。本文将详细介绍,如何通过 .net 程序实现这一功能,废话不多说,步入正题

Nuget 包

本文的核心包为:

  • iTextSharp,用它来操作 pdf 文件非常方便,具体的用法这里不多赘述,请参考官网
  • DynamicExpresso,一个非常好用的动态表达式解析工具包

Include="DynamicExpresso.Core" Version="2.16.1" />
Include="iTextSharp" Version="5.5.13.3" />
Include="Newtonsoft.Json" Version="13.0.3" />

素材准备

本案例用到的素材包括:用于测试的 pdf 文件一个,模拟电子章图片一张,以及盖章配置文件,文件内容如下:

[
{
"SignType" : "image",//素材类型,image表示图片素材,text 表示文本素材
"LastPage" : true,//是否仅最后一页盖章
"ImageUrl" : "https://xxxxxxxx",//图片素材的下载链接
"FileName" : "sign.png",//图片素材文件名称
"ScalePercent" : 20,//图片缩放百分比,100 表示不缩放
"Opacity" : 0.6,//图片透明度,1 表示不透明
"LocationX" : "(input.Width/10)*6",//图片素材的绝对位置表达式,(0,0) 表示左下角
"LocationY" : "input.Height/23 +20",//input.With 和 input.Height 代表 pdf 文件的宽度及高度
"Rotation" : 0//素材的旋转角度
},
{
"SignType" : "text",
"LastPage" : true,
"LocationX" : "(input.Width/10)*6+85",
"LocationY" : "input.Height/23 ",
"Rotation" : 0,
"FontSize" : 20,
"Opacity" : 0.6,
"FontColor" : {//文本素材的字体颜色值
"R" : 255,
"G" : 0,
"B" : 0
},
"Text" : "input.Date"//文本素材的表达式,也可以直接写固定文本
}
]

说明:

  1. 这里之所以设计为一个数组,是因为可能有些场景下,不仅需要盖电子章,还需要自动签上日期,比如本案例。
  2. 签署位置可以自定义,坐标(0,0)代表的是左下角,x 变大即表示横向右移,y 变大表示纵向上移。
  3. 配置文件存储,我这里是把配置文件放在了本地,当然你可以存储在任何地方,比如 MongoDB等。

代码展示

本案例采用的是 .net7.0,当然 .net6及以后都是可以的。

  1. 配置文件类,与上一步的 json 配置文件对应
namespace PdfSign;

public class SignOpt
{
public string SignType { get; set; }
public bool LastPage { get; set; }
public string ImageUrl { get; set; }
public string FileName { get; set; }
public int ScalePercent { get; set; } = 50;
public string LocationX { get; set; }
public string LocationY { get; set; }
public float LocationYf { get; set; }
public float Rotation { get; set; } = 0;
public int FontSize { get; set; }
public float Opacity { get; set; }
public RBGColor FontColor { get; set; }
public string? Text { get; set; }

public record RBGColor(int R, int G, int B);
}
  1. pdf 签署方法
using System.Dynamic;
using DynamicExpresso;
using iTextSharp.text;
using iTextSharp.text.pdf;
using Newtonsoft.Json.Linq;

namespace PdfSign;

public class SignService
{
public static string PdfSign(List signOpts, string pdfName)
{
var beforeFileName = pdfName; //签名之前文件名
var afterFileName = pdfName + "_sign"; //签名之后文件名
var idx = 0;
foreach (var opt in signOpts)
{
//创建盖章后生成pdf
var outputPdfStream =
new FileStream(afterFileName + ".pdf", FileMode.Create, FileAccess.Write, FileShare.);
//读取原有pdf
var pdfReader = new PdfReader(beforeFileName + ".pdf");
var pdfStamper = new PdfStamper(pdfReader, outputPdfStream);
//读取页数
var pdfPageSize = pdfReader.NumberOfPages;
//读取pdf文件第一页尺寸,得到 With 和 Height
var size = pdfReader.GetPageSize(1);
//通过表达式计算出签署的绝对坐标
var locationX = Eval(opt.LocationX, new { size.Width, size.Height });
var locationY = Eval(opt.LocationY, new { size.Width, size.Height });

if (opt.LastPage)
{
//盖章在最后一页
var pdfContentByte = pdfStamper.GetOverContent(pdfPageSize);
var gs = new PdfGState
{
FillOpacity = opt.Opacity
};
pdfContentByte.SetGState(gs);
switch (opt.SignType.ToLower())
{
case "image":
//获取图片
var image = Image.GetInstance(opt.FileName);
//设置图片比例
image.ScalePercent(opt.ScalePercent);
//设置图片的绝对位置,位置偏移方向为:左到右,下到上
image.SetAbsolutePosition(locationX, locationY);
//图片添加到文档
pdfContentByte.AddImage(image);
break;
case "text":
if (string.IsNullOrWhiteSpace(opt.Text))
continue;
var font = BaseFont.CreateFont();
var text = Eval(opt.Text, new { Date = DateTime.Now.ToString("yyyy-MM-dd") });
//开始写入文本
pdfContentByte.BeginText();
pdfContentByte.SetColorFill(
new BaseColor(opt.FontColor.R, opt.FontColor.G, opt.FontColor.B));
pdfContentByte.SetFontAndSize(font, opt.FontSize);
pdfContentByte.SetTextMatrix(0, 0);
pdfContentByte.ShowTextAligned(Element.ALIGN_CENTER, text,
locationX, locationY, opt.Rotation);

pdfContentByte.EndText();
break;
}
}

pdfStamper.Close();
pdfReader.Close();
idx++;
if (idx >= signOpts.Count) continue;
//文件名重新赋值
beforeFileName = afterFileName;
afterFileName += "_sign";
}

return afterFileName + ".pdf";
}

//计算动态表达式的值
public static T? Eval(string expr, object context)
{
if (string.IsNullOrWhiteSpace(expr))
return default;

var target = new Interpreter();
var input = JObject.FromObject(context);

target.SetVariable("input", input.ToObject());
return target.Eval(expr);
}
}
  1. 测试调用
using Newtonsoft.Json;
using PdfSign;

//读取签名所需配置文件
var signOpts = await GetSignOpt();

if (signOpts != null && signOpts.Any())
{
//执行 pdf 文件盖章
var signFileName= SignService.PdfSign(signOpts, "test");
}

//读取配置文件
static async Task<List<SignOpt>?> GetSignOpt()
{
var strSign = await File.ReadAllTextAsync("cfg.json");
return JsonConvert.DeserializeObject<List<SignOpt>>(strSign);
}
  1. 效果展示
    原 pdf 文件如下图: image.png 最终效果如下图: image.png

结束语

随着本文的深入探讨,我们共同经历了一个完整的旅程,从理解电子印章的重要性到实现一个自动化的.NET程序,用于在PDF文件上高效、准确地加盖电子章。我们不仅学习了.NET环境下处理PDF文件的技术细节,还掌握了如何将电子印章整合到我们的应用程序中,以实现自动化的文档认证过程。


作者:架构师小任
来源:juejin.cn/post/7377643248187080715

收起阅读 »

MySQL的 where 1=1会不会影响性能?看完官方文档就悟了!

在日常业务开发中,会通过使用where 1=1来简化动态 SQL语句的拼接,有人说where 1=1会影响性能,也有人说不会,到底会不会影响性能?本文将从 MySQL的官方资料来进行分析。 动态拼接 SQL的方法 在 Mybatis中,动态拼接 SQL最常用的...
继续阅读 »

在日常业务开发中,会通过使用where 1=1来简化动态 SQL语句的拼接,有人说where 1=1会影响性能,也有人说不会,到底会不会影响性能?本文将从 MySQL的官方资料来进行分析。


动态拼接 SQL的方法


在 Mybatis中,动态拼接 SQL最常用的两种方式:使用 where 1=1 和 使用标签。


使用where 1=1


使用过 iBATIS的小伙伴应该都知道:在 iBATIS中没有标签,动态 SQL的处理相对较为原始和复杂,因此使用where 1=1这种写法的用户很大一部分是还在使用 iBATIS 或者是从 iBATIS过度到 Mybatis。


如下示例,通过where 1=1来动态拼接有效的 if语句:


<select id="" parameterType = "">
SELECT * FROM user
WHERE 1=1
<if test="name != null and name != ''">
AND name = #{name}
</if>
<if test="age != null ">
AND age = #{age }
</if>
</select>

使用标签


Mybatis提供了标签,标签只有在至少一个 if条件有值的情况下才去生成 where子句,若 AND或 OR前没有有效语句,where元素会将它们去除,也就是说,如果 Mybatis通过标签动态生成的语句为where AND name = '111',最终会被优化为where name = '111'


标签使用示例如下:


<select id="" parameterType = "">
SELECT * FROM user
<where>
<if test="name != null and name != ''">
AND name = #{name}
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>

标签是在 MyBatis中引入的,所以,很多一开始就使用 MyBatis的用户对这个标签使用的比较多。


性能影响


where 1=1到底会不会影响性能?我们可以先看一个具体的例子:



说明:示例基于 MySQL 8.0.30



可以使用如下指令查看 MySQL版本:


SELECT VERSION();

image.png


场景:基于一张拥有 100多万条数据的user表,根据name进行查询,


查看表结构和表的总数据,如下图:


image.png


image.png


下面,通过执行两条 SQL查询语句(一条带有 1=1):


select * from user where name = 'name-96d1b3ce-1a24-4d47-b686-6f9c6940f5f6';
select * from user where 1=1 and name = 'name-f692472e-40de-4053-9498-54b9800e9fb1';

image.png


对比两条 SQL执行的结果,可以发现它们消耗的时间几乎相同,因此,看起来where 1=1对整体的性能似乎并不影响。


为了排除一次查询不具有代表性,我们分别对两条 SQL语句查询 100遍,然后计算平均值:


SET PROFILING = 1;
DO SLEEP(0.001); -- 确保每次查询之间有足够时间间隔

SET @count = 0;
WHILE @count < 100 DO
select * from user where name = 'name-96d1b3ce-1a24-4d47-b686-6f9c6940f5f6';
-- or
select * from user where 1=1 and name = 'name-f692472e-40de-4053-9498-54b9800e9fb1';
SET @count = @count + 1;
END WHILE;

SHOW PROFILES;

两条 SQL分别执行 100次后,最终也发现它们的平均值几乎相同,因此,上述示例似乎证明了 where 1=1 对整体的性能并没有不影响。


为什么没有影响?是不是 MySQL对 1=1进行了优化?


为了证明猜想,我们借助show warnings命令来查看信息,在 MySQL中,show warnings命令用于显示最近执行的 SQL语句产生的警告、错误或通知信息。它可以帮助我们了解语句执行过程中的问题。如下示例:


explain select * from user where 1=1 and name = 'name-f692472e-40de-4053-9498-54b9800e9fb1';
show warnings;

image.png


将上述示例的 warnings信息摘出来如下:


/* select#1 */ select `yuanjava`.`user`.`id` AS `id`,
`yuanjava`.`user`.`name` AS `name`,
`yuanjava`.`user`.`age` AS `age`,
`yuanjava`.`user`.`sex` AS `sex`,
`yuanjava`.`user`.`created_at` AS `created_at`
from `yuanjava`.`user`
where (`yuanjava`.`user`.`name` = 'name-f692472e-40de-4053-9498-54b9800e9fb1')

从 warnings信息可以看出:1=1已经被查询优化器优化掉,因此,对整体的性能影响并不大。


那么,有没有 MySQL的官方资料可以佐证 where 1=1确实被优化了?


答案:有!MySQL有一种 Constant-Folding Optimization(常量折叠优化)的功能。


Constant-Folding Optimization


MySQL的优化器具有一项称为 Constant-Folding Optimization(常量折叠优化)的功能,可以从查询中消除重言式表达式。Constant-Folding Optimization 是一种编译器的优化技术,用于优化编译时计算表达式的常量部分,从而减少运行时的计算量,换句话说:Constant-Folding Optimization 是发生在编译期,而不是引擎执行期间。


对于上述表达的"重言式表达式"又是什么呢?


重言式


重言式(Tautology )又称为永真式,它的汉语拼音为:[Chóng yán shì],是逻辑学的名词。命题公式中有一类重言式,如果一个公式,对于它的任一解释下其真值都为真,就称为重言式(永真式)。


其实,重言式在计算机领域也具有重要应用,比如"重言式表达式"(Tautological expression),它指的是那些总是为真的表达式或逻辑条件。


在 SQL查询中,重言式表达式是指无论在什么情况下,结果永远为真,它们通常会被优化器识别并优化掉,以提高查询效率。例如,如果 where中包含 1=1 或 A=A 这种重言式表达式,它们就会被优化器移除,因为对查询结果没有实际影响。如下两个示例:


SELECT * from user where 1=1 and name = 'xxx';
-- 被优化成
SELECT * from user where name = 'xxx'

SELECT id, name, salary * (1 + 0.05 * 2) AS real_salary FROM employees;
-- 优化成(1 + 0.05 * 2 被优化成 1.1)
SELECT id, name, salary * 1.1 AS real_salary FROM employees;

另外,通过下面 MySQL架构示意图可以看出:优化器是属于 MySQL的 Server层,因此,Constant-Folding Optimization功能支持受 MySQL Server的版本影响。


image.png


查阅了 MySQL的官方资料,Constant-Folding Optimization 从 MySQL5.7版本开始引入,至于 MySQL5.7以前的版本是否具备这个功能,还有待考证。


如何选择?


where 1=1 标签 两种方案,该如何选择?



  • 如果 MySQL Server版本大于等于 5.7,两个随便选,或者根据团队的要求来选;

  • 如果 MySQL Server版本小于 5.7,假如使用的是 MyBatis,建议使用 标签,如果使用的还是比较老的 iBATIS,只能使用where 1=1

  • 如果 MySQL Server版本小于 5.7,建议升升级



信息补充:2009年5月,iBATIS从 2.0版本开始更名为 MyBatis, 标签最早出现在MyBatis 3.2.0版本中



总结


where 1=1 标签到底会不会影响性能,这个问题在网上已经出现了很多次,今天还是想从官方文档来进行说明。本文通过 MySQL的官方资料,加上百万数据的表进行真实测试,得出下面的结论:



  • 如果 MySQL Server版本大于等于 5.7,两个随便选,或者根据团队的要求来选;

  • 如果 MySQL Server版本小于 5.7,假如使用的是 MyBatis,建议使用 标签,如果使用的还是比较老的 iBATIS,只能使用where 1=1


最后,遇到问题,建议首先查找官方的一手资料,这样才能帮助自己在一条正确的技术道路上成长!


参考资料


MySQL8.0 Constant-Folding Optimization


MySQL5.7 WHERE Clause Optimization


What’s New in MySQL 5.7




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

代码很少,却很优秀!RocketMQ的NameServer是如何做到的?

今天我们来一起深入分析 RocketMQ的注册中心 NameServer。 本文基于 RocketMQ release-5.2.0 首先,我们回顾下 RocketMQ的内核原理鸟瞰图: 从上面的鸟瞰图,我们可以看出:Nameserver即和 Broker...
继续阅读 »

今天我们来一起深入分析 RocketMQ的注册中心 NameServer。



本文基于 RocketMQ release-5.2.0



首先,我们回顾下 RocketMQ的内核原理鸟瞰图:


image.png


从上面的鸟瞰图,我们可以看出:Nameserver即和 Broker交互,也和 Producer和 Consumer交互,因此,在 RocketMQ中,Nameserver起到了一个纽带性的作用。


接着,我们再看看 NameServer的工程结构,如下图:


image.png


整个工程只有 11个类(老版本好像只有不到 10个类),为什么 RocketMQ可以用如此少的代码,设计出如此高性能且轻量的注册中心?


我觉得最核心的有 3点是:



  1. AP设计思想

  2. 简单的数据结构

  3. 心跳机制


AP设计思想


像 ZooKeeper,采用了 Zab (Zookeeper Atomic Broadcast) 这种比较重的协议,必须大多数节点(过半数)可用,才能确保了数据的一致性和高可用,大大增加了网络开销和复杂度。


而 NameServer遵守了 CAP理论中 AP,在一个 NameServer集群中,NameServer节点之间是P2P(Peer to Peer)的对等关系,并且 NameServer之间并没有通信,减少很多不必要的网络开销,即便只剩一个 NameServer节点也能继续工作,足以保证高可用。


数据结构


NameServer维护了一套比较简单的数据结构,内部维护了一个路由表,该路由表包含以下几个核心元数据,对应的源码类RouteInfoManager如下:


public class RouteInfoManager {
private final static long DEFAULT_BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2; // broker失效时间 120s
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Map/* topic */, Map> topicQueueTable;
private final Map/* brokerName */, BrokerData> brokerAddrTable;
private final Map/* clusterName */, Set/* brokerName */>> clusterAddrTable;
private final Map/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
private final Map/* brokerAddr */, List/* Filter Server */> filterServerTable;
}


  • topicQueueTable: Topic消息队列路由信息,消息发送时根据路由表进行负载均衡

  • brokerAddrTable: Broker基础信息,包括brokerName、所属集群名称、主备Broker地址

  • clusterAddrTable: Broker集群信息,存储集群中所有Broker名称

  • brokerLiveTable: Broker状态信息,NameServer每次收到心跳包是会替换该信息

  • filterServerTable: Broker上的FilterServer列表,用于过滤标签(Tag)或 SQL表达式,以减轻 Consumer的负担,提高消息消费的效率。


TopicRouteData


TopicRouteData是 NameServer中最重要的数据结构之一,它包括了 Topic对应的所有 Broker信息以及每个 Broker上的队列信息,filter服务器列表,其源码如下:


public class TopicRouteData {
private List queueDatas;
private List brokerDatas;
private HashMap> filterServerTable;
//It could be null or empty
private Map/*brokerName*/, TopicQueueMappingInfo> topicQueueMappingByBroker;
}

BrokerData


BrokerData包含了 Broker的基本属性,状态,所在集群以及 Broker服务器的 IP地址,其源码如下:


public class BrokerData {
private String cluster;//所在的集群
private String brokerName;//所在的brokerName
private HashMap brokerAddrs;//该broker对应的机器IP列表
private String zoneName; // 区域名称
}

QueueData


QueueData包含了 BrokerName,readQueue的数量,writeQueue的数量等信息,对应的源码类是QueueData,其源码如下:


public class QueueData {
private String brokerName;//所在的brokerName
private int readQueueNums;// 读队列数量
private int writeQueueNums;// 写队列数量
private int perm; // 读写权限,参考PermName 类
private int topicSysFlag; // topic同步标记,参考TopicSysFlag 类
}

元数据举例


为了更好地理解元数据,这里对每一种元数据都给出一个数据实例:


topicQueueTable:{
"topicA":[
{
"brokeName":"broker-a",
"readQueueNums":4,
"writeQueueNums":4,
"perm":6,
"topicSyncFlag":0
},
{
"brokeName":"broker-b",
"readQueueNums":4,
"writeQueueNums":4,
"perm":6,
"topicSyncFlag":0
}
],
"topicB":[]
}

brokeAddrTable:{
"broker-a":{
"cluster":"cluster-1",
"brokerName":"broker-a",
"brokerAddrs":{
0:"192.168.0.1:8000",
1:"192.168.0.2:8000"
}
},
"broker-b":{
"cluster":"cluster-1",
"brokerName":"broker-b",
"brokerAddrs":{
0:"192.168.0.3:8000",
1:"192.168.0.4:8000"
}
}
}

brokerLiveTable:{
"192.168.0.1:8000":{
"lastUpdateTimestamp":1533434434344,//long 的时间戳
"dataVersion":dataVersionObj, //参考DataVersion类
"channel":channelObj,// 参考io.netty.channel.Channel
"haServerAddr":"192.168.0.2:8000"
},
"192.168.0.2:8000":{
"lastUpdateTimestamp":1533434434344,//long 的时间戳
"dataVersion":dataVersionObj, //参考DataVersion类
"channel":channelObj,// 参考io.netty.channel.Channel
"haServerAddr":"192.168.0.1:8000"
},
"192.168.0.3:8000":{ },
"192.168.0.4:8000":{ },
}

clusterAddrTable:{
"cluster-1":[{"broker-a"},{"broker-b"}],
"cluster-2":[],
}

filterServerTable:{
"192.168.0.1:8000":[{"192.168.0.1:7000"}{"192.168.0.1:9000"}],
"192.168.0.2:8000":[{"192.168.0.2:7000"}{"192.168.0.2:9000"}],
}

心跳机制


心跳机制是 NameServer维护 Broker的路由信息最重要的一个抓手,主要分为接收心跳、处理心跳、心跳超时 3部分:


接收心跳


Broker每 30s会向所有的 NameServer发送心跳包,告诉它们自己还存活着,从而更新自己在 NameServer的状态,整体交互如下图:


image.png


处理心跳


NameServer收到心跳包时会更新 brokerLiveTable缓存中 BrokerLiveInfo的 lastUpdateTimeStamp信息,整体交互如下图:


image.png


处理逻辑可以参考源码:
org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#processRequest#brokerHeartbeat:


public RemotingCommand brokerHeartbeat(ChannelHandlerContext ctx,
RemotingCommand request)
throws RemotingCommandException {
final RemotingCommand response = RemotingCommand.createResponseCommand(null);
final BrokerHeartbeatRequestHeader requestHeader =
(BrokerHeartbeatRequestHeader) request.decodeCommandCustomHeader(BrokerHeartbeatRequestHeader.class);

this.namesrvController.getRouteInfoManager().updateBrokerInfoUpdateTimestamp(requestHeader.getClusterName(), requestHeader.getBrokerAddr());

response.setCode(ResponseCode.SUCCESS);
response.setRemark(null);
return response;
}

心跳超时


NameServer每隔 10s(每隔5s + 5s延迟)扫描 brokerLiveTable检查 Broker的状态,如果在 120s内未收到 Broker心跳,则认为 Broker异常,会从路由表将该 Broker摘除并关闭 Socket连接,同时还会更新路由表的其他信息,整体交互如下图:


image.png


private void startScheduleService() {
this.scanExecutorService.scheduleAtFixedRate(NamesrvController.this.routeInfoManager::scanNotActiveBroker,
5, this.namesrvConfig.getScanNotActiveBrokerInterval(), TimeUnit.MILLISECONDS);
}

源码参考:org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#unRegisterBroker(),核心流程:



  1. 遍历brokerAddrTable

  2. 遍历broker地址

  3. 根据 broker地址移除 brokerAddr

  4. 如果当前 Topic只包含待移除的 Broker,则移除该 Topic


其他核心源码解读


NameServer启动


NameServer的启动类为:org.apache.rocketmq.namesrv.NamesrvStartup,整个流程如下图:


image.png
图片来自:Mark_Zoe


NameServer启动最核心的 3个事情是:



  1. 加载配置:NameServerConfig、NettyServerConfig主要是映射配置文件,并创建 NamesrvController。

  2. 启动 Netty通信服务:NettyRemotingServer是 NameServer和Broker,Producer,Consumer通信的底层通道 Netty服务器。

  3. 启动定时器和钩子程序:NameServerController实例一方面处理 Netty接收到消息后,一方面内部有多个定时器和钩子程序,它是 NameServer的核心控制器。


总结


NameServer并没有采用复杂的分布式协议来保持数据的一致性,而是采用 CAP理论中的 AP,各个节点之间是Peer to Peer的对等关系,数据的一致性通过心跳机制,定时器,延时感知来完成。


NameServer最核心的 3点设计是:



  1. AP的设计思想

  2. 简单的数据结构

  3. 心跳机制




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

VSCode无限画布模式(可能会惊艳到你的一个小功能)

❓现存的痛点VSCode是我的主力开发工具,在实际的开发中,我经常会对编辑器进行分栏处理,组件A的tsx、css代码、工具类方法各一个窗口,组件B的tsx、css代码、工具类方法各一个窗口,组件C的......当组件拆的足够多的时候,多个分栏会把本就不大的编辑...
继续阅读 »

❓现存的痛点

image.png

VSCode是我的主力开发工具,在实际的开发中,我经常会对编辑器进行分栏处理,组件A的tsx、css代码、工具类方法各一个窗口组件B的tsx、css代码、工具类方法各一个窗口,组件C的......

small.gif

当组件拆的足够多的时候,多个分栏会把本就不大的编辑器窗口分成N份,每一份的可视区域就小的可怜,切换组件代码时,需要不小的翻找成本,而且经常忘记我之前把文件放在了那个格子里,特别是下面的场景(一个小窗口内开了N个小tab),此时更难找到想要的窗口了...

多个tab.gif

问题汇总

  1. 分栏会导致每个窗口的面积变小,开发体验差(即使可以双击放大,但效果仍不符合预期);
  2. 编辑器窗口容易被新打开的窗口替换掉,常找不到之前打开的窗口;
  3. 窗口的可操作性不强,位置不容易调整

💡解题的思路

1. 自由 & 独立的编辑器窗口

  1. 分栏会导致每个窗口的面积变小,开发体验不好。

那就别变小了!每个编辑器窗口都还是原来的大小,甚至更大!

20240531-220533.gif

2. 无限画布

  1. 编辑器窗口容易被新打开的窗口替换掉,常找不到之前打开的窗口。
  2. 窗口的可操作性不强,位置不容易调整。

那就每个窗口都拥有一个自己的位置好了!拖一下就可以找到了!

scroll.gif

3. 画布体验

好用的画布是可以较大的提升用户体验的,下面重点做了四个方面的优化:

3.1 在编辑器里可以快速缩小 & 移动

因为不可避免的会出现一些事件冲突(比如编辑器里的滚动和画布的滚动、缩放等等),通过提供快捷键的解法,可以在编辑器内快速移动、缩放画布。

command + 鼠标上下滑动 = 缩放
option + 鼠标移动 = 画布移动

注意下图,鼠标还在编辑器窗口中,依然可以拖动画布👇🏻

single-editor.gif

3.2 快速放大和缩小编辑窗口

通过快捷按钮的方式,可以快速的放大和缩小编辑器窗口。

scale.gif

3.3 一键定位到中心点

不小心把所有窗口都拖到了画布视口外找不到了?没事儿,可以通过点击快捷按钮的方式,快速回到中心点。

center.gif

3.4 窗口的合并和分解

可以在窗口下进行编辑器的合并,即可以简单的把一些常用的窗口进行合并、分解。

add-remove.gif

💬 提出的背景

作为一名前端开发同学,避免不了接触UI同学的设计稿,我司使用的就是figma,以figma平台为例,其无限画布模式可以非常方便的平铺N个稿子,并快速的看到所有稿子的全貌、找到自己想要的稿子等等,效果如下:

figma.gif

没错!我就是基于这个思路提出了第一个想法,既然图片可以无限展示,编辑器为什么不能呢?

这个想法其实去年就有了,期间大概断断续续花了半年多左右的时间在调研和阅读VSCode的源码上,年后花了大概3个月的时间进行实现,最终在上个月做到了上面的效果。

经过约一个月的试用(目前我的日常需求均是使用这种模式进行的开发),发现效果超出预期,我经常会在画布中开启约10+个窗口,并频繁的在各个窗口之间来回移动,在这个过程中,我发现以下几点很让我很是欣喜:

  1. 空间感:我个人对“空间和方向”比较敏感,恰好画布模式会给我一种真实的空间感,我仿佛在一间房子里,里面摆满了我的代码,我穿梭在代码中,修一修这个,调一调这个~
  2. 满足感:无限画布的方式,相当于我间接拥有了无限大的屏幕,我只需要动动手指找到我的编辑窗口就好了,它可以随意的放大和缩小,所以我可以在屏幕上展示足够多的代码。
  3. 更方便的看源码:我可以把源码的每个文件单独开一个窗口,然后把每个窗口按顺序铺起来,摆成一条线,这条线就是源码的思路(当然可以用截图的方式看源码 & 缕思路,但是,需要注意一点,这个编辑器是可以交互的!)

⌨️ 后续的计划

后续计划继续增强画布的能力,让它可以更好用:

  1. 小窗口支持命名,在缩小画布时,窗口缩小,但是命名不缩小,可以直观的找到想要的窗口。
  2. 增强看源码的体验:支持在画布上添加其他元素(文案、箭头、连线),试想一下,以后在看源码时,拥有一个无限的画板来展示代码和思路,关键是代码是可以交互的,这该有多么方便!
  3. 类似MacOS的台前调度功能:把有关联的一些窗口分组,画布一侧有分组的入口,点击入口可以切换画布中的组,便于用户快速的进行批量窗口切换,比如A页面的一些JS、CSS等放在一个组,B页面放在另一个组,这样可以快速的切换文件窗口。

📔 其他的补充

调研过程中发现无法使用VSCode的插件功能来实现这个功能,所以只好fork了一份VSCode的开源代码,进行了大量修改,最终需要对源码进行编译打包才能使用(一个新的VSCode),目前只打包了mac的arm64版本来供自己试用。

另外,由于VSCode并不是100%开源(微软的一些服务相关的逻辑是闭源的),所以github上的开源仓库只是它的部分代码,经过编译之后,发现缺失了远程连接相关的功能,其他的功能暂时没有发现缺失。

image.png

🦽 可以试用吗

目前还没有对外提供试用版的打算,想自己继续使用一些时间,持续打磨一下细节,等功能细节更完善了再对外进行推广,至于这次的软文~ 其实是希望可以引起阅读到这里的同学进行讨论,可以聊一下你对该功能的一些看法,以及一些其他的好点子~,thx~

🫡 小小的致敬

  • 致敬VSCode团队,在阅读和改造他们代码的过程中学习到了不少hin有用的代码技能,也正是因为有他们的开源,才能有我的这次折腾👍🏻
  • 致敬锤子科技罗永浩老师,这次实现思路也有借鉴当年发布的“无限屏”功能,本文的头图就是来自当年的发布会截图。

image.png


作者:木头就是我呀
来源:juejin.cn/post/7375586227984220169
收起阅读 »

接口幂等和防抖还在傻傻分不清楚。。。

最近想重温下接口幂等的相关知识,正所谓温故而知新,太久不CV的东西要是哪天CV起来都生疏了,哈哈哈 先从字面意思来温习下吧,幂等的官方概念来源于数学上幂等概念的衍生,如幂等函数,即使用相同的参数重复执行,可以得到相同的结果的函数,翻译成IT行业专业术语就是一个...
继续阅读 »

最近想重温下接口幂等的相关知识,正所谓温故而知新,太久不CV的东西要是哪天CV起来都生疏了,哈哈哈


先从字面意思来温习下吧,幂等的官方概念来源于数学上幂等概念的衍生,如幂等函数,即使用相同的参数重复执行,可以得到相同的结果的函数,翻译成IT行业专业术语就是一个接口使用相同的入参,无论执行多少次,最后得到的结果且保存的数据和执行一次是完全一样的,所以,基于这个概念,分析我们的CRUD,首先,查询,它可以说是幂等的,但是如果更精细的说,它也可能不是幂等的,基于数据库数据不变的情况下,查询接口是幂等的,如果是变化的话那可能上一秒你查出来的数据,下一秒它就被人修改了也不是不可能,所以,基于这一点它不符合幂等概念


接下来是删除接口,它和查询一样,也是天然幂等的,但是如果你的接口提供范围删除,那么就破坏了幂等性原则,理论上这个删除接口就不应该存在,那如果是产品经理非要那也是可以存在滴,技术永远都是为业务服务的嘛


修改接口也是同样的道理,理论上都必须是幂等的,如果不是,那就要考虑接口幂等性了,比如你的修改积分接口里写修改积分,每次都使用i++这种操作,那么它就破坏了幂等原则,有一个好方法就是基于用户唯一性标识把积分变动通过一张表记录下来,最后统计这张表的积分数值,这里也就涉及到新增接口的知识点,其实到这里,我们会发现,所有的接口理论上都可以是幂等的,但是总是这个那个的原因导致不幂等,所以,总结起来就是,如果你的系统需求需要接口幂等,那么就实现它,现在让我们进入正题吧


刚开始温习幂等知识的时候,我百度了很多别人写的文章,发现另一个概念,叫防抖,防止用户重复点击的意思,有意思的是有些文章竟然认为防抖就是幂等,他们解决接口幂等的思路是每次调用需要实现幂等接口时,前端都需要调用后端的颁布唯一token的接口,后端颁布token后保存在缓存中,然后前端带着这个token的请求去请求我们的幂等接口,验证携带的token来实现接口幂等,按照这个思路每次请求的token都不一样,如何保证幂等中相同参数的条件呢,这显然和幂等南辕北辙了,这显然就是接口防抖或者接口加锁的思路嘛


还有一种是可以实现接口幂等性的思路,这里也可以分享一下,和上面的思路差不多,也是每次请求幂等接口的时候,先调用颁发唯一token的接口,唯一不同的是它颁发的token是基于入参生成的哈希值,后面的业务逻辑就是后端基于这个哈希值去校验,如果缓存中已经存在了,说明这个入参已经请求过了,那么直接拒绝请求并返回成功,这样,就从表面上实现了接口幂等性,因为执行100次我只执行一次,剩余的99次我都拒绝,并直接返回成功,这样,我的接口就从表面上幂等了,但是这个方案有一个很大的问题就是每次调用都需要浪费一部分资源在请求颁发token上,这对需要大量的幂等接口的系统来说就是一个累赘,所以,接下来,我们基于这个思路实现一个不需要二次调用的实现接口幂等的方法。


我的思路是这样的,业务上有些接口是实现防抖功能,有些是实现幂等功能,其实这两个功能在业务上确实是相差不大,所以,我的思路是定义一个注解,包含防抖和幂等的功能,首先基于幂等如果要是把所有入参都哈希化作为唯一标识的话有点费劲,可以基于业务上的一些唯一标识来做,如用户id或者code,还需要一个开关,用于决定是否保存这个唯一标识,还要一个时间,保存多久,还有保存时间的单位,最后,还有一个返回提醒,就是拒绝之后的友好提示,基于这些差不多了,如果你的接口功能只需要实现防抖,那么你可以设置时间段内过期,这样就实现了防抖,如果你的接口没有唯一标识,那么可以基于路由来做防抖,这个不要忘了设置过期时间,不然你的接口就永远是拒绝了,好了,思路有了,接下来就是实操了,话不多数,上代码


@Retention(RetentionPolicy.RUNTIME)
//注解用于方法
@Target({ElementType.TYPE, ElementType.METHOD})
//注解包含在JavaDoc中
@Documented
public @interface Idempotent {

/**
* 幂等操作的唯一标识,使用spring el表达式 用#来引用方法参数
*
* @return Spring-EL expression
*/

String key() default "";

/**
* 有效期 默认:1 有效期要大于程序执行时间,否则请求还是可能会进来
*
* @return expireTime
*/

int expireTime() default 100;

/**
* 时间单位 默认:s
*
* @return TimeUnit
*/

TimeUnit timeUnit() default TimeUnit.SECONDS;

/**
* 提示信息,可自定义
*
* @return String
*/

String info() default "重复请求,请稍后重试";

/**
* 是否在业务完成后删除key true:删除 false:不删除
*
* @return boolean
*/

boolean delKey() default false;


基本和我们上面的思路一样,唯一key,有效期,有效期时间单位,提示信息,是否删除,注解有了,那么我们就要基于注解写我们的逻辑了,这里我们需要用到aop,引用注解应该都知道吧,这里我们直接上代码了


@Aspect
@Slf4j
public class IdempotentAspect {
@Resource
private RedisUtil redisUtil;

private static final SpelExpressionParser PARSER = new SpelExpressionParser();

private static final LocalVariableTableParameterNameDiscoverer DISCOVERER = new LocalVariableTableParameterNameDiscoverer();

/**
* 线程私有map
*/

private static final ThreadLocal<Map<String, Object>> THREAD_CACHE = ThreadLocal.withInitial(HashMap::new);

private static final String KEY = "key";

private static final String DEL_KEY = "delKey";

// 以自定义 @Idempotent 注解为切点
@Pointcut("@annotation(com.liuhui.demo_core.spring.Idempotent)")
public void idempotent() {
}

@Before("idempotent()")
public void before(JoinPoint joinPoint) throws Throwable {
//获取到当前请求的属性,进而得到HttpServletRequest对象,以便后续获取请求URL和参数信息。
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
//从JoinPoint中获取方法签名,并确认该方法是否被@Idempotent注解标记。如果是,则继续执行幂等性检查逻辑;如果不是,则直接返回,不进行幂等处理。。
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
if (!method.isAnnotationPresent(Idempotent.class)) {
return;
}
Idempotent idempotent = method.getAnnotation(Idempotent.class);
String key;
// 若没有配置 幂等 标识编号,则使用 url + 参数列表作为区分;如果提供了key规则,则利用keyResolver根据提供的规则和切点信息生成键
if (!StringUtils.hasLength(idempotent.key())) {
String url = request.getRequestURL().toString();
String argString = Arrays.asList(joinPoint.getArgs()).toString();
key = url + argString;
} else {
// 使用jstl 规则区分
key = resolver(idempotent, joinPoint);
}
//从注解中读取并设置幂等操作的过期时间、描述信息、时间单位以及是否删除键的标志。
long expireTime = idempotent.expireTime();
String info = idempotent.info();
TimeUnit timeUnit = idempotent.timeUnit();
boolean delKey = idempotent.delKey();
String value = LocalDateTime.now().toString().replace("T", " ");
Object valueResult = redisUtil.get(key);
synchronized (this) {
if (null == valueResult) {
redisUtil.set(key, value, expireTime, timeUnit);
} else {
throw new IdempotentException(info);
}
}
Map<String, Object> map = THREAD_CACHE.get();
map.put(KEY, key);
map.put(DEL_KEY, delKey);
}


/**
* 从注解的方法的参数中解析出用于幂等性处理的键值(key)
*
* @param idempotent
* @param point
* @return
*/

private String resolver(Idempotent idempotent, JoinPoint point) {
//获取被拦截方法的所有参数
Object[] arguments = point.getArgs();
//从字节码的局部变量表中解析出参数名称
String[] params = DISCOVERER.getParameterNames(getMethod(point));
//SpEL表达式执行的上下文环境,用于存放变量
StandardEvaluationContext context = new StandardEvaluationContext();
//遍历方法参数名和对应的参数值,将它们一一绑定到StandardEvaluationContext中。
//这样SpEL表达式就可以引用这些参数值
if (params != null && params.length > 0) {
for (int len = 0; len < params.length; len++) {
context.setVariable(params[len], arguments[len]);
}
}
//使用SpelExpressionParser来解析Idempotent注解中的key属性,将其作为SpEL表达式字符串
Expression expression = PARSER.parseExpression(idempotent.key());
//转换结果为String类型返回
return expression.getValue(context, String.class);
}

/**
* 根据切点解析方法信息
*
* @param joinPoint 切点信息
* @return Method 原信息
*/

private Method getMethod(JoinPoint joinPoint) {
//将joinPoint.getSignature()转换为MethodSignature
//Signature是AOP中表示连接点签名的接口,而MethodSignature是它的具体实现,专门用于表示方法的签名。
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取到方法的声明。这将返回代理对象所持有的方法声明。
Method method = signature.getMethod();

//判断获取到的方法是否属于一个接口
//因为在Java中,当通过Spring AOP或其它代理方式调用接口的方法时,实际被执行的对象是一个代理对象,直接获取到的方法可能来自于接口声明而不是实现类。
if (method.getDeclaringClass().isInterface()) {
try {
//通过反射获取目标对象的实际类(joinPoint.getTarget().getClass())中同名且参数类型相同的方法
//这样做是因为代理类可能对方法进行了增强,直接调用实现类的方法可以确保获取到最准确的实现细节
method = joinPoint.getTarget().getClass().getDeclaredMethod(joinPoint.getSignature().getName(),
method.getParameterTypes());
} catch (SecurityException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
return method;
}


@After("idempotent()")
public void after() throws Throwable {
Map<String, Object> map = THREAD_CACHE.get();
if (CollectionUtils.isEmpty(map)) {
return;
}

String key = map.get(KEY).toString();
boolean delKey = (boolean) map.get(DEL_KEY);
if (delKey) {
redisUtil.delete(key);
log.info("[idempotent]:has removed key={}", key);
}
//无论是否移除了键,最后都会清空当前线程局部变量THREAD_CACHE中的数据,避免内存泄漏
THREAD_CACHE.remove();
}
}

上面redisUtil是基于RedisTemplate封装的工具类,可以直接替换哈,这里我们定义一个切入点,也就是我们定义的注解,然后在调用接口之前获取到接口的入参以及注解的参数,获取到这些之后,判断是否有唯一标识,没有就用路由,保存到reids当中,然后设置过期时间,最后需要把删除的标识放到线程私有变量THREAD_CACHE中在接口处理完之后判断是否需要删除redis当中保存的key,这里,我们的逻辑就写完了,接下来是使用了,使用这个就很简单,直接在你需要实现防抖和幂等的接口上打上我们的注解


/**
* 测试接口添加幂等校验
*
* @return
*/

@PostMapping("/redis")
@Idempotent(key = "#user.id", expireTime = 10, delKey = true, info = "重复请求,请稍后再试")
public Result<?> getRedis(@RequestBody User user) throws InterruptedException {
return Result.success(true);
}

这里key的定义方式我们使用了SpEL表达式,如果不指定这个表达式的话就会使用路由作为key了


到这里,接口幂等和防抖功能就顺利完成了,以后,别再防抖和幂等傻傻分不清楚了哈哈哈


最后,还是要送上一位名人曾说的一句话:手上没有剑和有剑不用是两回事!


作者:失乐园
来源:juejin.cn/post/7380274613185970195
收起阅读 »

阿里也出手了!Spring CloudAlibaba AI问世了

写在前面 在之前的文章中我们有介绍过SpringAI这个项目。SpringAI 是Spring 官方社区项目,旨在简化 Java AI 应用程序开发, 让 Java 开发者像使用 Spring 开发普通应用一样开发 AI 应用。 而SpringAI 主要面向的...
继续阅读 »

写在前面


在之前的文章中我们有介绍过SpringAI这个项目。SpringAI 是Spring 官方社区项目,旨在简化 Java AI 应用程序开发,


让 Java 开发者像使用 Spring 开发普通应用一样开发 AI 应用。


SpringAI 主要面向的是国外的各种大模型接入,对于国内开发者可能不太友好。


于是乎,Spring Cloud Alibaba AI便问世了,Spring Cloud Alibaba AI以 Spring AI 为基础,并在此基础上提供阿里云通义系列大模型全面适配,


让用户在 5 分钟内开发基于通义大模型的 Java AI 应用。


一、Spring AI 简介


可能有些小伙伴已经忘记了SpringAI 是啥?我们这儿再来简单回顾一下。


Spring AI是一个面向AI工程的应用框架。其目标是将可移植性和模块化设计等设计原则应用于AI领域的Spring生态系统,


并将POJO作为应用程序的构建块推广到AI领域。


转换为人话来说就是:Spring出了一个AI框架,帮助我们快速调用AI,从而实现各种功能场景。


二、Spring Cloud Alibaba AI 简介


Spring Cloud Alibaba AISpring AI 为基础,并在此基础上,基于 Spring AI 0.8.1 版本 API 完成通义系列大模型的接入


实现阿里云通义系列大模型全面适配。


在当前最新版本中,Spring Cloud Alibaba AI 主要完成了几种常见生成式模型的适配,包括对话、文生图、文生语音等,


开发者可以使用 Spring Cloud Alibaba AI 开发基于通义的聊天、图片或语音生成 AI 应用,


框架还提供 OutParserPrompt TemplateStuff 等实用能力。


三、第一个Spring AI应用开发


① 新建maven 项目


注: 在创建项目的时候,jdk版本必须选择17+


新建maven项目


② 添加依赖


<dependency>
   <groupId>com.alibaba.cloud</groupId>
   <artifactId>spring-cloud-alibaba-dependencies</artifactId>
   <version>2023.0.1.0</version>
   <type>pom</type>
   <scope>import</scope>
</dependency>

<dependency>
   <groupId>com.alibaba.cloud</groupId>
   <artifactId>spring-cloud-starter-alibaba-ai</artifactId>
   <version>2023.0.1.0</version>
</dependency>

注: 这里我们需要配置镜像源,否则是没法下载依赖的。会报如下错误



spring-ai: 0.8.1 dependency not found



<repositories>
   <repository>
       <id>spring-milestones</id>
       <name>Spring Milestones</name>
       <url>https://repo.spring.io/milestone</url>
       <snapshots>
           <enabled>false</enabled>
       </snapshots>
   </repository>
</repositories>

③ 在 application.yml 配置文件中添加api-key


spring:
cloud:
  ai:
    tongyi:
      api-key: 你自己申请的api-key

小伙伴如果不知道在哪申请,我把申请连接也放这儿了


dashscope.console.aliyun.com/apiKey


操作步骤:help.aliyun.com/zh/dashscop…


④ 新建TongYiController 类,代码如下


@RestController
@RequestMapping("/ai")
@CrossOrigin
@Slf4j
public class TongYiController {

   @Autowired
   @Qualifier("tongYiSimpleServiceImpl")
   private TongYiService tongYiSimpleService;

   @GetMapping("/example")
   public String completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {

       return tongYiSimpleService.completion(message);
  }
   
}

⑤ 新建TongYiService 接口,代码如下


public interface TongYiService {
   String completion(String message);

}

⑥ 新建TongYiSimpleServiceImpl 实现类,代码如下


@Service
@Slf4j
public  class TongYiSimpleServiceImpl  implements TongYiService {

   private final ChatClient chatClient;

   @Autowired
   public TongYiSimpleServiceImpl(ChatClient chatClient, StreamingChatClient streamingChatClient) {
       this.chatClient = chatClient;
  }

   @Override
   public String completion(String message) {
       Prompt prompt = new Prompt(new UserMessage(message));

       return chatClient.call(prompt).getResult().getOutput().getContent();
  }


}

到这儿我们一个简单的AI应用已经开发完成了,最终项目结构如下


项目结构


四、运行AI应用


启动服务,我们只需要在浏览器中输入:http://localhost:8080/ai/example 即可与AI交互。


① 不带message参数,则message=Tell me a joke,应用随机返回一个笑话


随机讲一个笑话1


② 我们在浏览器中输入:http://localhost:8080/ai/example?message=对话内容


message带入


五、前端页面对话模式


我们只需要在resources/static 路径下添加一个index.html前端页面,即可拥有根据美观的交互体验。


index.html代码官方github仓库中已给出样例,由于代码比较长,这里就不贴代码了


github.com/alibaba/spr…


添加完静态页面之后,我们浏览器中输入:http://localhost:8080/index.html 就可以得到一个美观的交互界面


美观交互界面


接下来,我们来实际体验一下


UI交互


六、其他模型


上面章节中我们只简单体验了对话模型,阿里还有很多其他模型。由于篇幅原因这里就不一一带大家一起体验了。


应用场景:


应用场景


各个模型概述:


模型概述


七、怎么样快速接入大模型


各种应用场景阿里官方GitHub都给出了接入例子


github.com/alibaba/spr…


官方样例


感兴趣的小伙伴可以自己到上面github 仓库看代码研究


本期内容到这儿就结束了,★,°:.☆( ̄▽ ̄)/$: .°★ 。 希望对您有所帮助


我们下期再见 ヾ(•ω•`)o (●'◡'●)


作者:xiezhr
来源:juejin.cn/post/7380771735681941523
收起阅读 »

反射为什么慢?

1. 背景 今天刷到一篇文章,标题是反射为什么慢,一下子懵逼了,确实没想过这个问题;盲猜了一下是由于反射实际上是做了一个代理的动作,导致执行的效率是小于直接实体类去调用方法的。 2. 文章给出的解释 文章中给出的理由是因为以下4点: 反射涉及动态解析的内容,...
继续阅读 »

1. 背景


今天刷到一篇文章,标题是反射为什么慢,一下子懵逼了,确实没想过这个问题;盲猜了一下是由于反射实际上是做了一个代理的动作,导致执行的效率是小于直接实体类去调用方法的。


2. 文章给出的解释


文章中给出的理由是因为以下4点:



  1. 反射涉及动态解析的内容,不能执行某些虚拟机优化,例如JIT优化技术

  2. 在反射时,参数需要包装成object[]类型,但是方法真正执行的时候,又使用拆包成真正的类型,这些动作不仅消耗时间,而且过程中会产生很多的对象,这就会导致gc,gc也会导致延时

  3. 反射的方法调用需要从数组中遍历,这个遍历的过程也比较消耗时间

  4. 不仅需要对方法的可见性进行检查,参数也需要做额外的检查


3. 结合实际理解


3.1 第一点分析


首先我们需要知道,java中的反射是一种机制,它可以在代码运行过程中,获取类的内部信息(变量、构造方法、成员方法);操作对象的属性、方法。
然后关于反射的原理,首先我们需要知道一个java项目在启动之后,会将class文件加载到堆中,生成一个class对象,这个class对象中有一个类的所有信息,通过这个class对象获取类相关信息的操作我们称为反射。


其次是JIT优化技术,首先我们需要知道在java虚拟机中有两个角色,解释器和编译器;这两者各有优劣,首先是解释器可以在项目启动的时候直接直接发挥作用,省去编译的时候,立即执行,但是在执行效率上有所欠缺;在项目启动之后,随着时间推移,编译器逐渐将机器码编译成本地代码执行,减少解释器的中间损耗,增加了执行效率。


我们可以知道JIT优化通常依赖于在编译时能够知道的静态信息,而反射的动态性可能会破坏这些假设,使得JIT编译器难以进行有效的优化。


3.2 第二点


关于第二点,我们直接写一段反射调用对象方法的demo:


@Test
public void methodTest() {
Class clazz = MyClass.class;

try {
//获取指定方法
//这个注释的会报错 java.lang.NoSuchMethodException
//Method back = clazz.getMethod("back");
Method back = clazz.getMethod("back", String.class);
Method say = clazz.getDeclaredMethod("say", String.class);
//私有方法需要设置
say.setAccessible(true);
MyClass myClass = new MyClass("abc", 99);
//反射调用方法
System.out.println(back.invoke(myClass, "back"));

say.invoke(myClass, "hello world");
} catch (Exception e) {
e.printStackTrace();
}
}

在上面这段代码中,我们调用了一个invoke 方法,并且传了class对象和参数,进入到invoke方法中,我们可以看到invoke方法的入参都是Object类型的,args更是一个Object 数组,这就第二点,关于反射调用过程中的拆装箱。


@CallerSensitive
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}

3.3 第三点


关于调用方法需要遍历这点,还是上面那个demo,我们在获取Method 对象的时候是通过调用getMethod、getDeclaredMethod方法,点击进入这个方法的源码,我们可以看到如下代码:


private static Method searchMethods(Method[] methods,
String name,
Class<?>[] parameterTypes)

{
Method res = null;
String internedName = name.intern();
for (int i = 0; i < methods.length; i++) {
Method m = methods[i];
if (m.getName() == internedName
&& arrayContentsEq(parameterTypes, m.getParameterTypes())
&& (res == null
|| res.getReturnType().isAssignableFrom(m.getReturnType())))
res = m;
}

return (res == null ? res : getReflectionFactory().copyMethod(res));
}

我们可以看到,底层实际上也是将class对象的所有method遍历了一遍,最终才拿到我们需要的方法的,这也就是第二点,执行具体方法的时候需要遍历class对象的方法。


3.4 第四点


第4点说需要对方法和参数进行检查,也就是我们在执行具体的某一个方法的时候,我们实际上是需要校验这个方法是否可见的,如果不可见,我们还需要将这个方法设置为可见,否则如果我们直接调用这个方法的话,会报错。


同时还有一个点,在我们调用invoke方法的时候,反射类会对方法和参数进行一个校验,让我们来看一下源码:


@CallerSensitive
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}

我们可以看到还有quickCheckMemberAccess、checkAccess 等逻辑


4. 总结


平时在反射这块用的比较少,也没针对性的去学习一下。在工作之余,还是得保持一个学习的习惯,这样子才不会出现今天这种被一个问题难倒的情况,而且才能产出更多、更优秀的方案。


作者:喜欢小钱钱
来源:juejin.cn/post/7330115846140051496
收起阅读 »

听我一句劝,业务代码中,别用多线程。

你好呀,我是歪歪。 前几天我在网上冲浪,看到一个哥们在吐槽,说他工作三年多了,没使用过多线程。 虽然八股文背的滚瓜烂熟,但是没有在实际开发过程中写的都是业务代码,没有使用过线程池,心里还是慌得一比。 我只是微微一笑,这不是很正常吗? 业务代码中一般也使不上多线...
继续阅读 »

你好呀,我是歪歪。


前几天我在网上冲浪,看到一个哥们在吐槽,说他工作三年多了,没使用过多线程。


虽然八股文背的滚瓜烂熟,但是没有在实际开发过程中写的都是业务代码,没有使用过线程池,心里还是慌得一比。


我只是微微一笑,这不是很正常吗?


业务代码中一般也使不上多线程,或者说,业务代码中不知不觉你以及在使用线程池了,你再 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 点多了。


本来想着在沙发上躺一会,结果一躺就是一整个下午。期间也想过起来写一会文章,坐在电脑前又飞快的躺回到沙发上,就是觉得这个事情索然无味,当下的那一刻就想躺着,然后无意识的刷手机,原本是拿来写文章中关于源码的部分的时间就这样浪费了。


像极了高中时的我,周末带大量作业回家,准备来个悬梁刺股,弯道超车,结果变成了一睡一天,捏紧刹车。


高中的时候,时间浪费了是真的可惜。


现在,不一样了。


荒腔走板这张图片,就是我躺在沙发上的时候,别人问我在干什么时随手拍的一张。


我并不为躺了一下午没有干正事而感到惭愧,浪费了的时间,才是属于自己的时间。


很久以前我看到别人在做一些浪费时间的事情的时候,我心里可能会嘀咕几句,劝人惜时。


这两年我不会了,允许自己做自己,允许别人做别人。


作者:why技术
来源:juejin.cn/post/7297980721590272040
收起阅读 »

全局异常统一处理很好,但建议你谨慎使用

思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。 作者:毅航😜 在SpringBoot应用开发中,利用@ControllerAdvice 结合 @ExceptionHandler来实现对后端服务的统一异常管理似乎已经成为了开发者约定俗成的规...
继续阅读 »



思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。

作者:毅航😜





SpringBoot应用开发中,利用@ControllerAdvice 结合 @ExceptionHandler来实现对后端服务的统一异常管理似乎已经成为了开发者约定俗成的规范了,为此网上也有很多文章来阐述如何更加优雅的来实现统一异常管理,但这样做真的好吗?


前言


SpringBoot中的全局统一异常管理通常是指利用@ControllerAdvice 结合 @ExceptionHandler来自定义一个全局的异常处理器,从而避免了在程序中频繁写书写try-catch来对异常进行处理。但结合笔者最近惨痛debug旧有项目的经历来看,笔者不推荐这样去做。


在讨论之前我们不妨先来看看实际开发中都有哪些地方可能出现的异常。


全局异常所带来的困惑


目前,大部分应用在开发时,通常会将代码划分为控制层,服务层,数据访问层三层结构,每个模块负责自己独立的逻辑。简单来看,控制层主要作用在于对外暴露 ur1访问地址,并将前台的处理请求委托给服务层来处理;而对于服务层来说其主要是业务逻辑处理的地方,以登录请求为例,用户名、密码的校验通常会放在服务层来处理;数据访问层则主要用于对数据库进行访问。如下这张图直观的反映了三层架构下各个模块所肩负的责任。


image.png


知晓了软件开发过程中的分层架构模式后。我们再来看每个模块可能产生的异常信息。其中:



  • 控制层主要用于处理实现前后端交互逻辑,其主要负责信息收禁、参数校验等,抛出的异常也多是参数校验、请求服务异常等异常信息。

  • 服务层主要用于处理业务逻辑,以及各种外部服务调用、访问数据作、缓存处理、消息处理等处理操作,这部分可能抛出的逻辑就非常多了。例如,数据库访问异常、服务调用异常等问题。

  • 数据访问层则主要负责数据访问实现,其一般没有业务逻辑。由于目前持久层使用技术通常为Mybatis,所以这一层抛出的异常多是Mybatis框架内部所抛出的异常。


不难发现,其中每一层所抛出的异常类型有着很大的差异。而我们在使用全局异常管理时,通常使用如下
代码逻辑:



@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGlobalException(Exception ex) {
// 在这里实现异常处理逻辑
log.error(ex.getMessage());//在控制台打印错误信息
return Result.error(ex.getMessage());
}
}



即我们通过在ExceptionHandler执行捕获异常为Exception来尽可能捕获业务中可能遇到的各种异常,相信网上已经有很多博主都在不断讲授这样的写法。


但这样做真的好吗?在笔者最近接手的旧项目中,当后端无法处理前端请求后会返回如下信息:


{
"message": “服务器异常”,
"code":602
}

后台服务通常会打印出如下信息:


image.png


(注:此处仅为展示,真实环境出现的异常远比此复杂)


不难发现,通过网上所盛传全局异常处理逻辑,我根本不知道问题出在哪里。我只知道当前程序出现异常,且异常信息为/ by zero。除此之外我无法得到任何有用的信息。


此外,通过在@ExceptionHandler配置Exception.class使得程序可以捕获多种异常信息,但这样粗粒度做法所导致的直接问题就是无法精确的定位异常问题发生所在地,极大的提升了问题定位的难度笔者项目同事debug经历来看,当项目无法正常运行时,组内的程序员通常会通过打日志的方法来一行一行定位问题所在。


破解之道


其实造成这样调试困难得本质原因在于全局异常机制的滥用,如下这张图真实的反映了引入全局异常机制后,异常的处理逻辑。


image.png


不难发现,当引入全局异常处理后,所有的异常信息都会交由RestExceptionHandler来进行处理。当程序遇到异常时,会将异常信息抛给RestExceptionHandler来处理,并由其定义错误的处理逻辑。


分析到此处,其实你已经发现了。对于全局异常处理逻辑而言,其更适合做异常的兜底工作。即如果当前层出现异常,并且不断上抛的仍然无法解决的话,不妨通过全局统一的异常管理来进行处理,以对这些未处理的异常进行捕获


此外,异常处理不应该进行像很多博客说的那样,仅是通过e.getMessage打印异常信息就可以了。这对于排查问题没有以一丁点的帮助,可以说是百害而无一利。


对于此,笔者更推荐在打印异常信息时,记录异常以及当前 URL、执行方法等信息以便后期方便问题排查。具体可参考如下代码:



@Slf4j
@RestControllerAdvice
public class GlobExceptionHandler {


@ExceptionHandler(ArithmeticException.class)//ArithmeticException异常类型通过注解拿到
public String exceptionHandler(HttpServletRequest request,ArithmeticException exception){
// 打印详细信息
log(request,exception);
return exception.getMessage();

}


public void log(HttpServletRequest request, Exception exception) {
//换行符
String lineSeparatorStr = System.getProperty("line.separator");

StringBuilder exStr = new StringBuilder();
StackTraceElement[] trace = exception.getStackTrace();
// 获取堆栈信息并输出为打印的形式
for (StackTraceElement s : trace) {
exStr.append("\tat " + s + "\r\n");
}
//打印error级别的堆栈日志
log.error("访问地址:" + request.getRequestURL() + ",请求方法:" + request.getMethod() +
",远程地址:" + request.getRemoteAddr() + lineSeparatorStr +
"错误堆栈信息如下:" + exception.toString() + lineSeparatorStr + exStr);
}
}

当程序发生错误时,其打印的日志信息如下:


image.png


不难发现,其完整的打印出了url、方法信息,错误参数、请求地址等信息,极大的降低了线上Bug的排查难度。


总结


技术本身并没有什么对与错之分,只不过有时我们用的方式和时机不对,进而使得本该提效的工具,反而在不断拖垮我们的效率。就如同本文分析的全局异常处理机制一样,其确实可以帮助我们降低try-catch的使用,但错误且不加考虑的乱用只会使得当系统出现问题时,我们只能两眼一抹黑,然后一行一行打日志来定位问题。


最后,对于代码中异常的捕获处理,笔者认为全局异常应该作为异常处理的都兜底操作,而不应该成为异常处理的灵丹妙药! 此外,全局异常处理过程不应该仅是简单的 e.getMessage()打印异常消息即可,其更应记录更加有助于异常排查的信息例如,方法,请求的url,请求参数等信息。



如果觉文章对你有帮助,不妨点赞+关注,不错过笔者之后的每一次更新!



作者:毅航
来源:juejin.cn/post/7291555600854106147
收起阅读 »

实现 Springboot 程序加密,禁止 jadx 反编译

背景 toB 的本地化 java 应用程序,通常是部署在客户机器上,为了保护知识产权,我们需要将核心代码(例如 Lience,Billing,Pay 等)进行加密或混淆,防止使用 jadx 等工具轻易反编译。同时,为了更深层的保护程序,也要防止三方依赖细节被窥...
继续阅读 »

背景


toB 的本地化 java 应用程序,通常是部署在客户机器上,为了保护知识产权,我们需要将核心代码(例如 Lience,Billing,Pay 等)进行加密或混淆,防止使用 jadx 等工具轻易反编译。同时,为了更深层的保护程序,也要防止三方依赖细节被窥探;


业界方案



  1. ProGuard

    • 简介:开源社区有名的免费混淆工具,相较于字节码加密,对性能基本无影响;

    • 优势:打包阶段混淆字节码,各种变量方法名都变成了abcdefg 等等无意义的符号,字节码可被反编译,但几乎无法阅读,通常被 Android App 用来防止逆向;

    • 不足1:只能混淆部分代码,打包阶段较为耗时,对于三方包混淆,并没有什么好办法。

    • 不足2:混淆后的代码,会影响 arthas 工具的使用,导致排查问题变慢。

    • 不足3:配置比较复杂,曾经在我司 T 项目上用过,令人眼花缭乱。

    • 不足4:无法加密三方依赖所有信息;



  2. jar-protect

    • 简介:一款国人开发的 springboot jar 加密工具;需要配合 javaagent 解密;

    • 优势:打包阶段使用 javassist 重写 class 文件;jadx 反编译后看到的都是空方法。反编译后只能看到类信息和方法签名,无法看到具体内容。

    • 不足1:使用 DES 方案,对于几百个三方 jar 的场景,加密手段过重,且加密后的不够完整;

    • 不足2:类文件放在一个目录(META-INF/.encode/),非常容易类冲突;

    • 不足3:无法加密三方依赖所有信息;



  3. GraalVM

    • 简介:Oracle GraalVM 提前将 Java 应用程序编译为独立的二进制文件。与在 Java 虚拟机 (JVM) 上运行的应用程序相比,这些二进制文件更小,启动速度提高了 100 倍,无需预热即可提供峰值性能,并且使用的内存和 CPU 更少, 并且无法反编译。

    • 不足:无法支持我司业务程序框架。



  4. core-lib/xjar

    • 简介:国人开源的,基于 golang 的加密工具。使用 maven 插件加密,启动时 golang 解密;性能影响未知。

    • 优势:可对所有 class 文件加密。

    • 不足1: 加密后 jar 文件体积翻倍;

    • 不足2:依赖 golang 编译,依赖 golang 启动;

    • 不足3:无法加密三方依赖所有信息;

    • 不足4:开源项目,3年未有新提交。




思考:


我们的需求到底是什么?a:保护知识产权。具体手段为:



  1. 对本司项目代码进行加密,使其无法被 jadx 工具轻易反编译,

  2. 对本司三方依赖进行加密,使其无法窥探我司三方依赖细节;


但上面的几个项目,基本都是围绕着 class 加密(除了GraalVM),这无法实现我们的第二个需求。


我们的方案


设计目标:



  1. 将项目三方依赖 jar 进行加密,使其无法使用 jadx 反编译,但运行时会生成解密后的临时文件。

  2. 将项目本身的 class 进行加密,使其无法使用 jadx 反编译运行时解密后的文件。

  3. 加密策略要灵活,轻量,对启动速度,包体积,内存消耗,接口性能的影响要控制在 5% 以内;


设计方案:



  1. 加密jar时,使用 maven 打包工具,repackage fat jar;将其内部 lib 目录的依赖进行加密;使 jadx 无法反编译;

  2. 加密class时,对于核心业务代码,使用 javassist 工具将其重写,清空方法体,重置属性值;

  3. 解密jar时,将指定目录的 加密包 解密 到指定目录,并将其放入 springboot classloader classpath 里。

  4. 解密class时,agent 配合判断是否是加密 class,如果是,则寻找加密 class 文件,找到后解密,返回解密后的 classBytes。


逻辑如下:



注意点:



  1. javassist 重写方法体时,需要将 lib 里的所有代码都加入 classpool 的 classpath 里。

  2. javassist 加密后的类,需将其放入到当前 lib 的单独目录进行个例,防止类冲突。

  3. agent 解密要轻量,不能影响程序性能;

  4. 三方包的加解密重新打包后,jar 顺序发生变化,较小可能会导致类冲突(比如 log4j)。需要在测试环境验证,如果存在冲突,则需要排包。


End


通过以上方案,我们实现了一个极其轻量的 maven 加密,agent 解密插件。他能够将三方包彻底加密,使 jadx 等工具无法反编译 ,屏蔽我们的三方依赖细节,同时,该插件也可以加密我们的业务 class 代码,使 jadx 无法反编译运行时生成的代码,从而一定程度的保护我们的知识产权;


另外,私有的加密算法,在性能,体积,内存等方便的影响都控制在 5% 以内。


为了防止混淆后的代码影响 arthas 的使用和 bug patch 的应用,我们放弃了混淆方案,只能说是一种权衡与取舍吧。


从软件防破解的角度来理解,通常只能是加大破解的难度,铁了心想要破解的话,就算是 ProGuard 混淆,也无法解决。也许只能用 GraalVM,但不是每一个客户都会用这个。


推荐


Java 扩展点/插件系统,支持热插拔,旨在解决大部分软件的功能定制问题


作者:莫那鲁道
来源:juejin.cn/post/7289661061984469051
收起阅读 »

应用容器化后为什么性能下降这么多?

1. 背景 随着越来越多的公司拥抱云原生,从原先的单体应用演变为微服务,应用的部署方式也从虚机变为容器化,容器编排组件k8s也成为大多数公司的标配。然而在容器化以后,我们发现应用的性能比原先在虚拟机上表现更差,这是为什么呢?。 2. 压测结果 2.1 容器化之...
继续阅读 »

1. 背景


随着越来越多的公司拥抱云原生,从原先的单体应用演变为微服务,应用的部署方式也从虚机变为容器化,容器编排组件k8s也成为大多数公司的标配。然而在容器化以后,我们发现应用的性能比原先在虚拟机上表现更差,这是为什么呢?。


2. 压测结果


2.1 容器化之前的表现


应用部署在虚拟机下,我们使用wrk工具进行压测,压测结果如下:


image.png


从压测结果看,平均RT1.68msqps716/s\color{red}{平均RT为1.68ms,qps为716/s},我们再来看下机器的资源使用情况,cpu基本已经被打满。
image.png


2.2 容器化后的表现


使用wrk工具进行压测,结果如下:
image.png


从压测结果看,平均RT2.11msqps554/s\color{red}{平均RT为2.11ms,qps为554/s},我们再来看下机器的资源使用情况,cpu基本已经被打满。
image.png


2.3 性能对比结果


性能对比虚拟机容器
RT1.68ms2.11ms
QPS716/s554/s


总体性能下降:RT(25%)、QPS(29%)



3. 原因分析


3.1 架构差异


由于应用在容器化后整体架构的不同、访问路径的不同,将可能导致应用容器化后性能的下降,于是我们先来分析下两者架构的区别。我们使用k8s作为容器编排基础设施,网络插件使用calico的ipip模式,整体架构如下所示。


x3.png


这里需要说明,虽然使用calico的ipip模式,由于pod的访问为service的nodePort模式,所以不会走tunl0网卡,而是从eth0经过iptables后,通过路由到calico的calixxx接口,最后到pod。


3.2性能分析


在上面压测结果的图中,我们容器化后,cpu的软中断si使用率明显高于原先虚拟机的si使用率,所以我们使用perf继续分析下热点函数。


image.png
为了进一步验证是否是软中断的影响,我们使用perf进一步统计软中断的次数。


image.png



我们发现容器化后比原先软中断多了14%,到这里,我们能基本得出结论,应用容器化以后,需要更多的软中断的网络通信导致了性能的下降。



3.3 软中断原因


由于容器化后,容器和宿主机在不同的网络namespace,数据需要在容器的namespace和host namespace之间相互通信,使得不同namespace的两个虚拟设备相互通信的一对设备为veth pair,可以使用ip link命令创建,对应上面架构图中红色框内的两个设备,也就是calico创建的calixxx和容器内的eth0。我们再来看下veth设备发送数据的过程


static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev)
{
...
if (likely(veth_forward_skb(rcv, skb, rq, rcv_xdp)
...
}

static int veth_forward_skb(struct net_device *dev, struct sk_buff *skb,
struct veth_rq *rq, bool xdp)
{
return __dev_forward_skb(dev, skb) ?: xdp ?
veth_xdp_rx(rq, skb) :
netif_rx(skb);//中断处理
}


/* Called with irq disabled */
static inline void ____napi_schedule(struct softnet_data *sd,
struct napi_struct *napi)
{
list_add_tail(&napi->poll_list, &sd->poll_list);
//发起软中断
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}

通过虚拟的veth发送数据和真实的物理接口没有区别,都需要完整的走一遍内核协议栈,从代码分析调用链路为veth_xmit -> veth_forward_skb -> netif_rx -> __raise_softirq_irqoff,veth的数据发送接收最后会使用软中断的方式,这也刚好解释了容器化以后为什么会有更多的软中断,也找到了性能下降的原因。


4. 优化策略


原来我们使用calico的ipip模式,它是一种overlay的网络方案,容器和宿主机之间通过veth pair进行通信存在性能损耗,虽然calico可以通过BGP,在三层通过路由的方式实现underlay的网络通信,但还是不能避免veth pari带来的性能损耗,针对性能敏感的应用,那么有没有其他underly的网络方案来保障网络性能呢?那就是macvlan/ipvlan模式,我们以ipvlan为例稍微展开讲讲。


4.1 ipvlan L2 模式


IPvlan和传统Linux网桥隔离的技术方案有些区别,它直接使用linux以太网的接口或子接口相关联,这样使得整个发送路径变短,并且没有软中断的影响,从而性能更优。如下图所示:


ipvlan l2 mode


上图是ipvlan L2模式的通信模型,可以看出container直接使用host eth0发送数据,可以有效减小发送路径,提升发送性能。


4.2 ipvlan L3 模式


ipvlan L3模式,宿主机充当路由器的角色,实现容器跨网段的访问,如下图所示:


ipvlan L3 mode


4.3 Cilium


除了使用macvlan/ipvlan提升网络性能外,我们还可以使用Cilium来提升性能,Cilium为云原生提供了网络、可观测性、网络安全等解决方案,同时它是一个高性能的网络CNI插件,高性能的原因是优化了数据发送的路径,减少了iptables开销,如下图所示:


cilium netwok


虽然calico也支持ebpf,但是通过benchmark的对比,Cilium性能更好,高性能名副其实,接下来我们来看看官网公布的一些benchmark的数据,我们只取其中一部分来分析,如下图:


xxxx2
xxxx3


无论从QPS和CPU使用率上Cilium都拥有更强的性能。


5. 总结


容器化带来了敏捷、效率、资源利用率的提升、环境的一致性等等优点的同时,也使得整体的系统复杂度提升一个等级,特别是网络问题,容器化使得整个数据发送路径变长,排查难度增大。不过现在很多网络插件也提供了很多可观测性的能力,帮助我们定位问题。


我们还是需要从实际业务场景出发,针对容器化后性能、安全、问题排查难度增大等问题,通过优化架构,增强基础设施建设才能让我们在云原生的路上越走越远。


最后,感谢大家观看,也希望和我讨论云原生过程中遇到的问题。


5. 参考资料


docs.docker.com/network/dri…


cilium.io/blog/2021/0…


作者:云之舞者
来源:juejin.cn/post/7268663683881828413
收起阅读 »

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

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

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


a00e8833034583c6895e1582c899a2f3.png


问题原因


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


解决方案:


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


修改Nginx配置文件


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


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

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


server {
listen 443 ssl;
server_name xxx.com;

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

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

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

SpringBoot代码实现


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


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

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

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


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

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

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

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

前言



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


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


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



正文


1、接口别随便暴露



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




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




比如下面这种接口:



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

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



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




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




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




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




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




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




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




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



2、什么是横向越权



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




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




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




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



1.jpg



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




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




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




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



3、权限控制不了吗



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




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




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




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




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




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




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




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




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




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




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



4、如何防范



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


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


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


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


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


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




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



总结



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


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


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


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


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

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

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

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


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


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


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


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


image.png


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


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


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


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


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


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


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


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


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


故障发生前的半小时


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


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


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


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


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


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


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


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


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


select * from order_discount_detail where orderId = 1123;


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


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


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


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


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


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


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


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


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


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


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


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


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


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


orderId 是 String 类型,SQL就变为


select * from order_discount_detail where orderId = ‘1123’;


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


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


险些背锅


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


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


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


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


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


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


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


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


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


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

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

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

后端接口设计


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


image.png


1.接口参数校验


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



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

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


2.接口扩展性


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


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


3.接口幂等设计


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


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


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


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



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

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


image.png


4.关键接口日志打印


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



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

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


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


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


image.png


6.第三方接口异常重试


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



  • 异常处理


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


  • 请求超时


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


  • 重试机制


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



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


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


image.png


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


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


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


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


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

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

9.高频接口注意限流


自定义注解 + AOP


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

@Aspect
@Component
public class RateLimiterAspect {

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

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

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

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

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

@RestController
public class ApiController {

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

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

10.保障接口安全


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


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


11.接口控制锁粒度


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


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


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


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



  • 锁粒度过大:


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



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


  • 缩小锁粒度


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

12.避免长事务问题


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


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



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


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


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


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



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


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

使用TransactionTemplat进行编排式事务


@Resource
private TransactionTemplate transactionTemplate;

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

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

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

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

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


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


1. 导入依赖


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

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


2. 使用jasypt


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


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


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



2.1. 加/解密


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


public interface StringEncryptor {  

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

public String encrypt(String message);


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

public String decrypt(String encryptedMessage);

}

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


@SpringBootTest  
@Slf4j
class JasryptApplicationTests {

@Autowired
private StringEncryptor stringEncryptor;

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

}

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


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

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


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

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


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


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


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

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


3. 结尾


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


此致


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

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

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

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


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



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

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


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


Nginx 是什么?


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


1667274211133.jpg


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


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



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






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





Nginx 的核心特性


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


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


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


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

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


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


sudo nginx -s reload

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


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

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

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


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

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


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

搭建 Nginx 服务


1-48.jpeg


如何安装?


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


sudo apt update
sudo apt install nginx

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


sudo yum install epel-release
sudo yum install nginx

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


sudo systemctl start nginx

设置开机自启动:


sudo systemctl enable nginx

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


常用命令有哪些


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



  1. 启动 Nginx:nginx

  2. 停止 Nginx:nginx -s stop

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

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

  5. 查看 Nginx 版本:nginx -v


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



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

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


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


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


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



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

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

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

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

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






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

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

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

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

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

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

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

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

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

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


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


a0e2c2ce0c28044531b1589f5e3fb83263cb690c.jpeg


打开 Nginx 配置世界大门


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


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

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

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

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

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

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

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

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

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

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

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

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


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


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


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


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


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

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


2、反向代理


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

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


3、负载均衡


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

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


4、HTTPS 配置


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

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


5、安全防护


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

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


63d12faf8e9f0972ed2f0d90_1024.jpeg


Nginx 深入学习-负载均衡


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


健康检查


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


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

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

server {
listen 80;

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


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



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


负载均衡算法


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


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


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

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


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


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

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


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


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

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


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


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

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


开源模块


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


image (1).png


总结


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


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

别再用后缀判断文件类型了,来认识一下魔数头

引言 最近公司整改,招了个安全测试,安全测试的同事搞了个危险的文件,悄咪咪加了个.png的后缀,就丢到我们的生产服务器上面去了,这么勇,你敢信? 不管你信不信,事实他就是发生了,就问你怕不怕。 好了,这里也就暴露了一个很大的问题,我们的开发同事判断文件类型,...
继续阅读 »

引言



最近公司整改,招了个安全测试,安全测试的同事搞了个危险的文件,悄咪咪加了个.png的后缀,就丢到我们的生产服务器上面去了,这么勇,你敢信?


不管你信不信,事实他就是发生了,就问你怕不怕。


好了,这里也就暴露了一个很大的问题,我们的开发同事判断文件类型,是使用文件后缀来判断的,所以被直接跳过。咱来看看更加科学的识别方式。



一、认识魔数


魔数:也被称为签名或文件签名,是一种用于识别文件类型和格式的短序列字节。它们通常位于文件的开头,并被设计为易于识别,以便软件可以快速确定文件是否是它所支持的格式。


魔数是一种简单的识别机制,它由一系列字节组成,这些字节在特定的文件格式中是唯一的。当一个程序打开一个文件时,它会检查文件的开始处是否包含这些特定的字节。如果找到,程序就认为文件是该格式的,并按照相应的规则进行解析和处理。


二、文件类型检测的原理



文件类型检测通常基于文件的“魔数”(magic number),也称为签名或文件签名。魔数是文件开头的字节序列,用于标识文件格式。以下是文件类型检测的原理和步骤:



2.1 文件头的读取方法


image.png


打开文件:首先,需要以二进制模式打开文件,以便能够读取文件的原始字节。


读取字节:接着,读取文件开头的一定数量的字节(通常是前几个字节)。


关闭文件:读取完成后,关闭文件以释放资源。


2.2 如何通过文件头识别文件类型


image.png


比较魔数:将读取的字节与已知的文件类型魔数进行比较。


匹配类型:如果字节序列与某个文件类型的魔数匹配,则可以确定文件类型。


处理异常:如果字节序列与任何已知魔数都不匹配,可能需要进一步的分析或返回未知文件类型。


三、Java实现文件类型检测


当需要通过文件头(魔数头)判断文件类型时,可以按照以下文字描述的流程进行实现:


graph LR
F[打开文件] --> B[读取文件头]
B --> C[判断文件类型]
C --> D[比较文件头]
D --> E[输出文件类型]

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#B2FFFF,stroke:#B2FFFF,stroke-width:2px


  1. 打开文件:使用文件输入流(FileInputStream)打开待判断类型的文件。

  2. 读取文件头:从文件中读取一定长度的字节数据作为文件头。通常,文件头的长度为固定的几个字节,一般是 2-8 个字节。

  3. 判断文件类型:根据不同文件类型的魔数头进行判断。魔数头是文件中特定位置的字节序列,用于标识文件类型。每种文件类型都有不同的魔数头。

  4. 比较文件头:将读取到的文件头与已知文件类型的魔数头进行比较。如果匹配成功,则确定文件类型。

  5. 输出文件类型:根据匹配的文件类型,输出相应的文件类型描述信息。


3.1 具体实现方法


3.1.1 定义枚举类


/**
* 文件类型魔数枚举
* 使用场景:用于判断文件类型
* 使用方法:FileUtils.isFileType(new FileInputStream(file), FileTypeEnum.XLSX)
*
* @author bamboo panda
* @version 1.0
* @date 2024/5/23 19:37
*/

public enum FileTypeEnum {
/**
* JPEG
*/

JPEG("JPEG", "FFD8FF"),

/**
* PNG
*/

PNG("PNG", "89504E47"),

/**
* GIF
*/

GIF("GIF", "47494638"),

/**
* TIFF
*/

TIFF("TIFF", "49492A00"),

/**
* Windows bitmap
*/

BMP("BMP", "424D"),

/**
* CAD
*/

DWG("DWG", "41433130"),

/**
* Adobe photoshop
*/

PSD("PSD", "38425053"),

/**
* Rich Text Format
*/

RTF("RTF", "7B5C727466"),

/**
* XML
*/

XML("XML", "3C3F786D6C"),

/**
* HTML
*/

HTML("HTML", "68746D6C3E"),

/**
* Outlook Express
*/

DBX("DBX", "CFAD12FEC5FD746F "),

/**
* Outlook
*/

PST("PST", "2142444E"),

/**
* doc;xls;dot;ppt;xla;ppa;pps;pot;msi;sdw;db
*/

OLE2("OLE2", "0xD0CF11E0A1B11AE1"),

/**
* Microsoft Word/Excel
*/

XLS_DOC("XLS_DOC", "D0CF11E0"),

/**
* Microsoft Access
*/

MDB("MDB", "5374616E64617264204A"),

/**
* Word Perfect
*/

WPB("WPB", "FF575043"),

/**
* Postscript
*/

EPS_PS("EPS_PS", "252150532D41646F6265"),

/**
* Adobe Acrobat
*/

PDF("PDF", "255044462D312E"),

/**
* Windows Password
*/

PWL("PWL", "E3828596"),

/**
* ZIP Archive
*/

ZIP("ZIP", "504B0304"),

/**
* ARAR Archive
*/

RAR("RAR", "52617221"),

/**
* WAVE
*/

WAV("WAV", "57415645"),

/**
* AVI
*/

AVI("AVI", "41564920"),

/**
* Real Audio
*/

RAM("RAM", "2E7261FD"),

/**
* Real Media
*/

RM("RM", "2E524D46"),

/**
* Quicktime
*/

MOV("MOV", "6D6F6F76"),

/**
* Windows Media
*/

ASF("ASF", "3026B2758E66CF11"),

/**
* MIDI
*/

MID("MID", "4D546864"),
/**
* xlsx
*/

XLSX("XLSX", "504B0304"),
/**
* xls
*/

XLS("XLS", "D0CF11E0A1B11AE1");

private String key;
private String value;

FileTypeEnum(String key, String value) {
this.key = key;
this.value = value;
}

public String getValue() {
return value;
}

public String getKey() {
return key;
}
}

3.1.2 文件类型判断工具类


import java.io.IOException;
import java.io.InputStream;

/**
* 文件类型判断工具类
*
* @author bamboo panda
* @version 1.0
* @date 2024/5/23 19:38
*/

public class FileTypeUtils {

/**
* 获取文件头
*
* @param inputStream 输入流
* @return 16 进制的文件投信息
* @throws IOException io异常
*/

private static String getFileHeader(InputStream inputStream) throws IOException {
byte[] b = new byte[28];
inputStream.read(b, 0, 28);
inputStream.close();
return bytes2hex(b);
}

/**
* 将字节数组转换成16进制字符串
*
* @param src 文件字节数组
* @return 16进制字符串
*/

private static String bytes2hex(byte[] src) {
StringBuilder stringBuilder = new StringBuilder("");
if (src == null || src.length <= 0) {
return null;
}
for (byte b : src) {
int v = b & 0xFF;
String hv = Integer.toHexString(v);
if (hv.length() < 2) {
stringBuilder.append(0);
}
stringBuilder.append(hv);
}
return stringBuilder.toString();
}

/**
* 判断指定输入流是否是指定文件格式
*
* @param inputStream 输入流
* @param fileTypeEnum 文件格式枚举
* @return true 是; false 否
* @throws IOException io异常
*/

public static boolean isFileType(InputStream inputStream, FileTypeEnum fileTypeEnum) throws IOException {
if (null == inputStream) {
return false;
}
String fileHeader = getFileHeader(inputStream);
return fileHeader.toUpperCase().startsWith(fileTypeEnum.getValue());
}

}

3.1.3 测试方法


import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

/**
* 测试:判断文件是否是excel
*
* @author bamboo panda
* @version 1.0
* @date 2024/5/23 19:33
*/

public class Test {

public static void main(String[] args) {
File file = new File("C:\Users\Admin\Desktop\temp\Import file.xlsx");
try (FileInputStream fileInputStream = new FileInputStream(file)) {
if (!FileTypeUtils.isFileType(fileInputStream, FileTypeEnum.XLSX) || !FileTypeUtils.isFileType(fileInputStream, FileTypeEnum.XLS)) {
System.out.println(true);
} else {
System.out.println(false);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

}

四、其它注意事项


在处理文件类型检测和数据保护时,安全性和隐私是两个非常重要的考虑因素。以下是一些相关的安全性问题和最佳实践:


4.1 魔数检测的安全性问题


graph LR
F(文件类型判断)
B(魔数检测的安全性问题)
C(误报和漏报)
D(恶意文件伪装)
E(更新和维护)

F ---> B
B ---> C
B ---> D
B ---> E

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#B2FFFF,stroke:#B2FFFF,stroke-width:2px


  1. 误报和漏报:魔数检测可能因为文件损坏或不完整而产生误报或漏报。一些恶意软件可能会模仿合法文件的魔数来逃避检测。

  2. 恶意文件伪装:攻击者可能故意在文件中嵌入合法的魔数,使得恶意文件看起来像是合法的文件类型。

  3. 更新和维护:随着新文件类型的出现和旧文件类型的淘汰,魔数列表需要定期更新,否则检测系统可能会变得不准确或过时。


4.2 数据保护和隐私的最佳实践


graph LR
I(文件类型判断)

A(数据保护和隐私的最佳实践)

G(最小权限原则)
B(数据加密)
C(安全的数据传输)
D(访问控制)
E(定期审计和测试)
F(数据最小化)

I ---> A
A ---> B
A ---> C
A ---> D
A ---> E
A ---> F
A ---> G

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#B2FFFF,stroke:#B2FFFF,stroke-width:2px
style G fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style A fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
style I fill:#EEDD82,stroke:#EEDD82,stroke-width:2px


  1. 最小权限原则:确保应用程序只请求执行其功能所必需的权限,不要求额外的权限。

  2. 数据加密:对敏感数据进行加密,无论是在传输中还是存储时,都应使用强加密标准。

  3. 安全的数据传输:使用安全的协议(如HTTPS)来保护数据在网络中的传输。

  4. 访问控制:实施严格的访问控制措施,确保只有授权用户才能访问敏感数据。

  5. 定期审计和测试:定期进行安全审计和渗透测试,以发现和修复潜在的安全漏洞。

  6. 数据最小化:只收集完成服务所必需的最少数据量,避免收集不必要的个人信息。


4.3 魔数的局限性和风险


魔数判断文件类型是一种常用的方法,但也存在一些局限性和风险,包括以下几点:


graph LR
I(文件类型判断)

A(魔数的局限性和风险)

B(可伪造性)
C(文件类型扩展性)
D(文件损坏或篡改)
E(多重文件类型)
F(文件类型模糊性)

I ---> A
A ---> B
A ---> C
A ---> D
A ---> E
A ---> F

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#B2FFFF,stroke:#B2FFFF,stroke-width:2px
style A fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
style I fill:#EEDD82,stroke:#EEDD82,stroke-width:2px


  1. 可伪造性:魔数头是文件中的特定字节序列,攻击者可以通过修改文件的魔数头来伪装文件类型。这可能导致误判文件类型或绕过文件类型检测。

  2. 文件类型扩展性:随着新的文件类型的出现,魔数头的定义可能需要不断更新,以适应新的文件类型。如果应用程序不及时更新对新文件类型的判断逻辑,可能无法正确识别新的文件类型。

  3. 文件损坏或篡改:如果文件的魔数头部分被损坏或篡改,可能导致无法正确判断文件类型,或者将文件错误地归类为不正确的类型。

  4. 多重文件类型:某些文件可能具有多重文件类型,即使使用魔数头判断了其中一种类型,也可能存在其他类型。这可能导致文件类型的混淆和判断的不准确性。

  5. 文件类型模糊性:某些文件类型可能具有相似或相同的魔数头,这可能导致在这些类型之间进行区分时出现困难。这可能增加了误判文件类型的风险。


五、总结


好了,到这里魔数怎么用的就说明白了。


魔数的广泛的应用在在文件类型检测中。魔数是文件开头的特定字节序列,帮助软件快速识别文件格式。


然而,魔数检测存在安全性问题,如误报、恶意伪装等,需定期更新魔数库。此外,应用魔数检测时要考虑文件损坏、多重类型等局限性,结合实际情况采取综合措施,如数据加密、访问控制等,确保安全性和准确性。



希望本文对您有所帮助。如果有任何错误或建议,请随时指正和提出。


同时,如果您觉得这篇文章有价值,请考虑点赞和收藏。这将激励我进一步改进和创作更多有用的内容。


感谢您的支持和理解!



作者:竹子爱揍功夫熊猫
来源:juejin.cn/post/7372100124636381194
收起阅读 »

我有点想用JDK17了

大家好呀,我是summo,JDK版本升级的非常快,现在已经到JDK20了。JDK版本虽多,但应用最广泛的还得是JDK8,正所谓“他发任他发,我用Java8”。 其实我也不太想升级JDK版本,感觉投入高,收益小,不过有一次我看到了一些使用JDK17新语法写的代码...
继续阅读 »

大家好呀,我是summo,JDK版本升级的非常快,现在已经到JDK20了。JDK版本虽多,但应用最广泛的还得是JDK8,正所谓“他发任他发,我用Java8”。


其实我也不太想升级JDK版本,感觉投入高,收益小,不过有一次我看到了一些使用JDK17新语法写的代码,让我改变了对升级JDK的看法,因为这些新语法我确实想用!


废话不多说,上代码!


一、JDK17语法新特性


1. 文本块



这个更新非常实用。在没有这个特性之前,编写长文本非常痛苦。虽然IDEA等集成开发工具可以自动处理,但最终效果仍然丑陋,充满拼接符号。现在,通过字符串块,我们可以轻松编写JSON、HTML、SQL等内容,效果更清爽。这个新特性值得五颗星评价,因为它让我们只需关注字符串本身,而无需关心拼接操作。



原来的写法


/**
* 使用JDK8返回HTML文本
*
* @return 返回HTML文本
*/

public static final String getHtmlJDK8() {
return "<html>\n" +
" <body>\n" +
" <p>Hello, world</p>\n" +
" </body>\n" +
"</html>";
}

新的写法


/**
* 使用JDK17返回HTML文本
*
* @return 返回HTML文本
*/

public static final String getHtmlJDK17() {
return """
<html>
<body>
<p>Hello, world</p>
</body>
</html>
"""
;
}


推荐指数:⭐️⭐️⭐️⭐️⭐️



2. NullPointerException增强



这一功能非常强大且实用,相信每位Java开发者都期待已久。空指针异常(NPE)一直是Java程序员的痛点,因为报错信息无法直观地指出哪个对象为空,只抛出一个NullPointerException和一堆堆栈信息,定位问题耗时且麻烦。尤其在遇到喜欢级联调用的代码时,逐行排查更是令人头疼。如果在测试环境中,可能还需通过远程调试查明空对象,费时费力。为此,阿里的编码规范甚至不允许级联调用,但这并不能彻底解决问题。Java17终于在这方面取得了突破,提供了更详细的空指针异常信息,帮助开发者迅速定位问题源头。



public static void main(String[] args) {
try {
//简单的空指针
String str = null;
str.length();
} catch (Exception e) {
e.printStackTrace();
}
try {
//复杂一点的空指针
var arr = List.of(null);
String str = (String)arr.get(0);
str.length();
} catch (Exception e) {
e.printStackTrace();
}
}

运行结果



推荐指数:⭐️⭐️⭐️⭐️⭐️



3. Records



在Java中,POJO对象(如DO、PO、VO、DTO等)通常包含成员变量及相应的Getter和Setter方法。尽管可以通过工具或IDE生成这些代码,但修改和维护仍然麻烦。Lombok插件为此出现,能够在编译期间自动生成Getter、Setter、hashcode、equals和构造函数等代码,使用起来方便,但对团队有依赖要求。
为此,Java引入了标准解决方案:Records。它通过简洁的语法定义数据类,大大简化了POJO类的编写,如下所示。虽然hashcode和equals方法仍需手动编写,但IDE能够自动生成。这一特性有效解决了模板代码问题,提升了代码整洁度和可维护性。



package com.summo.jdk17;

/**
* 3星
*
* @param stuId 学生ID
* @param stuName 学生名称
* @param stuAge 学生年龄
* @param stuGender 学生性别
* @param stuEmail 学生邮箱
*/

public record StudentRecord(Long stuId,
String stuName,
int stuAge,
String stuGender,
String stuEmail)
{
public StudentRecord {
System.out.println("构造函数");
}

public static void main(String[] args) {
StudentRecord record = new StudentRecord(1L, "张三", 16, "男", "xxx@qq.com");
System.out.println(record);
}
}


推荐指数:⭐️⭐️⭐️⭐️



4. 全新的switch表达式



有人可能问了,Java语言不早已支持switch了嘛,有什么好提的?讲真,这次的提升还真有必要好好地来聊一聊了。在Java12的时候就引入了switch表达式,注意这里是表达式,而不是语句,原来的switch是语句。如果不清楚两者的区别的话,最好先去了解一下。主要的差别就是就是表达式有返回值,而语句则没有。再配合模式匹配,以及yield和“->”符号的加入,全新的switch用起来爽到飞起来。



package com.summo.jdk17;

public class SwitchDemo {
/**
* 在JDK8中获取switch返回值方式
*
* @param week
* @return
*/

public int getByJDK8(Week week) {
int i = 0;
switch (week) {
case MONDAY, TUESDAY:
i = 1;
break;
case WEDNESDAY:
i = 3;
break;
case THURSDAY:
i = 4;
break;
case FRIDAY:
i = 5;
break;
case SATURDAY:
i = 6;
break;
case SUNDAY:
i = 7;
break;
default:
i = 0;
break;
}

return i;
}

/**
* 在JDK17中获取switch返回值
*
* @param week
* @return
*/

public int getByJDK17(Week week) {
// 1, 现在的switch变成了表达式,可以返回值了,而且支持yield和->符号来返回值
// 2, 再也不用担心漏写了break,而导致出问题了
// 3, case后面支持写多个条件
return switch (week) {
case null -> -1;
case MONDAY -> 1;
case TUESDAY -> 2;
case WEDNESDAY -> 3;
case THURSDAY -> {yield 4;}
case FRIDAY -> 5;
case SATURDAY, SUNDAY -> 6;
default -> 0;
};
}

private enum Week {
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
}
}


推荐指数:⭐️⭐️⭐️⭐️



5. 私有接口方法



从Java8开始,允许在interface里面添加默认方法,其实当时就有些小困惑,如果一个default方法体很大怎么办,拆到另外的类去写吗?实在有些不太合理,所以在Java17里面,如果一个default方法体很大,那么可以通过新增接口私有方法来进行一个合理的拆分了,为这个小改进点个赞。



public interface PrivateInterfaceMethod {
    /**
     * 接口默认方法
     */

    default void defaultMethod() {
        privateMethod();
    }

    // 接口私有方法,在Java8里面是不被允许的,不信你试试
    private void privateMethod() {
    }
}


推荐指数:⭐️⭐️⭐️



6. 模式匹配



在JDK 17中,模式匹配主要用于instanceof表达式。模式匹配增强了instanceof的语法和功能,使类型检查和类型转换更加简洁和高效。在传统的Java版本中,我们通常使用instanceof结合类型转换来判断对象类型并进行处理,这往往会导致冗长的代码。



原来的写法


/**
* 旧式写法
*
* @param value
*/

public void matchByJDK8(Object value) {
if (value instanceof String) {
String v = (String)value;
System.out.println("遇到一个String类型" + v.toUpperCase());
} else if (value instanceof Integer) {
Integer v = (Integer)value;
System.out.println("遇到一个整型类型" + v.longValue());
}
}

新的写法


/**
* 转换并申请了一个新的变量,极大地方便了代码的编写
*
* @param value
*/

public void matchByJDK17(Object value) {
if (value instanceof String v) {
System.out.println("遇到一个String类型" + v.toUpperCase());
} else if (value instanceof Integer v) {
System.out.println("遇到一个整型类型" + v.longValue());
}
}


推荐指数:⭐️⭐️⭐️⭐️



7. 集合类的工厂方法



在Java8的年代,即便创建一个很小的集合,或者固定元素的集合都是比较麻烦的,为了简洁一些,有时我甚至会引入一些依赖。



原来的写法


Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("c"

新的写法


Set<String> set = Set.of("a", "b", "c");


推荐指数:⭐️⭐️⭐️⭐️⭐️



二、其他的新特性


1. 新的String方法



  • repeat:重复生成字符串

  • isBlank:不用在引入第三方库就可以实现字符串判空了

  • strip:去除字符串两边的空格,支持全角和半角,之前的trim只支持半角

  • lines:能根据一段字符串中的终止符提取出行为单位的流

  • indent:给字符串做缩进,接受一个int型的输入

  • transform:接受一个转换函数,实现字符串的转换


2. Stream API的增强



增加takeWhile, dropWhile, ofNullable, iterate以及toList的API,越来越像一些函数式语言了。用法举例如下。



// takeWhile 顺序返回符合条件的值,直到条件不符合时即终止继续判断,
// 此外toList方法的加入,也大大减少了节省了代码量,免去了调用collect(Collectors::toList)方法了
List<Integer> list = Stream.of(2,2,3,4,5,6,7,8,9,10)
        .takeWhile(i->(i%2==0)).toList(); // 返回2, 2

// dropWhile 顺序去掉符合条件的值,直到条件不符合时即终止继续判断
List<Integer> list1 = Stream.of(2,2,3,4,5,6,7,8,9,10)
        .dropWhile(i->(i%2==0)).toList(); //返回3, 4, 5, 6, 7, 8, 9, 10

// ofNullable,支持传入空流,若没有这个且传入一个空流,那么将会抛NPE
var nullStreamCount = Stream.ofNullable(null).count(); //返回0

// 以下两行都将输出0到9
Stream.iterate(0, n -> n < 10, n -> n + 1).forEach(x -> System.out.println(x));
Stream.iterate(0, n -> n + 1).limit(10).forEach(x -> System.out.println(x));

3. 全新的HttpClient



这个API首次出现在9之中,不过当时并非是一个稳定版本,在Java11中正式得到发布,所以在Java17里面可以放心地进行使用。原来的JDK自带的Http客户端真的非常难用,这也就给了很多像okhttp、restTemplate、Apache的HttpClient和feign这样的第三方库极大的发挥空间,几乎就没有人愿意去用原生的Http客户端的。但现在不一样了,感觉像是新时代的API了。FluentAPI风格,处处充满了现代风格,用起来也非常地方便,再也不用去依赖第三方的包了,就两个字,清爽。



// 同步请求
HttpClient client = HttpClient.newBuilder()
        .version(Version.HTTP_1_1)
        .followRedirects(Redirect.NORMAL)
        .connectTimeout(Duration.ofSeconds(20))
        .proxy(ProxySelector.of(new InetSocketAddress("proxy.example.com", 80)))
        .authenticator(Authenticator.getDefault())
        .build();
   HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
   System.out.println(response.statusCode());
   System.out.println(response.body());
// 异步请求
HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://foo.com/"))
        .timeout(Duration.ofMinutes(2))
        .header("Content-Type", "application/json")
        .POST(BodyPublishers.ofFile(Paths.get("file.json")))
        .build();
   client.sendAsync(request, BodyHandlers.ofString())
        .thenApply(HttpResponse::body)
        .thenAccept(System.out::println);
 

4. jshell



在新的JDK版本中,支持直接在命令行下执行java程序,类似于python的交互式REPL。简而言之,使用 JShell,你可以输入代码片段并马上看到运行结果,然后就可以根据需要作出调整,这样在验证一些简单的代码的时候,就可以通过jshell得到快速地验证,非常方便。



5. java命令直接执行java文件



在现在可以直接通过执行“java xxx.java”,即可运行该java文件,无须先执行javac,然后再执行java,是不是又简单了一步。



6. ZGC



在ParallelOldGC、CMS和G1之后,JDK 11引入了全新的ZGC(Z Garbage Collector)。这个名字本身就显得很牛。官方宣称ZGC的垃圾回收停顿时间不超过10ms,能支持高达16TB的堆空间,并且停顿时间不会随着堆的增大而增加。那么,ZGC到底解决了什么问题?Oracle官方介绍它是一个可伸缩的低延迟垃圾回收器,旨在降低停顿时间,尽管这可能会导致吞吐量的降低。不过,通过横向扩展服务器可以解决吞吐量问题。官方已建议ZGC可用于生产环境,这无疑将成为未来的主流垃圾回收器。要了解更多,请参阅官方文档



三、小结一下


作为程序员,持续学习和充电非常重要。随着Java8即将停止免费官方支持,越来越多的项目将转向Java17,包括大名鼎鼎的Spring Boot 3.0,它在2022年1月20日发布的第一个里程碑版本(M1)正是基于Java17构建的。该项目依赖的所有组件也将快速升级,未来如果想利用某些新特性,在Java8下将无法通过编译.,到这时候再换就真的晚了... ...


作者:summo
来源:juejin.cn/post/7376444924424241162
收起阅读 »

对象存储URL被刷怕了,看我这样处理

个人项目:社交支付项目(小老板) 作者:三哥,j3code.cn 文档系统:admire.j3code.cn/note 社交支付类的项目,怎么能没有图片上传功能呢! 涉及到文件存储我第一时间就想到了 OSS 对象存储服务(腾讯叫 COS),但是接着我又想到了...
继续阅读 »

个人项目:社交支付项目(小老板)


作者:三哥,j3code.cn


文档系统:admire.j3code.cn/note



社交支付类的项目,怎么能没有图片上传功能呢!


涉及到文件存储我第一时间就想到了 OSS 对象存储服务(腾讯叫 COS),但是接着我又想到了”OSS 被刷 150 T 的流量,1.5 W 瞬间就没了?“。


本来想着是自己搭建一套 MinIO ,但后来一想服务器的开销又要大了,还是作罢了。就在此时,我脑袋突然灵光了一下,既然对象存储的流量是由于资源 url 泄漏导致的外界不停的访问 url 使公网流量剧增从而引起巨额消费,那我能不能不泄露这个 url 呢!


理论上是可以不直接给用户云存储的 url ,那用户如何访问资源?



转换,当用户上传图片时,将云存储的 url 保存入库,而返回用户一个本系统的资源访问接口。当用户访问该接口时,系统从库中获取真实 url 进行资源访问,并返回资源给用户,完成一次转换。


虽然可以解决 url 泄漏问题,但是也是有性能消耗(从直接访问,变为间接访问,而且系统挂了,资源就不可用)。



方案,虽然曲折了点,但为了 money ,牺牲一点是值得的(后来思考了一下,觉得还是有些问题,文章最后会说)。而且即使有人通过刷系统的接口访问资源,也没事,系统有很强的限流和黑名单处理,不会产生过多的公网流量费用的。


那下面我们就先开通相关功能,然后再编码实现。


1、腾讯云对象存储创建


地址:console.cloud.tencent.com/cos


开通对象存储的步骤还是非常简单的,具体步骤如下:


1)开通功能


Snipaste_2023-07-14_15-27-24.png


2)配置存储桶


Snipaste_2023-07-14_15-28-24.png


下一步


Snipaste_2023-07-14_15-30-35.png


下一步


Snipaste_2023-07-14_15-33-10.png


3)创建访问的密钥


腾讯的所有 API 接口都需要这个访问密钥,如果以前创建过就可以直接拿来使用


Snipaste_2023-07-14_15-34-05.png


下一步


Snipaste_2023-07-14_15-35-26.png


基本的功能我们已经开通了,而且以后我们只需向这个存储桶中上传图片即可。


2、SpringBoot 对接对象存储


既然准备工作都已经完成了,那就开始编写上传文件的代码吧!当然,这里我们还是要借助官方文档,便于我们开发,地址如下:



cloud.tencent.com/document/pr…



2.1 配置准备


先来思考一下,对于腾讯 COS 文件上传需要那些配置:



  1. 云 API 的 SecretId 和 SecretKey

  2. 桶名称

  3. 文件上传大小限制

  4. 再加一个 cos 上传后的访问域名


ok,大致就这些,那咱们就先来写个配置文件:application-cos.yml


tx.cos:
# 云 API 的 SecretId
secret-id: ENC(X7Uu6Y0QD6aCeUmNhyqv1jcr8fSN+fqM/FSP/rqhM+6pkbte2LW5gR3wntsm24n3NAg6sIwBC3pqm1lSNWwElc3iuGe3lE4L/k3zih+EstM=)
# 云 API 的 SecretKey
secret-key: ENC(ui3jqYJpyTRtPAizYdtll2Zc1EVzUjK28vjTyD+t3AIydQO6I+JQOVacc5+NJVybsbFptELswKhY55OQLW+BKfujNTOYEM/zb4CMi+AK80w=)
# 域名访问
domain: ENC(oRsaRjwRCVLEYfcNB0CjPGyqSMxGM5uzWnSpSifauLF7c5YMt5hZFi7xAthJI4CjmOLVA810Jbgy8lnkKrXUH0g1ee14cr67xSdtPRy1ZaJOXQOMlBgCKNO2wDBg2YW2)
# 文件上传的桶名称
bucket-name: ENC(TUsQfDEFx6KSAOpRwG7UYOJbGwnFT0Z9tjS4h+/HeenAE3XbhKsCwn3TTo80n5tUUP9Dzrnu+Ck84FNSYQk5fw==)

spring:
servlet:
multipart:
# 限制文件上传大小
max-request-size: 5MB
max-file-size: 5MB

注:这里,我的配置值是加密的,所以你们需要配置自己的值


再根据这个配置文件,写一个对应的配置类:


地址:cn.j3code.common.config


@Slf4j
@Data
@Configuration
@ConfigurationProperties(prefix = "tx.cos")
public class TxCosConfig {
/**
* 访问域名
*/

private String domain;

/**
* 桶名称
*/

private String bucketName;

/**
* api密钥中的secretId
*/

private String secretId;

/**
* api密钥中的应用密钥
*/

private String secretKey;
}

2.2 上传文件代码


这里,我们先实现单个文件的上传,那来思考一下,上传文件应该需要那些步骤:



  1. 校验文件名称

  2. 重新生成一个新文件名称

  3. 腾讯 COS 文件存储路径生成

  4. 文件上传

  5. 拼接文件访问 url


对应此步骤的流程图,如下:


Snipaste_2023-07-16_17-31-32.jpg


1)controller 编写


位置:cn.j3code.other.api.v1.controller


@Slf4j
@AllArgsConstructor
@ResponseResult
@RestController
@RequestMapping(UrlPrefixConstants.WEB_V1 + "/image/upload")
public class ImageUploadController {

private final FileService fileService;


/**
* 图片上传
* @param file 文件
* @return 返回文件 url
*/

@PostMapping("")
public String upload(@RequestParam("file") MultipartFile file){
return fileService.imageUpload(file);
}
}

2)service 编写


位置:cn.j3code.other.service


public interface FileService {
String imageUpload(MultipartFile file);
}

@Slf4j
@AllArgsConstructor
@Service
public class FileServiceImpl implements FileService {

/**
* 允许上传的图片类型
*/

public static final Set<String> IMG_TYPE = Set.of("jpg", "jpeg", "png", "gif");

/**
* 腾讯 cos 配置
*/

private final TxCosConfig txCosConfig;
private final UrlKeyService urlKeyService;

/**
* 图片上传
*
* @param file
* @return
*/

@Override
public String imageUpload(MultipartFile file) {
// 文件名称
String newFileName = getNewFileName(file);

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String format = formatter.format(LocalDate.now());
// key = /用户id/年月日/文件
String key = SecurityUtil.getUserId() + "/" + format + "/" + newFileName;

String prefix = newFileName.substring(0, newFileName.lastIndexOf(".") - 1);
String suffix = newFileName.substring(newFileName.lastIndexOf(".") + 1);
File tempFile = null;
File rename = null;
try {
// 生成临时文件
tempFile = File.createTempFile(prefix, "." + suffix);
file.transferTo(tempFile);
// 重命名文件
rename = FileUtil.rename(tempFile, newFileName, true, true);
// 上传
upload(new FileInputStream(rename), key);
} catch (Exception e) {
log.error("imageUpload-error:", e);
} finally {
if (Objects.nonNull(tempFile)) {
FileUtil.del(tempFile);
}
if (Objects.nonNull(rename)) {
FileUtil.del(rename);
}
}
// 返回访问链接
return initUrl(key);
}

/**
* 初始化图片文件访问 url(本地url和第三方url)
*
* @param key 路径
* @return
*/

private String initUrl(String key) {
// 组装第三方 url
String imageUrl = txCosConfig.getDomain() + "/" + key;

// 保存 url 到 数据库
UrlKey urlKey = new UrlKey()
.setUrl(imageUrl)
.setKey(RandomUtil.randomString(16) + RandomUtil.randomString(16) + RandomUtil.randomString(16))
.setUserId(SecurityUtil.getUserId());

// 保存成功,返回本地中转的 url 出去
boolean save = Boolean.FALSE;
try {
save = urlKeyService.save(urlKey);
} catch (Exception e) {
}

if (save) {
return CallbackUrlConstants.IMAGE_OPEN_URL + urlKey.getKey();
}
// 保存失败,直接把第三方 url 返回给用户
return imageUrl;
}

/**
* 文件上传到第三方
*
* @param fileStream 文件流
* @param path 路径
*/

private void upload(InputStream fileStream, String path) {
PutObjectResult putObjectResult = COSClientUtil.getCosClient(txCosConfig)
.putObject(new PutObjectRequest(txCosConfig.getBucketName(), path, fileStream, null));
log.info("upload-result:{}", JSON.toJSONString(putObjectResult));
}

/**
* 生成一个新文件名称
* 会校验文件名称和类型
*
* @param file 文件
* @return
*/

private String getNewFileName(MultipartFile file) {
String originalFilename = file.getOriginalFilename();
if (StringUtil.isEmpty(originalFilename)) {
throw new SysException("文件名称获取失败!");
}
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));

if (!IMG_TYPE.contains(suffix.substring(1))) {
throw new SysException(String.format("仅允许上传这些类型图片:%s", JSON.toJSONString(IMG_TYPE)));
}

return RandomUtil.randomString(8) + SnowFlakeUtil.getId() + suffix;
}
}

代码写的很详细了,应该能看懂,但,有两点我没有提,就是:COSClientUtil 和 UrlKeyService,下面就来结介绍。


2.2.1 cos 客户端配置提取


系统中肯定有很多的文件上传,难道是每上传一次,就配置一次 cos 客户端吗?显然不是,这个 cos 客户端肯定是要抽出来的,全局系统中我们只配置一次。也即只有第一次过来是创建 cos 客户端,后续过来的文件上传请求直接返回创建好的 cos 客户端就行。


COSClientUtil 类就是我抽的公共 cos 客户获取类,具体实现如下:


位置:cn.j3code.other.util


public class COSClientUtil {

/**
* 统一 cos 上传客户端
*/

private static COSClient cosClient;

public static COSClient getCosClient(TxCosConfig txCosConfig) {
if (Objects.isNull(cosClient)) {
synchronized (COSClient.class) {
if (Objects.isNull(cosClient)) {
// 1 初始化身份
COSCredentials cred = new BasicCOSCredentials(txCosConfig.getSecretId(), txCosConfig.getSecretKey());
// 2 创建配置,及设置地域
ClientConfig clientConfig = new ClientConfig(new Region("ap-guangzhou"));
// 3 生成 cos 客户端。
cosClient = new COSClient(cred, clientConfig);
}
}
}
return cosClient;
}
}

私有构造器,且之对外提供 getCosClient 方法获取 COSClient 对象,保证全局只有一个 cos 客户端配置。


2.2.2 隐藏云存储 URL 处理


还记得 FileServiceImpl 类中有个 UrlKeyService 属性嘛,这个类就是做 云存储 URL 隐藏及中转功能的。


具体做法如图:


Snipaste_2023-07-16_18-14-59.jpg


文件上传部分我们已经写好了,不过有点超前的意思了,不过没关系,看整体就行。


从上面我们要开始抓住一个细节了,就是映射关系,即 key 和 url 的映射。这里我用的是 MySQL 保存,也即用表来存,并没有用 Redis。这里我的考虑是,后续可以把表中的数据定时刷到 Redis 中,接着访问的顺序是从 Redis 中找映射,没有再去 MySQL 中找。


不过,我们首先还是把数据先存表再说,先来看看映射表结构字段:



id


user_id


key


url


create_time


update_time



ok,就这些字段,把用户 id 加上是为了好回溯看看是谁上传了图片。


SQL 如下:


CREATE TABLE `sb_url_key` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`key` varchar(64) COLLATE utf8_unicode_ci NOT NULL COMMENT 'key',
`url` varchar(200) COLLATE utf8_unicode_ci NOT NULL COMMENT '资源url',
`user_id` bigint(20) DEFAULT NULL COMMENT '上传用户',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
KEY `key` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

紧接着就是通过 MyBatisX 插件生成对应的实体、service、mapper 代码了,不过多赘述。那,现在就来开发用户访问图片资源,咱们如何去请求第三方,然后返回用户图片 byte[] 资源数组吧!


1)controller 编写


位置:cn.j3code.other.api.v1.controller


@Slf4j
@AllArgsConstructor
@RestController
@RequestMapping(UrlPrefixConstants.OPEN + "/resource/image")
public class ImageResourceController {

private final UrlKeyService urlKeyService;

/**
* 获取图片 base64
*
* @param key
* @return
* @throws Exception
*/

@GetMapping("/base64/{key}")
public String imageBase64(@PathVariable("key") String key) throws Exception {
UrlKey urlKey = urlKeyService.oneByKey(key);
return "data:image/jpg;base64," + Base64Encoder.encode(IoUtil.readBytes(new URL(urlKey.getUrl()).openStream()));
}


/**
* 获取图片 byte 数组
*
* @param key
* @return
* @throws Exception
*/

@GetMapping(value = "/io/{key}", produces = MediaType.IMAGE_JPEG_VALUE)
public byte[] imageIo(@PathVariable("key") String key) throws Exception {
UrlKey urlKey = urlKeyService.oneByKey(key);

return IoUtil.readBytes(new URL(urlKey.getUrl()).openStream());
}
}

注意:这里写了两个方法,目的是返回两种不同形式的图片资源:base64 和 byte[]。且,这种资源访问的接口,我们系统的相关拦截器请放行,如:认证,ip 记录等拦截器。


2)service 编写


位置:cn.j3code.other.service


public interface UrlKeyService extends IService<UrlKey> {
UrlKey oneByKey(String key);
}
@Service
public class UrlKeyServiceImpl extends ServiceImpl<UrlKeyMapper, UrlKey>
implements UrlKeyService {

@Override
public UrlKey oneByKey(String key) {
UrlKey urlKey = lambdaQuery().eq(UrlKey::getKey, key).one();
if (Objects.isNull(urlKey)) {
throw new SysException(SysExceptionEnum.NOT_DATA_ERROR);
}
return urlKey;
}
}

ok,这样咱们就处理好了,但是仔细想想这种中转的方法有什么问题。


2.3 思考


2.2 节我们已经实现了文件上传和防止 cos 访问 url 泄露的操作,但是我留了个问题,就是思考这种方式有什么问题。


下面是我的思考:



  1. 用户上传的图片,访问时每次都会经过本系统,造成了本系统的压力

  2. 如果一个页面需要回显的图片过多,那页面响应会不会很慢

  3. 如果系统崩溃了或者服务崩溃了,会导致图片不可访问,但其实第三方 url 是没有问题的


好吧,其实上面总结就两个问题,即:性能可用性


这里的解决方法是,如果资金充裕而且 COS 做了黑白名单等之类的防御措施可以直接把 COS 的原始 url 返回出去,没必要把图片资源压力给我我们本系统。如果你不是这种情况,那么就给图片访问接口增加部署资源,即升级服务器增加内存和带款,提高资源访问效率及系统性能。


以上就是本节内容,如果文章的中转方法有啥不足或者您有什么意见,欢迎一起讨论研究。


作者:J3code
来源:juejin.cn/post/7256306281538928701
收起阅读 »

MyBatis居然也有并发问题

为了节省dalaos时间先说结论:确实是个问题,issue链接:github.com/mybatis/myb… 下面就是源码分析环节,及处理过程,感兴趣的可以看看。 bug,任何时候都要解决!不解决不行,你们想想,你早上刚到公司,打开电脑,写着需求听着歌,突...
继续阅读 »

为了节省dalaos时间先说结论:确实是个问题,issue链接:github.com/mybatis/myb…


下面就是源码分析环节,及处理过程,感兴趣的可以看看。



bug,任何时候都要解决!不解决不行,你们想想,你早上刚到公司,打开电脑,写着需求听着歌,突然就被ding了……所以没有bug的日子才是好日子!



日志


上了服务器一看,Mybatis报错,接口还是个相当频繁的接口,一想,完了,绩效大概率不保。


2023-08-08 09:52:05,386|aaaaaaaaa|XXXXXXXXXXXXXX|unknown exception occurred
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.builder.BuilderException: Error evaluating expression 'projects != null and projects.size() > 0 '. Cause: org.apache.ibatis.ognl.MethodFailedException: Method "size" failed for object [aaa,bbb,ccc] [java.lang.IllegalAccessException: Class org.apache.ibatis.ognl.OgnlRuntime can not access a member of class java.util.Arrays$ArrayList with modifiers "public"]
at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:75) ~[mybatis-spring-1.2.2.jar:1.2.2]
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:371) ~[mybatis-spring-1.2.2.jar:1.2.2]
at com.sun.proxy.$Proxy57.selectList(Unknown Source) ~[na:na]
at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:198) ~[mybatis-spring-1.2.2.jar:1.2.2]
at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:119) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:63) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:52) ~[mybatis-3.2.8.jar:3.2.8]
at com.sun.proxy.$Proxy102.queryExperienceCardOrder(Unknown Source) ~[na:na]
// 业务相关堆栈,保险起见不贴了
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.3.6.RELEASE.jar:4.3.6.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:652) ~[spring-aop-4.3.6.RELEASE.jar:4.3.6.RELEASE]
at com.xxxxxxxxxxxxxxxxxxxxxx$$EnhancerBySpringCGLIB$$b85a94bd.queryHasExperienceCardNew(<generated>) ~[zuhao-user-service-1.0.0.jar:na]
at sun.reflect.GeneratedMethodAccessor564.invoke(Unknown Source) ~[na:na]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_131]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_131]
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333) ~[spring-aop-4.3.6.RELEASE.jar:4.3.6.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) [spring-aop-4.3.6.RELEASE.jar:4.3.6.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) [spring-aop-4.3.6.RELEASE.jar:4.3.6.RELEASE]
at xxxxxxxxxxxxx.common.interceptor.ApiInterceptor.invoke(ApiInterceptor.java:79) ~[common-0.0.9-20211228.052440-12.jar:na]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) [spring-aop-4.3.6.RELEASE.jar:4.3.6.RELEASE]
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213) [spring-aop-4.3.6.RELEASE.jar:4.3.6.RELEASE]
at com.sun.proxy.$Proxy185.queryHasExperienceCardNew(Unknown Source) [na:na]
at com.alibaba.dubbo.common.bytecode.Wrapper36.invokeMethod(Wrapper36.java) [na:2.5.3]
at com.alibaba.dubbo.rpc.proxy.javassist.JavassistProxyFactory$1.doInvoke(JavassistProxyFactory.java:46) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.rpc.proxy.AbstractProxyInvoker.invoke(AbstractProxyInvoker.java:72) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.rpc.protocol.InvokerWrapper.invoke(InvokerWrapper.java:53) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.rpc.filter.ExceptionFilter.invoke(ExceptionFilter.java:64) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.monitor.support.MonitorFilter.invoke$original$LFhJaVNd(MonitorFilter.java:65) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.monitor.support.MonitorFilter.invoke$original$LFhJaVNd$accessor$urPnHrIw(MonitorFilter.java) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.monitor.support.MonitorFilter$auxiliary$RJHyKBeq.call(Unknown Source) [dubbo-2.5.3.jar:2.5.3]
at org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstMethodsInter.intercept(InstMethodsInter.java:86) [skywalking-agent.jar:8.16.0]
at com.alibaba.dubbo.monitor.support.MonitorFilter.invoke(MonitorFilter.java) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.rpc.filter.TimeoutFilter.invoke(TimeoutFilter.java:42) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.rpc.protocol.dubbo.filter.TraceFilter.invoke(TraceFilter.java:78) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.rpc.filter.ContextFilter.invoke(ContextFilter.java:60) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.rpc.filter.GenericFilter.invoke(GenericFilter.java:112) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.rpc.filter.ClassLoaderFilter.invoke(ClassLoaderFilter.java:38) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.rpc.filter.EchoFilter.invoke(EchoFilter.java:38) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol$1.reply(DubboProtocol.java:108) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeHandler.handleRequest(HeaderExchangeHandler.java:84) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeHandler.received(HeaderExchangeHandler.java:170) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.remoting.transport.DecodeHandler.received(DecodeHandler.java:52) [dubbo-2.5.3.jar:2.5.3]
at com.alibaba.dubbo.remoting.transport.dispatcher.ChannelEventRunnable.run(ChannelEventRunnable.java:82) [dubbo-2.5.3.jar:2.5.3]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_131]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_131]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_131]
Caused by: org.apache.ibatis.builder.BuilderException: Error evaluating expression 'projects != null and projects.size() > 0 '. Cause: org.apache.ibatis.ognl.MethodFailedException: Method "size" failed for object [u号租, 租号牛, 租号酷] [java.lang.IllegalAccessException: Class org.apache.ibatis.ognl.OgnlRuntime can not access a member of class java.util.Arrays$ArrayList with modifiers "public"]
at org.apache.ibatis.scripting.xmltags.OgnlCache.getValue(OgnlCache.java:47) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.scripting.xmltags.ExpressionEvaluator.evaluateBoolean(ExpressionEvaluator.java:32) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.scripting.xmltags.IfSqlNode.apply(IfSqlNode.java:33) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.scripting.xmltags.MixedSqlNode.apply(MixedSqlNode.java:32) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.scripting.xmltags.TrimSqlNode.apply(TrimSqlNode.java:54) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.scripting.xmltags.MixedSqlNode.apply(MixedSqlNode.java:32) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.scripting.xmltags.TrimSqlNode.apply(TrimSqlNode.java:54) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.scripting.xmltags.MixedSqlNode.apply(MixedSqlNode.java:32) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.scripting.xmltags.DynamicSqlSource.getBoundSql(DynamicSqlSource.java:40) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.mapping.MappedStatement.getBoundSql(MappedStatement.java:278) ~[mybatis-3.2.8.jar:3.2.8]
at com.github.pagehelper.PageInterceptor.intercept(PageInterceptor.java:83) ~[pagehelper-5.1.4.jar:na]
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:60) ~[mybatis-3.2.8.jar:3.2.8]
at com.sun.proxy.$Proxy234.query(Unknown Source) ~[na:na]
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:108) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:102) ~[mybatis-3.2.8.jar:3.2.8]
at sun.reflect.GeneratedMethodAccessor347.invoke(Unknown Source) ~[na:na]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_131]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_131]
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:358) ~[mybatis-spring-1.2.2.jar:1.2.2]
... 54 common frames omitted
Caused by: org.apache.ibatis.ognl.MethodFailedException: Method "size" failed for object [aaa,bbb,ccc]
at org.apache.ibatis.ognl.OgnlRuntime.callAppropriateMethod(OgnlRuntime.java:837) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.ognl.ObjectMethodAccessor.callMethod(ObjectMethodAccessor.java:61) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.ognl.OgnlRuntime.callMethod(OgnlRuntime.java:860) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.ognl.ASTMethod.getValueBody(ASTMethod.java:73) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:170) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:210) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.ognl.ASTChain.getValueBody(ASTChain.java:109) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:170) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:210) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.ognl.ASTGreater.getValueBody(ASTGreater.java:49) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:170) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:210) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.ognl.ASTAnd.getValueBody(ASTAnd.java:56) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:170) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:210) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.ognl.Ognl.getValue(Ognl.java:333) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.ognl.Ognl.getValue(Ognl.java:310) ~[mybatis-3.2.8.jar:3.2.8]
at org.apache.ibatis.scripting.xmltags.OgnlCache.getValue(OgnlCache.java:45) ~[mybatis-3.2.8.jar:3.2.8]
... 72 common frames omitted


赶紧查了下这个接口的调用情况,大部分没问题,偶尔冒了这么个错(还好还好)


根据堆栈反查错误位置,有点想不通,这里会有问题?那就只能翻源码了



源码分析


经过排查,ognl表达式中用到的方法,会通过反射,获取method,并缓存至静态变量中,所以,存在多线程状态中,产生并发问题,往下看



这里是缓存方法的逻辑,org.apache.ibatis.ognl.OgnlRuntime#getMethods(java.lang.Class, boolean)感兴趣的可以自己看


这里就是bug点,如果调用一旦多,存在A线程修改成true,还没调用方法,B线程就修改成false,此时调用失败,这不是个坑吗


image.png
mybatis中一搜果然有这个issue:github.com/mybatis/myb…



作者给的方案呢是升级mybatis。




你以为就这样结束了?


升级是不可能升级的,这辈子都不可能升级的,代码这么稳定,行行都像诗句[狗头]


开个玩笑,看看如何避免



原因呢就是Arrays.asList返回的是内部类,是private。所以导致了(!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers()))这个条件为true,进入了设置
accessible的逻辑,后面又给设置回原样


总结



  • 问题:如果需要ognl的对象的方法和类不是public,那么会存在并发问题

  • 解决1:针对并发问题,升级Mybatis

  • 解决2:Lists.newArrayList或者其他写法代替,反正看下,内部类是不是private


有问题希望留言指出哈


作者:山间小僧
来源:juejin.cn/post/7264921613551730722
收起阅读 »

在Spring Boot中使用Sa-Token实现路径拦截和特定接口放行

在Spring Boot中使用Sa-Token实现路径拦截和特定接口放行 很喜欢的一段话:别想太多,好好生活,也许日子过着过着就会有答案,努力走着走着就会有温柔的着落。 春在路上,花在枝上,所有的美好都在路上,努力过好自己的生活,偶尔慌乱,偶尔平稳,都各有...
继续阅读 »

在Spring Boot中使用Sa-Token实现路径拦截和特定接口放行


在这里插入图片描述



很喜欢的一段话:别想太多,好好生活,也许日子过着过着就会有答案,努力走着走着就会有温柔的着落。
春在路上,花在枝上,所有的美好都在路上,努力过好自己的生活,偶尔慌乱,偶尔平稳,都各有滋味,怀着诚恳,好好努力好好生活,闲事勿虑,别让鸡零狗碎的破事,耗尽你对美好生活的所有向往。



1. 引入依赖


首先,在pom.xml文件中引入Sa-Token相关的依赖。Sa-Token是一个轻量级的Java权限认证框架,可以帮助我们轻松实现用户登录状态的管理和权限认证。


<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.27.0</version>
</dependency>

2. 创建配置类 SecurityProperties


定义一个配置类SecurityProperties,用于读取和存储从配置文件中加载的排除路径信息。这里使用了Spring Boot的@ConfigurationProperties注解来绑定配置文件中的属性。


import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import lombok.Data;

@Data
@Component
@ConfigurationProperties(prefix = "security")
public class SecurityProperties {
/**
* 排除路径
*/

private String[] excludes;
}


  • @Data:这是Lombok的注解,自动生成getter和setter方法。

  • @Component:将该类注册为Spring的组件。

  • @ConfigurationProperties:指定前缀security,从配置文件中读取以该前缀开头的属性,并将这些属性映射到该类的字段上。


3. 编写配置文件


在配置文件application.yml或者application.properties中,配置需要排除的路径。例如:


application.yml:


security:
excludes:
- "/public/**"
- "/login"
- "/register"

application.properties:


security.excludes=/public/**,/login,/register


  • /public/**:排除所有以/public/开头的路径。

  • /login:排除/login路径。

  • /register:排除/register路径。


4. 配置拦截器


创建一个配置类WebConfig,实现WebMvcConfigurer接口,在其中配置Sa-Token的拦截器,并将排除的路径应用到拦截器中。


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Autowired
private SecurityProperties securityProperties;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handler -> {
// 获取所有的URL并进行检查
SaRouter.match("/**").check(() -> {
// 检查是否登录
StpUtil.checkLogin();
});
}))
.addPathPatterns("/**") // 拦截所有路径
.excludePathPatterns(securityProperties.getExcludes()); // 排除指定路径
}
}


  • @Configuration:标识这是一个配置类。

  • addInterceptors:重写该方法,向Spring的拦截器注册中心添加自定义的拦截器。

  • SaInterceptor:Sa-Token提供的拦截器,主要用于权限验证。

  • SaRouter.match("/**"):匹配所有路径。

  • StpUtil.checkLogin():Sa-Token提供的登录状态检查方法,用于验证用户是否已登录。

  • excludePathPatterns:从拦截中排除指定的路径,这些路径从SecurityProperties中获取。


5. 验证拦截效果


启动Spring Boot应用程序,验证配置是否生效。以下是一些测试步骤:



  1. 访问排除路径



    • 尝试访问配置文件中排除的路径,如/public/**/login/register

    • 这些路径应不会触发登录检查,可以直接访问。



  2. 访问其他路径



    • 尝试访问其他未排除的路径,如/admin/user/profile等。

    • 这些路径应触发Sa-Token的登录验证逻辑,如果用户未登录,将会被拦截,并返回相应的未登录提示。




代码解析



  • SecurityProperties:通过@ConfigurationProperties注解,Spring Boot会自动将前缀为security的配置属性绑定到该类的excludes字段上,从而实现排除路径的配置。

  • 配置文件:在配置文件中定义需要排除的路径,以便动态加载到SecurityProperties中。

  • WebConfig:实现WebMvcConfigurer接口,通过addInterceptors方法添加Sa-Token的拦截器,并使用excludePathPatterns方法将配置文件中定义的排除路径应用到拦截器中。


详细解释


依赖配置


Sa-Token是一个轻量级的权限认证框架,可以帮助我们轻松实现用户登录状态的管理和权限认证。通过引入sa-token-spring-boot-starter依赖,我们可以很方便地将其集成到Spring Boot项目中。


配置类 SecurityProperties


SecurityProperties类的作用是将配置文件中定义的排除路径读取并存储到excludes数组中。通过使用@ConfigurationProperties注解,我们可以将前缀为security的属性绑定到该类的excludes字段上。这样做的好处是,排除路径可以通过配置文件进行动态配置,方便管理和维护。


配置文件


在配置文件中,我们定义了需要排除的路径。这些路径将不会被拦截器拦截,可以直接访问。配置文件支持YAML格式和Properties格式,根据项目需要选择合适的格式进行配置。


拦截器配置


WebConfig类中,我们实现了WebMvcConfigurer接口,并重写了addInterceptors方法。在该方法中,我们创建了一个Sa-Token的拦截器,并通过SaRouter.match("/**")匹配所有路径。对于匹配到的路径,我们使用StpUtil.checkLogin()方法进行登录状态检查。如果用户未登录,将会被拦截,并返回相应的未登录提示。


通过excludePathPatterns方法,我们将从SecurityProperties中获取的排除路径应用到拦截器中。这样一来,配置文件中定义的排除路径将不会被拦截器拦截,可以直接访问。


总结


通过本文的介绍,我们了解了如何在Spring Boot中使用Sa-Token实现路径拦截和特定接口放行。我们首先引入了Sa-Token的依赖,然后定义了一个配置类SecurityProperties,用于读取和存储排除路径信息。接着,在配置文件中定义了需要排除的路径,并在WebConfig类中配置了Sa-Token的拦截器,将排除路径应用到拦截器中。最后,通过测试和验证,确保配置生效,实现了对特定路径的放行和其他路径的权限验证。


这种方式可以帮助开发者更灵活地管理Web应用中的访问控制,提升系统的安全性和可维护性。如果你有更多的自定义需求,可以根据Sa-Token的文档进行进一步配置和扩展。


作者:IT小辉同学
来源:juejin.cn/post/7379117970797183030
收起阅读 »

为什么阿里巴巴为什么不推荐使用keySet()进行遍历HashMap?

引言 HashMap相信所有学Java的都一定不会感到陌生,作为一个非常重用且非常实用的Java提供的容器,它在我们的代码里面随处可见。因此遍历操作也是我们经常会使用到的。HashMap的遍历方式现如今有非常多种: 使用迭代器(Iterator)。 使用 k...
继续阅读 »

引言


HashMap相信所有学Java的都一定不会感到陌生,作为一个非常重用且非常实用的Java提供的容器,它在我们的代码里面随处可见。因此遍历操作也是我们经常会使用到的。HashMap的遍历方式现如今有非常多种:



  1. 使用迭代器(Iterator)。

  2. 使用 keySet() 获取键的集合,然后通过增强的 for 循环遍历键。

  3. 使用 entrySet() 获取键值对的集合,然后通过增强的 for 循环遍历键值对。

  4. 使用 Java 8+ 的 Lambda 表达式和流。


以上遍历方式的孰优孰劣,在《阿里巴巴开发手册》中写道:


image.png


这里推荐使用的是entrySet进行遍历,在Java8中推荐使用Map.forEach()。给出的理由是遍历次数上的不同。



  1. keySet遍历,需要经过两次遍历。

  2. entrySet遍历,只需要一次遍历。



其中keySet遍历了两次,一次是转为Iterator对象,另一次是从hashMap中取出key所对应的value。



其中后面一段话很好理解,但是前面这句话却有点绕,为什么转换成了Iterator遍历了一次?


我查阅了各个平台对HashMap的遍历,其中都没有或者原封不动的照搬上句话。(当然也可能是我没有查阅到靠谱的文章,欢迎指正)


keySet如何遍历了两次


我们首先写一段代码,使用keySet遍历Map。


public class Test {


public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("k1", "v1");
map.put("k2", "v2");
map.put("k3", "v3");
for (String key : map.keySet()) {
String value = map.get(key);
System.out.println(key + ":" + value);
}
}

}

运行结果显而易见的是


k1:v1
k2:v2
k3:v3

两次遍历,第一次遍历所描述的是转为Iterator对象我们好像没有从代码中看见,我们看到的后面所描述的遍历,也就是遍历map,keySet()所返回的Set集合中的key,然后去HashMap中拿取value的。


Iterator对象呢?如何遍历转换为Iterator对象的呢?


image.png


首先我们这种遍历方式大家都应该知道是叫:增强for循环,for-each


这是一种Java的语法糖~。可以看上篇文章了解~


我们可以通过反编译,或者直接通过Idea在class文件中查看对应的Class文件


image.png
public class Test {
public Test() {
}

public static void main(String[] args) {
Map<String, String> map = new HashMap();
map.put("k1", "v1");
map.put("k2", "v2");
map.put("k3", "v3");
Iterator var2 = map.keySet().iterator();

while(var2.hasNext()) {
String key = (String)var2.next();
String value = (String)map.get(key);
System.out.println(key + ":" + value);
}

}
}

和我们编写的是存在差异的,其中我们可以看到其中通过map.keySet().iterator()获取到了我们所需要看见的Iterator对象。


那么它又是怎么转换成的呢?为什么需要遍历呢?我们查看iterator()方法


iterator()


image.png

发现是Set定义的一个接口。返回此集合中元素的迭代器


HashMap.KeySet#iterator()


我们查看HashMap中keySet类对该方法的实现。


image.png
image.png
    final class KeySet extends AbstractSet<K> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<K> iterator() { return new KeyIterator(); }
public final boolean contains(Object o) { return containsKey(o); }
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
public final Spliterator<K> spliterator() {
return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super K> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}

其中的iterator()方法返回的是一个KeyIterator对象,那么究竟是在哪里进行了遍历呢?我们接着往下看去。


HashMap.KeyIterator


image.png
    final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}

这个类也很简单:



  1. 继承了HashIterator类。

  2. 实现了Iterator接口。

  3. 一个next()方法。


还是没有看见哪里进行了遍历,那么我们继续查看HashIterator


HashMap.HashIterator


image.png
    abstract class HashIterator {
Node<K,V> next; // next entry to return
Node<K,V> current; // current entry
int expectedModCount; // for fast-fail
int index; // current slot

HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);
}
}

public final boolean hasNext() {
return next != null;
}

final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}

public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}

我们可以发现这个构造器中存在了一个do-while循环操作,目的是找到一个第一个不为空的entry


        HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);
}
}

KeyIterator是extendHashIterator对象的。这里涉及到了继承的相关概念,大家忘记的可以找相关的文章看看,或者我也可以写一篇~~dog。


例如两个类


public class Father {

public Father(){
System.out.println("father");
}
}

public class Son extends Father{

public static void main(String[] args) {
Son son = new Son();
}
}

创建Son对象的同时,会执行Father构造器。也就会打印出father这句话。


那么这个循环操作就是我们要找的循环操作了。


总结



  1. 使用keySet遍历,其实内部是使用了对应的iterator()方法。

  2. iterator()方法是创建了一个KeyIterator对象。

  3. KeyIterator对象extendHashIterator对象。

  4. HashIterator对象的构造方法中,会遍历找到第一个不为空的entry



keySet->iterator()->KeyIterator->HashIterator



大家想更清楚了解这个entry是什么?可以看我的HashMap文章~。文章如果存在错误,欢迎大家评论区指正~~


image.png


作者:以范特西之名
来源:juejin.cn/post/7295353579002396726
收起阅读 »

盘点Lombok的几个骚操作

前言 本文不讨论对错,只讲骚操作。 有的方法看看就好,知道可以这么用,但是否应用到实际开发中,那就仁者见仁,智者见智了。 一万个读者就会有一万个哈姆雷特,希望这篇文章能够给您带来一些思考。 耐心看完,你一定会有所收获。 正文 @onX 例如 onConstr...
继续阅读 »

前言


本文不讨论对错,只讲骚操作。


有的方法看看就好,知道可以这么用,但是否应用到实际开发中,那就仁者见仁,智者见智了。


一万个读者就会有一万个哈姆雷特,希望这篇文章能够给您带来一些思考。


耐心看完,你一定会有所收获。


giphy (2).gif


正文


@onX


例如 onConstructor, oMethod, 和 onParam 允许你在生成的代码中注入自定义的注解。一个常见的用例是结合 Spring 的 @Autowired


在 Spring 的组件(如 @Service@Controller@Component@Repository 等)中使用 @RequiredArgsConstructor(onConstructor = @__(@Autowired)),可以让 Lombok 在生成构造函数时也加上 @Autowired 注解,这样,Spring 就可以自动注入所需的依赖。


例如下面这段代码


@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class MyService {
private final AnotherService anotherService;
}

上述代码片段使用 Lombok 和 Spring 注解,Lombok 会为其生成以下代码


@Service
public class MyService {
private final AnotherService anotherService;

@Autowired
public MyService(AnotherService anotherService) {
this.anotherService = anotherService;
}
}


从生成的代码中可以看出:



  • MyService 生成了一个构造函数,该构造函数接受一个 AnotherService 类型的参数。

  • 由于构造函数上有 @Autowired 注解,Spring 会自动查找合适的 AnotherService bean 实例并注入到 MyService 中。


这种方式结合了 Lombok 的自动代码生成功能和 Spring 的依赖注入功能,使得代码更为简洁。


但是,使用此技巧时要确保团队成员都理解其背后的含义,以避免混淆。


@Delegate


@Delegate可以让你的类使用其他类的方法,而不需要自己写代码。


比如,你有一个类叫做A,它有一个方法叫做sayHello(),你想让另一个类B也能用这个方法,那就可以在B类中加上一个A类型的字段,并在这个字段上加上@Delegate注解,这样,B类就可以直接调用sayHello()方法,就像它是自己的方法一样。看个例子:


// 一个类,有一个方法
public class A {
public void sayHello() {
System.out.println("Hello");
}
}

// 一个类,委托了A类的方法
public class B {
@Delegate // 委托A类的方法
private A a = new A();

public static void main(String[] args) {
B b = new B();
b.sayHello(); // 调用A类的方法
}
}

这样写最大的好处就是可以避免类的层次过深或者耦合过紧,提高代码的可读性和可维护性,各种继承来继承去是真的看得头疼。


@Cleanup


@Cleanup可以自动管理输入输出流等各种需要释放的资源,确保安全地调用close方法。


它的使用方法是在声明的资源前加上@Cleanup,例如:


@Cleanup InputStream in = new FileInputStream("some/file");

这样,当你的代码执行完毕后,Lombok会自动在一个try-finally块中调用in.close()方法,释放资源。


如果要释放资源的方法名不是close,也可以指定要调用的方法名,例如:


@Cleanup("release") MyResource resource = new MyResource();

Lombok会自动在try-finally块中调用resource.release()方法,释放资源。


可以看到,这比手动写try-finally要简洁得太多了,只要使用@Cleanup就能管理任何有无参方法的资源,指定正确的方法名即可。


@Singular 和 @Builder 组合


@Builder让你的类支持链式构造,而@Singular让集合类型字段可以更方便的维护。


@Singular注解可以用在集合类型的字段上,它会生成两个方法,一个是添加单个元素的方法,一个是添加整个集合的方法。这两个方法可以和 @Builder 生成的其他方法一起链式调用,给你的类的所有字段赋值。


这么讲可能有点懵,直接看示例:


@Data
@Builder
public class User {
private String name;
private int age;
@Singular
private List<String> hobbies;
}

// 使用 @Builder 和 @Singular 生成的方法
User user = User.builder()
.name("练习时长两年半")
.age(28)
.hobby("篮球") // 添加单个元素
.hobby("唱歌") // 添加单个元素
.hobbies(Arrays.asList("跳舞", "其他")) // 添加整个集合
.build(); // 构造 User 对象

可以看出,使用 @Singular 注解的好处是,你可以灵活地添加集合类型的字段,而不需要自己创建和初始化集合对象。


另外,使用 @Singular 注解生成的集合字段,在调用 build() 方法后,会被转换为不可变的集合,这样可以保证对象的不变性和线程安全性。你也可以使用 clear() 方法来清空集合字段,例如:


User user = User.builder()
.name("签")
.age(28)
.hobby("说唱")
.hobby("跳舞")
.clearHobbies() // 清空集合字段
.hobby("踩缝纫机") // 重新添加元素
.build();

但需要注意的是,如果你的类继承了一个父类,那么 @Builder 只会生成当前类的字段和参数,不包括父类的。


结尾


请注意,尽管 Lombok 提供了许多方便的功能,但过度使用不当使用可能会导致代码难以理解和维护。


因此,在使用这些功能时,务必始终保持审慎,并且要充分考虑其影响。


作者:一只叫煤球的猫
来源:juejin.cn/post/7322724142779252762
收起阅读 »

网上被吹爆的Spring Event事件订阅有缺陷,一个月内我被坑了两次!

Spring Event事件订阅框架,被网上一些人快吹上天了,然而我们在新项目中引入后发现,这个框架缺陷很多,玩玩可以,千万不要再公司项目中使用。还不如自己手写一个监听者设计模式,那样更稳定、可靠。 之前我已经被Spring Event(事件发布订阅组件)坑过...
继续阅读 »

Spring Event事件订阅框架,被网上一些人快吹上天了,然而我们在新项目中引入后发现,这个框架缺陷很多,玩玩可以,千万不要再公司项目中使用。还不如自己手写一个监听者设计模式,那样更稳定、可靠。


之前我已经被Spring Event(事件发布订阅组件)坑过一次。那次是在服务关闭期间,有请求未处理完成,当调用Spring Event时,出现异常。


根源是:Spring关闭期间,不得调用GetBean,也就是无法使用Spring Event 。详情点击这里查看


然而新项目大量使用了Spring Event,在另一个Task服务还未来得及移除Spring Event的情况下,出现了类似的问题。


当领导听说新引入的Spring Event再次出现问题时,非常愤怒,因为一个月内出现了两次故障。在复盘会议上,差点爆粗口。


在上线过程中,丢消息了?


“五哥,你看一眼钉钉给你发的监控截图,线上好像有丢消息?” 旁边同事急匆匆的跟我说。


“线上有问题?强哥在上线,我让他先暂停下~”,于是我赶紧通知强哥,先暂停发布。优先排查线上问题~


怎么会有问题呢?我有点意外,正好我和强哥各有代码上线,我只改动很小一段代码。我对这次代码变更很自信,坚信不会有问题,所以我并没有慌乱和紧张。搁之前,我的小心脏早就怦怦跳了!


诡异的情况


出现问题的业务逻辑是 消费A 消息,经过业务处理后,再发送B消息。


image.png
从线上监控和日志分析,Task服务收到了 A 消息,然后处理失败了。诡异之处是没有任何异常日志和异常打点,仿佛凭空消失了。


分析代码分支后,我和同事十分确信,任何异常退出的代码分支都有打印异常日志和上报异常监控打点,出现异常不可能不留一丝痕迹。


正当陷入困境之时,我们发现蹊跷之处。“丢消息”的时间只有 3秒钟,之后便恢复正常。问题出在启动阶段,消息A进入Task服务,服务还未完全发布完成时,导致不可预测的情况发生。


当分析Spring 源代码以后,我们发现原因出在 Spring Event……


在详细说明问题根源前,我简单介绍一下 SpringEvent使用,熟悉它的读者,可以自行跳过。


Spring Event的简单使用


声明事件


自定义事件需要继承Spring ApplicationEvent。我选择使用泛型,支持子类可以灵活关联事件的内容。


public class BaseEvent<T> extends ApplicationEvent {
private final T data;

public BaseEvent(T source) {
super(source);
this.data = source;
}

public T getData() {
return data;
}
}

发布事件


使用Spring上下文 ApplicationContext发布事件


applicationContext.publishEvent(new BaseEvent<>(param));

Idea为Spring提供了跳转工具,点击绿色按钮位置,就可以 跳转到事件的监听器列表。


image.png


监听事件


监听器只需要 在方法上声明为 EventListener注解,Spring就会自动找到对应的监听器。Spring会根据方法入参的事件类型和 发布的事件类型 自动匹配。


@EventListener
public void handleEvent(BaseEvent<PerformParam> event) {
//消费事件
}

服务启动阶段,Spring Event 注册严重滞后


在Kafka 消费逻辑中,通过Spring Event发布事件,业务逻辑都封装在 Event Listenr 中。经过分析和验证后,我们终于发现问题所在。


当Kafka 消费者已经开始消费消息,但Spring Event 监听者还没有注册到Spring ApplicationContext中, 所以Spring Event 事件发布后,没有Event Listener消费该事件。3秒钟以后,Event Listener被注册到Spring后,异常就消失了。


问题根源在:Event Listener 注册的时间点滞后于 init-method 的时间点!


image.png


init-method ——— Kafka 开始监听的时间点


Kafka 消费者的启动点 在 Spring init-method中,例如下面的 XML中,init-method 声明 HelloConsumer 的初始化方法为 init方法。在该方法中注册到Kafka中,抢占分片,开始消费消息。


<bean id="kafkaConsumer" class="com.helloworld.KafkaConsumer" init-method="init" destroy-method="destroy">


如果在init-method 方法中,成功注册到Kafka,抢占到分片,然而 Spring Event Listener还未注册到Spring ,就会 “Spring事件丢失” 的现象。


EventListener注册到Spring 的时间点


在Spring的启动过程中,EventListener 的启动点滞后于 init-method 。如下图Spring的启动顺序所示。


其中init-methodInitializingBean中被触发,而 EventListenerSmartInitializingSingleton 中初始化。由于启动顺序的先后关系,当init-method的执行时间较长时(例如连接超时),就会出现Kafka已开始消费,但EventListener还未注册的问题。


Spring 启动顺序
image.png


InitializingBean 的初始化代码


通过分析 Spring源代码。InitializingBean 阶段, invokeInitMethod 会执行init-method方法,Kafka消费者就是在init-method 执行完成后开始消费kafka消息。
image.png


SmartInitializingSingleton


继续分析Spring源代码。 EventListenerMethodProcessorSmartInitializingSingleton 子类,该类负责解析Spring 中所有的Bean,如果有方法添加EventListener注解,则将 EventListener方法 注册到 Spring 中


以下是代码截图
image.png


Spring Event很好,我劝你别用


通过代码分析可以发现,在Spring中,init-method方法会先执行,然后才会解析和注册Event Listener。因此,在消费Kafka和注册EventListener之间存在一个时间间隔,如果在这期间发布了Spring Event,该事件将无法被消费。


通常情况下,这个时间间隔非常短暂,但是当init-method执行较慢时,比如Kafka消费者 A 初始化很快,但是Kafka消费者 B 建立连接超时导致init-method执行时间较长,就会出现问题。在这段时间内,Kafka消费者 A 发布的Spring事件无法被消费。


尽管这不是一个稳定必现的问题,但是当线上流量较大时,它发生的概率会增加,后果也会更严重。我们在上线3个月后,线上环境才首次遇到这个问题。


《服务关闭期,Spring Event消费失败》这篇文章中,有读者评论提到了这个问题。


image.png



有朋友说: 这和spring event有什么关系,自己实现一套,不也有同样的问题吗?关键是得优雅停机啊!



他所说的是正确的,如果服务能够完美地进行优雅发布,即使是在大流量场景下,Spring Event也不会出现问题。


一般情况下,公司的项目通常会在 init-method 方法中,统一初始化消息队列 MQ 消费者。如果想要安全地使用Spring Event,必须等到Spring完全发布完成之后才能初始化 Kafka 消费者。


对于公司的项目来说,稳定性非常重要。引入 SpringEvent 前,一定要确保服务的入口流量在正确的时点开启。


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

WSPA台灣分部在2024年第二季度以6億美元TvPv表現亮眼

根據歐盟總部最新財務報表數據顯示,Wisdom Square Prosperous Ark Fintech (WSPA)台灣分部在2024年第二季度(Q2)創下驚人的6億美元交易量收益率(TvPv)。這一卓越的表現獲得了歐盟高層的高度認可,並在最近召開的股東會...
继续阅读 »

根據歐盟總部最新財務報表數據顯示,Wisdom Square Prosperous Ark Fintech (WSPA)台灣分部在2024年第二季度(Q2)創下驚人的6億美元交易量收益率(TvPv)。這一卓越的表現獲得了歐盟高層的高度認可,並在最近召開的股東會上宣布,將釋出25個策略案名額,供台灣分部社群用戶使用。為了表彰台灣分部在今年的傑出表現,這25個策略案被統一命名為「QCA藍圖策略案」。這不僅是對台灣分部成績的讚揚,也是對其在歐盟WSPA集團中突出貢獻的一種榮譽表彰。這一特別命名顯示了歐盟對台灣分部的高度重視以及其在金融領域中的卓越表現。

這25個名額將通過線上或線下預約方式提供,這是一次極為珍貴的機會。參與者將有機會獲得獨特的策略案和專業指導,從中學習最前沿的財務戰略和技術支持。WSPA集團希望通過這次機會,讓台灣分部的社群用戶受益於最新的財務戰略和技術支持,進一步提升他們的競爭力和市場影響力。此次釋出的「QCA藍圖策略案」不僅是對台灣分部過去成績的肯定,更是WSPA集團對其未來發展的期許。這些策略案將為台灣分部社群用戶提供獨特的財務戰略洞見和專業支持,幫助他們在全球金融市場中持續保持競爭優勢。

收起阅读 »

我是DB搬运工,我哪会排查问题。。。

今天说说如何排查线上问题,首先声明如果想看什么cpu优化。jvm优化的,我这不适合,我这属于广大底层人士的,纯纯的CRUD,没那么多的性能优化; 开干 报错信息的问题 首先说一个报错信息的问题:对于线上显而易见的界面提示错误,我们要完全避免不要将后台的报错打到...
继续阅读 »

今天说说如何排查线上问题,首先声明如果想看什么cpu优化。jvm优化的,我这不适合,我这属于广大底层人士的,纯纯的CRUD,没那么多的性能优化;


开干


报错信息的问题


首先说一个报错信息的问题:对于线上显而易见的界面提示错误,我们要完全避免不要将后台的报错打到前台界面上来,不要将后台的报错打到前台界面上来,不要将后台的报错打到前台界面上来,重要的说三遍,我看到很多线上生产系统报出java报错信息和php报错信息了;外人来看可能看不懂,觉得炫酷,内行人看简直了,垮diao;类似于我找的这个网图


image.png


如何排查问题


再说下我们开发人员前后端都写的情况下如何排查问题,对于前后端都开发的人员其实避免了很多扯皮的事情,也少了很多沟通的问题,如果我们环境点击报错,我们可以



  1. 打开浏览器的f12查看该请求的地址

  2. 按该地址找到后台对应的接口地址,启动本地,打上断点

  3. 如果没有走进后台断点处那么存在三个问题,一个是contentType或者请求方式两者没有保持一致,这个一般开发自测的时候就可以测出来,另一个就是你的地址可能中间环节有路由,路由有问题一般对于大部分功能都有影响,不会是小范围的,还有一种就是我们的后台有拦截器但是我们不熟悉这块,一般大家接手项目的时候估计只会扫一眼这块,恰好这块对于某些业务权限卡的很的项目来说会经常发生这种事,而你恰好不熟悉所以你排查半天也不会有头绪;

  4. 进入断点以后,我们按流程往下执行就能找到报错的地方了

  5. 如果你日志打的详细而且也可以轻松获取生产的日志,那就在日志中就可以找到我们报错的信息;

  6. 如果你是传回前台后报错,那么我们需要在浏览器上打断点,然后去定位是不是咱们传的参数和前台解析的参数属性不一致还是一些其他的问题,以上就形成了闭环;


如果我们是只写后端,分离项目的那种,那咱们就是加强沟通,和气生财,一切问题出在我后端,前端都是完美的,来问题了你先排查起来,确定没问题了,再去告诉项目大哥,让前端兄弟排查一下,有些新手可能会问为什么不让前端先排查,这个其实不该问,只要是前后端分离的,业务层其实都是摆在后端的,而问题大部分是出在业务上的,所以后端干就完了;


image.png
如果我们使用了一些中间件,要没事带关注这些玩意,有时候大家共用的Redis,你不知道别人怎么操作,然后Redis崩了,你能怎么办,如果你是业务前置部门,虽然与你无瓜,但客户的感知就是你报错了,别人躲在后面到不了那一步,所以你得去各方联系重启机器;


ABUIABACGAAg9b-EhwYo0omnkwUwkAM4kAM.jpg


项目执行过程真的报oom了呢,那你必须去生产环境捞日志,找到位置,看看机器配置,看看项目执行占用资源情况,纯小白方式直接top命令查看,资源的确给的少了,那么我们启动的时候调整下jvm参数,把它调大,如果是代码执行循环导致的,那么我们就得优化代码,如果是执行任务之类的,比如给个无界队列,那么队列也会把数据撑爆,这时候我们也需要调整业务逻辑,(**记住,队列撑爆内存千万别直接把队列弄成有界的,一定要去沟通怎么优化,得到认可才能干,我们开发对于业务场景是没有产品经理清晰的**)这种挤爆jvm的不是那么多见,但的确很长见识的;


部署打包


排查完、修改完我们就要打包了,其实我特别不建议本地打包那种方式(应该禁止),万一哪个卧龙本地打包后认为活结束了然后忘了提交,然后他离职了然后电脑重置然后over;不管有意无意,环节得控制好我在第一篇就说了,避免后期维护压力,要控制好每一个环节,其实很简单,代码上传git或者svn,用jenkins来打,Jenkins还会记录每一次的打包时间,然后下载发给生产,我觉得比本地打包优秀多了;


jenkins.jpg


还有就是我们上生产的配置文件尽量读取服务器上的配置,不要和打包一起,你的项目可能部署在很多地方用,单独的配置避免了频繁的找文件,如果需要直接生产copy一份然后修改再上传,


ok!完成


四、总结



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



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

一次偶然提问引发的惊喜体验

大家有没有一种感受,虽然我们在一个满是信息的时代,却往往在寻求精确答案时感到迷茫,因为五花八门的说法太多了。然而,最近在一个网站上的一个随意提问,却让我见证了知识共享的魅力,体验到了前所未有的惊喜——解答的非常快速,也确实解决了我的问题。那天,出于好奇也是有点...
继续阅读 »

大家有没有一种感受,虽然我们在一个满是信息的时代,却往往在寻求精确答案时感到迷茫因为五花八门的说法太多了。然而,最近在一个网站上的一个随意提问,却让我见证了知识共享的魅力,体验到了前所未有的惊喜——解答的非常快速,也确实解决了我的问题。

那天,出于好奇也是有点着急需要解答一个写代码中的问题,我在一个网站上键入了心中的疑惑原因是我在知乎上看到说这个是一个专业的IT一站式学习服务平台,本以为就写一写就算了,没想到,短短几分钟内,就有人来解答了而且回答还挺精准,让我一下恍然大悟。而且里面的AI回复也挺智能,挺有意思的,之后,我就认真逛了一下这个网站,他里面是专门一个帮助专栏的,就和我们发朋友圈一样的感觉,但是是单独拎出来的一个板块,我看大家在里面的提问都有人或者官方去回复的,也可能是因为这是一个新站点,也是IT的一个垂直领域,东西没那么杂,里面的人也都是和IT相关的,所以才能比较快得到答案。对了,这个网站叫云端源想,百度直接搜索就可以找到的,编程过程中需要寻求帮助的小伙伴可以去看看哈。

收起阅读 »

有了这玩意,分分钟开发公众号功能!

大家好,我是程序员鱼皮。 不论在企业、毕设还是个人练手项目中,很多同学或多或少都会涉及微信相关生态的开发,例如微信支付、开放平台、公众号等等。 一般情况下,我们需要到官网查阅这些模块对应的 API 接口,自己编写各种对接微信服务器的代码,结果很多时间都花在了看...
继续阅读 »

大家好,我是程序员鱼皮。


不论在企业、毕设还是个人练手项目中,很多同学或多或少都会涉及微信相关生态的开发,例如微信支付、开放平台、公众号等等。


一般情况下,我们需要到官网查阅这些模块对应的 API 接口,自己编写各种对接微信服务器的代码,结果很多时间都花在了看文档和理解流程上。


好在,某位大佬开源了一个 WxJava 库,它可以让我们更高效快速地开发微信相关的功能。


什么是 WxJava?


WxJava 是一个开箱即用的 SDK,封装了微信生态后端开发绝大部分的 API 接口为现成的方法,包括微信支付、开放平台、小程序、企业微信、公众号等。我们开发时直接调用这个 SDK 提供的方法即可,同时作者针对这个 SDK 还提供了很多接入的 Demo,大部分场景跟着 demo 就能很快上手,非常高效!不需要深入阅读微信开发者官方文档,也能学会微信开发。


WxJava 开发 Demo


这个项目在 GitHub 上 已经有 29.1k 的 star ,社区活跃,且在持续维护更新中。



下面我会通过一个实战案例《公众号的菜单管理功能》,带大家入门 WxJava。


公众号的菜单管理开发实战


1、功能介绍


正常情况下,公众号的管理员可以在公众号网页后台来编辑菜单,例如下面这个页面:



上图中,我在菜单栏分别添加了三个按钮:主菜单一、点击事件、主菜单三。


用户点击 主菜单一 后,就会打开我们设置的跳转网页地址。



上图的 url 仅为演示,实际仅能填写跟公众号相关的网址。



用户点击 点击事件 后,就会自动回复一条消息:您点击了菜单。



你可能会好奇了:公众号网页后台都自带了菜单管理能力,我们还开发什么?


举个例子,如果我们希望用户点了菜单后,调用我们的后端完成新用户注册,就必须要自定义菜单了,因为需要对接我们自己的后端服务器。


而一旦你在后台配置了自己的服务器,就无法使用公众号自带的网页后台来管理菜单和自动回复了,如图:



这种情况下,就只能完全自己在后端写代码来实现这些功能。


2、开发实战


接下来我们用 WxJava 提供的 SDK,通过代码来实现上述同样的功能。


首先,我们需要在 maven 中引入 sdk:


<dependency>
  <groupId>com.github.binarywang</groupId>
  <artifactId>wx-java-mp-spring-boot-starter</artifactId>
  <version>4.4.0</version>
</dependency>

然后在配置文件中添加公众号的 appId 和 appSecret 配置:



按照 WxJava 的规则,编写一个配置类,构建 WxMpService 的 Bean 实例,注入到 Spring 容器中。



上图中的 WxMpService 就是 WxJava 提供的操作微信公众号相关服务的工具类。


接下来,就可以直接创建菜单啦!示例代码如下图:




再次备注:对应 url 内容填写仅为演示,实际 url 对应的网址必须是当前公众号的内容



执行上述代码,其实就可以配置菜单了,你甚至感受不到跟微信服务器 “打交道” 的流程。


这里再简单介绍下菜单二的点击事件,如上面演示,点击 点击事件 公众号会自动回复:“您点击了菜单”。


这个动作被定义为一个叫 CLICK_MENU_KEY 的 key,当用户点击这个按钮后,公众号就会向我们部署的后端服务发送这个事件 key,根据 key 的内容可以执行不同的动作,例如上面说的回复一段文字。


我们仅需把这个 key 绑定到路由上,当触发这个事件就调用对应的 handler 即可,典型的事件驱动设计~



EventHandler 的动作就是返回 “您点击了菜单” 这段文字:



3、其他功能演示


再举例个小功能,如果我们要删除菜单怎么办呢?


非常简单,可以先调用获取菜单的方法:


WxMenu wxMenu = wxMpService.getMenuService().menuGet();

然后根据菜单 ID 就可以调用删除方法来删除菜单:


wxMpService.getMenuService().menuDelete(menuId);

如果要修改菜单,可以再次调用 menuCreate 直接覆盖即可。


最后


利用 WxJava 我们已经实现了菜单的管理,可以看到接口定义非常清晰,使用起来也很方便。当然,以上只是个 Demo,实际企业中如果要操作公众号菜单,不可能每次都是手动执行代码,而是会有一个对应的公众号管理前端,或者再省点事,直接用接口文档来调用操作菜单的接口。感兴趣的同学可以自己实现~


总之希望大家通过这篇教程能够明白,微信相关的开发,并没有那么难,多去做一些调研、多主动搜索一些方案,你会发现很多路前人已经帮你打通了!


可访问我的 Github:github.com/liyupi ,了解更多技术和项目内容。


作者:程序员鱼皮
来源:juejin.cn/post/7368319486779375642
收起阅读 »

为什么list.sort()比Stream().sorted()更快?

昨天写了一篇文章《小细节,大问题。分享一次代码优化的过程》,里面提到了list.sort()和list.strem().sorted()排序的差异。 说到list sort()排序比stream().sorted()排序性能更好。 但没说到为什么。 有朋友也...
继续阅读 »

昨天写了一篇文章《小细节,大问题。分享一次代码优化的过程》,里面提到了list.sort()和list.strem().sorted()排序的差异。

说到list sort()排序比stream().sorted()排序性能更好。

但没说到为什么。


企业微信截图_16909362105085.png


有朋友也提到了这一点。


本文重新开始,先问是不是,再问为什么。




真的更好吗?




先简单写个demo


List userList = new ArrayList<>();
Random rand = new Random();
for (int i = 0; i < 10000 ; i++) {
userList.add(rand.nextInt(1000));
}
List userList2 = new ArrayList<>();
userList2.addAll(userList);

Long startTime1 = System.currentTimeMillis();
userList2.stream().sorted(Comparator.comparing(Integer::intValue)).collect(Collectors.toList());
System.out.println("stream.sort耗时:"+(System.currentTimeMillis() - startTime1)+"ms");

Long startTime = System.currentTimeMillis();
userList.sort(Comparator.comparing(Integer::intValue));
System.out.println("List.sort()耗时:"+(System.currentTimeMillis()-startTime)+"ms");

输出


stream.sort耗时:62ms
List.sort()耗时:7ms

由此可见list原生排序性能更好。

能证明吗?

证据错了。




再把demo变换一下,先输出stream.sort


List userList = new ArrayList<>();
Random rand = new Random();
for (int i = 0; i < 10000 ; i++) {
userList.add(rand.nextInt(1000));
}
List userList2 = new ArrayList<>();
userList2.addAll(userList);

Long startTime = System.currentTimeMillis();
userList.sort(Comparator.comparing(Integer::intValue));
System.out.println("List.sort()耗时:"+(System.currentTimeMillis()-startTime)+"ms");

Long startTime1 = System.currentTimeMillis();
userList2.stream().sorted(Comparator.comparing(Integer::intValue)).collect(Collectors.toList());
System.out.println("stream.sort耗时:"+(System.currentTimeMillis() - startTime1)+"ms");

此时输出变成了


List.sort()耗时:68ms
stream.sort耗时:13ms

这能证明上面的结论错误了吗?

都不能。

两种方式都不能证明什么。


使用这种方式在很多场景下是不够的,某些场景下,JVM会对代码进行JIT编译和内联优化。


Long startTime = System.currentTimeMillis();
...
System.currentTimeMillis() - startTime

此时,代码优化前后执行的结果就会非常大。


基准测试是指通过设计科学的测试方法、测试工具和测试系统,实现对一类测试对象的某项性能指标进行定量的和可对比的测试。

基准测试使得被测试代码获得足够预热,让被测试代码得到充分的JIT编译和优化。




下面是通过JMH做一下基准测试,分别测试集合大小在100,10000,100000时两种排序方式的性能差异。


import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 2, time = 1)
@Measurement(iterations = 5, time = 5)
@Fork(1)
@State(Scope.Thread)
public class SortBenchmark {

@Param(value = {"100", "10000", "100000"})
private int operationSize;


private static List arrayList;

public static void main(String[] args) throws RunnerException {
// 启动基准测试
Options opt = new OptionsBuilder()
.include(SortBenchmark.class.getSimpleName())
.result("SortBenchmark.json")
.mode(Mode.All)
.resultFormat(ResultFormatType.JSON)
.build();
new Runner(opt).run();
}

@Setup
public void init() {
arrayList = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < operationSize; i++) {
arrayList.add(random.nextInt(10000));
}
}


@Benchmark
public void sort(Blackhole blackhole) {
arrayList.sort(Comparator.comparing(e -> e));
blackhole.consume(arrayList);
}

@Benchmark
public void streamSorted(Blackhole blackhole) {
arrayList = arrayList.stream().sorted(Comparator.comparing(e -> e)).collect(Collectors.toList());
blackhole.consume(arrayList);
}

}


性能测试结果:



可以看到,list sort()效率确实比stream().sorted()要好。




为什么更好?




流本身的损耗




java的stream让我们可以在应用层就可以高效地实现类似数据库SQL的聚合操作了,它可以让代码更加简洁优雅。


但是,假设我们要对一个list排序,得先把list转成stream流,排序完成后需要将数据收集起来重新形成list,这部份额外的开销有多大呢?


我们可以通过以下代码来进行基准测试


import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 2, time = 1)
@Measurement(iterations = 5, time = 5)
@Fork(1)
@State(Scope.Thread)
public class SortBenchmark3 {

@Param(value = {"100", "10000"})
private int operationSize; // 操作次数


private static List arrayList;

public static void main(String[] args) throws RunnerException {
// 启动基准测试
Options opt = new OptionsBuilder()
.include(SortBenchmark3.class.getSimpleName()) // 要导入的测试类
.result("SortBenchmark3.json")
.mode(Mode.All)
.resultFormat(ResultFormatType.JSON)
.build();
new Runner(opt).run(); // 执行测试
}

@Setup
public void init() {
// 启动执行事件
arrayList = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < operationSize; i++) {
arrayList.add(random.nextInt(10000));
}
}

@Benchmark
public void stream(Blackhole blackhole) {
arrayList.stream().collect(Collectors.toList());
blackhole.consume(arrayList);
}

@Benchmark
public void sort(Blackhole blackhole) {
arrayList.stream().sorted(Comparator.comparing(Integer::intValue)).collect(Collectors.toList());
blackhole.consume(arrayList);
}

}

方法stream测试将一个集合转为流再收集回来的耗时。


方法sort测试将一个集合转为流再排序再收集回来的全过程耗时。




测试结果如下:



可以发现,集合转为流再收集回来的过程,肯定会耗时,但是它占全过程的比率并不算高。


因此,这部只能说是小部份的原因。




排序过程




我们可以通过以下源码很直观的看到。




  • 1 begin方法初始化一个数组。

  • 2 accept 接收上游数据。

  • 3 end 方法开始进行排序。

    这里第3步直接调用了原生的排序方法,完成排序后,第4步,遍历向下游发送数据。


所以通过源码,我们也能很明显地看到,stream()排序所需时间肯定是 > 原生排序时间。


只不过,这里要量化地搞明白,到底多出了多少,这里得去编译jdk源码,在第3步前后将时间打印出来。


这一步我就不做了。

感兴趣的朋友可以去测一下。


不过我觉得这两点也能很好地回答,为什么list.sort()比Stream().sorted()更快。


补充说明:



  1. 本文说的stream()流指的是串行流,而不是并行流。

  2. 绝大多数场景下,几百几千几万的数据,开心就好,怎么方便怎么用,没有必要去计较这点性能差异。


作者:是奉壹呀
来源:juejin.cn/post/7262274383287500860
收起阅读 »

utf8和utf8mb4有什么区别?

utf8或者utf-8是大家常见的一个词汇,它是一种信息的编码格式,特别是不同开发平台的系统进行对接的时候,编码一定要对齐,否则就容易出现乱码。 什么是编码? 先说说什么是编码。编码就像我们日常生活中的语言,不同的地方说不同的话,编码就是计算机用来表示这些“话...
继续阅读 »

utf8或者utf-8是大家常见的一个词汇,它是一种信息的编码格式,特别是不同开发平台的系统进行对接的时候,编码一定要对齐,否则就容易出现乱码。


什么是编码?


先说说什么是编码。编码就像我们日常生活中的语言,不同的地方说不同的话,编码就是计算机用来表示这些“话”的一种方式。比如我们使用汉字来说话,计算机用二进制数来表示这些汉字的方式,就是编码。


utf8就是这样一种编码格式,正式点要使用:UTF-8,utf8是一个简写形式。


为什么需要utf8?


在计算机早期,主要使用ASCII编码,只能表示128个字符,汉字完全表示不了。后来,才出现了各种各样的编码方式,比如GB2312、GBK、BIG5,但这些编码只能在特定的环境下使用,不能全球通用。


UTF-8就像一个万能翻译官,它的全称是“Unicode Transformation Format - 8 bit”,注意这里不是说UTF-8只能使用8bit来表示一个字符,实际上UTF-8能表示世界上几乎所有的字符。


它的特点是:



  • 变长编码:一个字符可以用1到4个字节表示,英文字符用1个字节(8bit),汉字用3个字节(24bit)。

  • 向后兼容ASCII:ASCII的字符在UTF-8中还是一个字节,这样就兼容了老系统。

  • 节省空间:对于英文字符,UTF-8比其他多字节编码更省空间。


UTF-8适用于网页、文件系统、数据库等需要全球化支持的场景。


经常接触代码的同学应该还经常能看到 Unicode 这个词,它和编码也有很大的关系,其实Unicode是一个字符集标准,utf8只是它的一种实现方式。Unicode 作为一种字符集标准,为全球各种语言和符号定义了唯一的数字码位(code points)。其它的Unicode实现方式还有UTF-16和UTF-32:



  • UTF-16 使用固定的16位(2字节)或者变长的32位(4字节,不在常用字符之列)来编码 Unicode 字符。

  • UTF-32 每一个字符都直接使用固定长度的32位(4字节)编码,不论字符的实际数值大小。这会消耗更多的存储空间,但是所有字符都可以直接索引访问。



图片来源:src: javarevisited.blogspot.com/2015/02/dif…


utf8mb4又是什么?


utf8mb4并不常见,它是UTF-8的一个扩展版本,专门用于MySQL数据库。MySQL在 5.5.3 之后增加了一个utf8mb4的编码,mb4就是最多4个字节的意思(most bytes 4),它主要解决了UTF-8不能表示一些特殊字符的问题,比如Emoji表情,这在论坛或者留言板中也经常用到。大家使用小红书时应该见过各种各样的表情符号,小红书后台也可能使用utf8mb4保存它们。


编码规则和特点:



  • 最多4个字节:utf8mb4中的每个字符最多用4个字节表示。

  • 支持更多字符:能表示更多的Unicode字符,包括Emoji和其他特殊符号。


utf8和utf8mb4的比较


存储空间



  • 数据库:utf8mb4每个字符最多用4个字节,比UTF-8多一个字节,存储空间会增加。

  • 文件:类似的,文件用utf8mb4编码也会占用更多的空间。


性能影响



  • 数据库:utf8mb4的查询和索引可能稍微慢一些,因为占用更多的空间和内存。

  • 网络传输:utf8mb4编码的字符会占用更多的带宽,传输速度可能会稍慢。


不过因为实际场景中使用的utf8mb4的字符也不多,其实对存储空间和性能的影响很小,大家基本没有必要因为多占用了一些空间和流量,而不是用utf8mb4。


只是我们在定义字段长度、规划数据存储空间、网络带宽的时候,要充分考虑4字节带来的影响,预留好足够的空间。


实战选择


在实际开发中,选择编码要根据具体需求来定。如果你的网站或者应用需要支持大量的特殊字符和Emoji,使用utf8mb4是个不错的选择。如果主要是英文和普通中文文本,utf8足够应付。


注意为了避免乱码问题,前端、后端、数据库都应该使用同一种编码,比如utf8,具体到编码时就是要确保数据库连接、网页头部、文件读写都设置为相同的编码。


另外还需要注意Windows和Linux系统中使用UTF-8编码的文件可能是有差别的,Windows中的UTF-8文件可能会携带一个BOM头,方便系统进行识别,但是Linux中不需要这个头,所以如果要跨系统使用这个文件,特别是程序脚本,可能需要在Linux中去掉这个头。




以上就是本文的主要内容,如有问题欢迎留言讨论。


关注萤火架构,加速技术提升!


作者:萤火架构
来源:juejin.cn/post/7375504338758025254
收起阅读 »