普通视图

发现新文章,点击刷新页面。
昨天以前LarsCheng

valine访问leancloud国际版异常,评论失效修复

作者 LarsCheng
2022年1月11日 16:34

起因

太久没维护博客了,最近发现Valine评论都展示不出来,看了下console发现是leancloud访问出了问题

查了下前因后果,大概就是LeanCloud对部分域名不再进行维护了,如果继续使用老的域名去拉取评论数据必然失败。

这里和大家同步下我的环境

调整方案如下

获取新域名

  • 登录leancloud后台
  • 查询自己的APPID

替换https://你的appid前8位.api.lncldglobal.com获得新域名

修改valine代码

  • 主题配置文件中的valine配置增加配置: severURLs(私有leancloud域名)
  • 修改主题中valine对应的js源码:加载私有域名
  • 更新av-min.js文件:确保私有域名可生效

示例

不同的主题可能涉及到的代码位置不同,但是调整思路类似,这里我贴下我的主题配置和涉及到调整的代码片段

主题配置文件config.yml

1
2
3
4
5
6
7
8
9
10
11
12
13

valine:
enable: true # if you want use valine,please set this value is ture
appId: 12345678 # leancloud application app id
appKey: 1234123123123 # leancloud application app key
notify: false # valine mail notify (true/false) https://github.com/xCss/Valine/wiki
verify: false # valine verify code (true/false)
pageSize: 10 # comment list page size
avatar: monsterid # gravatar style https://valine.js.org/#/avatar
lang: zh-cn # i18n: zh-cn/en/tw
placeholder: 📢📢📢留下邮箱可以收到回复提醒哦~
guest_info: nick,mail,link #valine comment header inf
serverURLs: https://12345678.api.lncldglobal.com #替换为你的私有域名

valine对应的js源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//更新av-min.js
<script src="//code.bdstatic.com/npm/leancloud-storage@4.12.0/dist/av-min.js"></script>
<script src="//unpkg.com/valine/dist/Valine.min.js"></script>
<script>
var GUEST_INFO = ['nick','mail','link'];
var guest_info = '<%= theme.valine.guest_info %>'.split(',').filter(function(item){
return GUEST_INFO.indexOf(item) > -1
});
var notify = '<%= theme.valine.notify %>' == true;
var verify = '<%= theme.valine.verify %>' == true;
var valine = new Valine();
valine.init({
el: '#vcomment',
notify: notify,
verify: verify,
appId: "<%= theme.valine.appId %>",
appKey: "<%= theme.valine.appKey %>",
placeholder: "<%= theme.valine.placeholder %>",
pageSize:'<%= theme.valine.pageSize %>',
avatar:'<%= theme.valine.avatar %>',
lang:'<%= theme.valine.lang %>',
//增加serverURLs
serverURLs:'<%= theme.valine.serverURLs %>'
})
</script>

测试

本地构建启动之后可能会因为不在leancloud白名单内,返回403,不过不要紧说明已经生效

直接hexo d发布就能生效了

聊一下换工作

作者 LarsCheng
2021年3月13日 20:52

近半年博客都没怎么更新和维护,一方面确实是忙,另一方面就是一直在为找工作奔波。

终于工作也尘埃落定,马上也要入职,最近在处理工作交接的事情,就写一篇文章来记录下人生中第一次换工作的经历吧。

首先这次工作是从杭州换到了上海,新工作解决了一些个人问题,薪资也达到了预期,新的开始祝自己一切顺利!

我是从大三校招就进了老东家开始程序生涯、毕业就直接拿到了提前转正,说实话,老东家确实挺好的,无论是工作氛围、领导、同事都是无可挑剔的,我在这里生活了三年,和大家都很熟,这里就好像是我的舒适区,拿着够花的工资,过着朝九晚五的生活,周末和同事朋友约饭、游山玩水。在杭州这样的城市真的可以说是美滋滋,当然了前提是你没有外部压力(诸如房子、车子、等等)。

老东家是杭州的一家物联网公司,如果有需要内推的可直接发我邮箱

跳槽、换工作在互联网公司实在是太普遍了,三年间送走了一批又一批,我所在的小组从我入职到现在,除了我之前的人已经全换了一批。以前都是我受邀参加同事的散伙饭,终于今天也到老同事们被我邀请,轮到他们送我,一伙人坐到桌前,仿佛有种不真实的感觉,一起聊着这几年的事,就仿佛都还是昨天…

我差不多是从去年12月份开始陆陆续续投起了简历,然后截止到年前2月初陆陆续续面了大小共6家公司
其中有运气也有自己的因素,拿到了5份offer,最终在年后开工后确定了入职公司。
这也算是参加工作后的第一次换工作,一路上磕磕绊绊总算有了定论。

扯了这么多,还是和大家分享下找工作需要的注意事项

时间

选择一个适合的时间段来执行你的计划是非常重要,都说金三银四、金九银十是跳槽的最佳时间,还是有一定道理的,每年三月份左右企业都过完新年刚开工,年前制定的招聘计划正是开展的时候,我就是在年底这个尴尬的时间点开始的,春招吧有点早,秋招吧有点晚,但是如果你准备好了,其实什么时候找工作都可以,如果刚好赶上金三银四、金九银十岗位的选择机会会更多一些。毕竟开发面试还是得看技术。

渠道

既然要找工作了,渠道很重要,如何从岗位海洋里找到和你契合度高的岗位,并且如何高效的送达简历,其实都是至关重要。

  • 招聘App:我主要是在BOSS直聘和前程无忧两个APP上,其他的没有使用过也就不做评论
  • 内推:确定了目标公司或者意向岗位,先发动下你的小伙伴们,看能不能内推,如果不行可以发动互联网资源,像牛客、知乎、Ve2x,甚至github也有一些内推渠道
  • 猎头:寻找一名优秀的猎头,提出你的需求,交给他来帮你物色,但前提是你们俩要相互信任、并且信息对等且真实,不然工作谈好了最终因为你提供的相关信息与实际不符(比如学历、当前薪资情况等等)导致翻车。

准备

简历投出去了,就预示着你随时会收到面试邀请,可万万不能等收到面试再准备复习,到那个时候只能是临时抱佛脚,很可能被佛踢一脚!!

  • 夯实基础:基础不牢,地动山摇。面试过程中基础知识的考察还是占一定比重的,很多一面基本都是基础考察,所以基础是你能否二面的关键
  • 梳理项目:根据你的简历,梳理你的项目,主要从项目架构(为什么这样设计)、核心功能逻辑(流程熟悉)、遇到的困难这几块来准备
  • 技能自查:简历中一般都会列举自己掌握的技术能力,从熟悉到了解,既然你写上去了,那就要做到完全的准备,随时迎接面试官的连环炮
  • 时间管理:工作、复习、面试是一个漫长的过程,三者之间还是需要一个比较好的时间安排,本职工作还是需要同样重视,毕竟你还没离职。

面试

基本上现在的互联网面试方式就三种:电话面试、视频面试、现场面试

论效率的话现场面试效率最高,电话、视频面一般都只是一面、二面简单了解下。我因为是异地面试的原因,通常都会和对方商量,一共几面,可否当天全部安排。一站式的面试很考验人的精神状态。

  • 面试礼仪:毕竟是面试,打工人骨子里的修养和礼貌还是要有的,电话、视频的沟通方式,需要的注意事项都要提前准备
  • 了解面试:一定要了解下面试的整体流程,会有几面、大概多久会出结果。一方面做到心中有数,同时也能合理的安排其他时间
  • 自信谦虚:去面试一定要自信,既然他已经通知你面试了,说明你还是很优秀的,但切记不能自信过头转而极度自负,还是要保持谦虚,切记不能夸夸其谈

其实面试就像是平时的技术分享一样,把你掌握的一些骚操作、知识点分享给面试官,在我个人的体会下,一场成功的面试就是两个技术人的经验交流,面试者发挥出了自己的所学也看到了自己的短板,面试官测出了对方的深度也发现了对方的闪光点。

复盘

第一场面试结束后,大概率你的心态已经发生了一丝变化,要么信心满满要么就是可能被虐了一顿,但是不论如何,面试后的复盘是尤为重要,技术面试中被问到的问题,哪些是你非常熟悉的,哪些是你印象模糊含含糊糊的,哪些又是你从来没接触过的。这些都需要进行复盘总结。

通过面试后的复盘,来查漏补缺,花时间补一补自己的薄弱点,用每一场面试来磨砺自己,直到你可以在面试中游刃有余,那说明你已经来感觉了。这也代表着你面试大概率要通过了。

抉择

无论你的预期是什么,当你在有可以选择的情况下一定要多方面多角度考虑和抉择

  • 薪资待遇:出来打工为的就是赚钱,所以薪资待遇也是最关注的问题,是否达到预期,是否可以接受,五险一金缴纳细则、
  • 技术氛围:对方的技术氛围如何,是不是让你去开荒(比如全公司就你一个写Java的),技术栈是否与当前的你匹配,如果就职对你的技术实力是否有所提升
  • 个人发展:就职后对个人的发展如何,是否是高危暴雷行业,晋升规则方式如何

最后关于薪资多说两嘴:

时薪时薪时薪!!!!重要的事情说三遍!

有的朋友觉得加班无所谓只要钱管够、有的朋友觉得绝对不加班,加班的我就不去

但是无论加班还是不加班,我都建议你先计算一下时薪,福报型企业加班多自然到手的也多一些,正常型企业不加班但是薪资可能稍微低一点

但是并不是薪资低就不考虑,这个时候建议你算一下时薪,如果不加班的工作可以拿到和加班工作相近的时薪,那还真的需要你好好斟酌,毕竟双休、朝九晚五的生活也是很美的。

最后

唠唠叨叨扯了这么多,也是经历这次换工作后,把自己遇到的一些坑点和经验分享给大家。还是那句话,换工作可以,但是不要盲目的换。

你为什么换工作?你的新工作是否解决了你的困惑,达到了你的预期?

最后,还是祝自己也祝大家工作顺利~

IoT系列(2):WIFI设备常见配网方案介绍

作者 LarsCheng
2021年1月15日 09:39

前言

本文讨论目前市面上基于WIFI智能设备的配网方案,结合自身开发案例,对不同的配网方案进行对比介绍。

阅读本文你可以了解到如下几种配网方案:

  1. 一键配网
  2. 设备热点配网
  3. 零配
  4. 手机热点配网

设备配网说明

提到设备配网这一流程,通俗的理解就是让设备连上网,本文主要就WIFI智能设备的配网展开讨论,目前市面上常见的配网方案都绕不开以下几个步骤:

  • WIFI设备拿到某一wifi的SSID和Password
  • APP拿到WIFI设备的唯一编号
  • APP用户发起设备绑定请求
  • WIFI设备发起入网请求

下面我们针对不同的配网方案来注意分析器配网流程

一键配网

如果你近几年购买过一些智能灯具、智能插座等等WIFI设备,那么大概率他的配网方式就是一键配网

因为一键配网方案,用户操作简单,只需要录入wifi的ssid和password,即可等待设备完成配网。

正如此一键配网几乎是智能设备的通用标准,但是它最大的痛点就是成功率低,特别低!!!

下面一起来看下一键配网的实现原理:

一键配网

  1. 手机提前连接至路由器wifi
  2. APP中输入ssid和密码点击配网,开始进行广播
  3. WIFI智能设备抓取广播包,拿到wifi信息,连接至路由器
  4. WIFI设备连接至路由器后,将自身唯一编号MAC进行局域网广播
  5. 手机APP收到设备广播的MAC编号,向服务器发起设备绑定

从步骤上来看,没有任何毛病,但是在实际的用户配网过程中会出现各种各样的问题,导致用户体验极差,配网成功率极低

  • 路由器兼容性:部分型号的路由器不支持或者禁止发送广播包,直接导致配网永远无法成功,并且用户无法排查
  • 手机兼容性:WIFI设备连接的频段和手机连接的频段不同,导致双方无法收发广播包,例如5G和2.4G频段
  • wifi同名:如果设备附近有多个同名的ssid信号,极有可能设备会无法连接到正确的路由器
  • 等等一些稀奇古怪的问题

看似用户操作方便,并且使用率极高的配网方式,实际操作中有很苛刻的配网条件,这也是一键配网让人又爱又恨的地方

如果有新的WIFI智能设备项目,不建议选用一键配网方案!

设备热点配网

既然一键配网成功率这么低,那有没有成功率高的方案呢,当然是有的:设备热点配网

由于它出众的配网成功率,很快成为wifi设备配网的新宠,像米家的摄像头就采用的这种配网方式

一起来看看他的实现原理:

设备热点配网

  1. WIFI设备进入AP模式,对外提供一个wifi热点
  2. 用户手机连接此wifi,然后通过APP将路由器的SSID和密码发送给WIFI设备
  3. WIFI设备收到SSID信息后将唯一编号MAC发送给APP
  4. 手机APP收到MAC编号,向服务器发起设备绑定【预绑定】
  5. 设备连接路由器联网,向服务器发起入网【激活绑定】

设备热点配网时首先由设备AP模式,手机STA模式,去连接到设备热点上,进行数据传输

整个过程不需要通过路由器广播数据,所以不存在路由器兼容性,也不存在信号频段问题

唯一的风险点就是用户通过APP输入SSID和密码错误,导致设备无法联网。

针对这一风险点,在绑定流程上设计了预绑定和激活绑定:
app携带用户id和设备mac发起预绑定,如果设备正常联网上线,那么绑定生效,设备激活;如果设备拿到了错误的ssid信息一定时间内没有上线,那么清除预绑定记录。

设备热点配网相对于一键配网几乎没有任何额外的成本增加,在尽量不增加用户操作复杂度的前提下,极大的提高了配网成功率,这也是当下新的WIFI设备配网首选方案。

零配

零配,我最早在天猫精灵系列设备的配网方案中遇到过,这是一种特定场景的配网方案,大致思路是通过已经配网成功的设备(智能音箱)给新的设备进行配网,实现真正意义上的零配置配网

现在大部分的智能音箱联动场景中都支持零配方案。

先看一下的的实现步骤:

零配

前提:通过其他方式已经完成配网的智能设备(天猫精灵),与服务器连接正常,并存有路由器SSID信息

  1. 手动触发WIFI设备将自己MAC信息通过Sniffer报文发送到天猫精灵
  2. 天猫精灵收到设备MAC信息后,将本地保存的路由器SSID信息发送给WIFI设备
  3. 天猫精灵向服务器发起该设备的预绑定请求
  4. WIFI设备连接路由器联网,并向服务器发起激活绑定请求

该方案需要有一台已经联网的智能设备,并且该设备保存了用户信息和路由器SSID信息,优化掉了用户手动输入SSID和密码的步骤,进一步简化了用户配网操作。

在实际使用中,用户开启WIFI设备后,只需要对天猫精灵说一句“找队友”即可完成配网,可以说用户的配网体验感很好。

手机热点配网

这种方案和设备热点配网方案比较相似,从名字能看出来,这种方案的热点是由手机提供。同样都是为了解决路由器兼容性而提出的解决方案。

这种方案在阿里IoT中被作为一键配网失败后的补救措施。当一键配网失败后,用户可以通过手机设置特定的wifi热点,设备连接到手机热点上后进行信息交互。

原理图如下:

手机热点配网

流程基本上和设备热点方案类似,区别就是提供热点的是手机端

不过在实际应用中,使用率不是很高,一方面用户操作复杂度过高,可能用户完全不知道如何开启手机热点。另一方面能想到手机热点配网方案,肯定会采用设备热点配网方案了。

所以总的来说,该方案成功率相对较高,但是用户操作复杂度也随之增大,可以作为其他方案失败后的备选方案,但并不推荐使用,毕竟用户体验是第一位

总结

总结一下上面提到的四种方案的特点:

方案使用率成功率用户体验路由器兼容性频段兼容性手机兼容性使用场景
一键配网差(不支持广播)差(2.4G/5G)不推荐使用
设备热点配网WIFI配网首选方案
零配优(免输入SSID信息)音箱联动场景推荐
手机热点配网差(手动开启热点)不推荐使用

以上四种配网方案也是我目前工作中接触到的一些常用方案,为了方便理解,简化了各种方案的细节,实际通讯和交互流程会更为复杂。

当然除了这些,也有一些其他方案比如路由器热点配网方案WEB配网方案等等,这些方案都因为需要特定场景和复杂流程等因素逐渐不被经常使用。

IoT系列(1):什么是物联网

作者 LarsCheng
2021年1月14日 10:39

前言

本文主要讨论物联网的相关概念知识,阅读本文你会有如下几点了解:

  1. 物联网概念引入
  2. 物联网与互联网的区别与联系
  3. 什么是物联网
  4. 物联网在我们生活中有哪些应用

物联网引子

  • 如果一把伞可以感知当地天气并提醒主人今天是否应该带伞
  • 如果某种可穿戴设备能够监测病人的健康状况并预测病情是否恶化以便及时准确地通知医生
  • 如果汽车上的计算和预测分析系统能够提醒用户保养计划以避免突如其来的部件故障,我们的生活将会如何?

如今的物联网解决方案已经能够轻松实现上面的设想。我们的生活也在逐渐走向万物互联。

先说说当你听到物联网(Internet of Things),你想到了什么?有没有下面这些:

互联网、IoT、传感器、智能家居、智能空调、智能手机、智能酒店、车联网…

物联网从字面看蕴含着物物相联的意思,从我们身边的物联网产品来看,它具备着将设备与设备相互连接,人与设备连接的能力。

物联网与互联网

物联网和互联网在很多人的理解中可能觉得都差不多、都可以上网之类的。但实际上他们两者可以说是完全不同的两个场景。

在互联网时代,最初是PC电脑实现人与人之间的沟通变得越来越简单,而后手机作为一个媒介打开了移动互联网的热潮。无论手机还是电脑,都是为了实现人与人之间高效连接

其中,人是消费者也是生产者,手机或者电脑是作为传输媒介进行信息传输

上面是在互联网中的模型,而在物联网中则是另外一中场景,举个栗子:

你购买了一个智能灯,智能灯可以通过手机app进行wifi配网后连接到网络,用户可以通过app控制设备,设备的状态会实时的通知到app,用户可以通过app检查设备是否正常。

以上是一个典型的物联网设备使用场景,在这个场景中涉及到了3个设备:智能灯、手机、路由器。他们搭配在一起,实现了一个设备与设备连接,设备与人连接的场景

与互联网中不同,设备的参与度更高,设备不仅仅进行消息的传输,他也是消息的生产者和消费者。

通过以上的对比,有一个最显著的感受就是物联网中,设备的参与度更高,更倾向于设备与设备的连接互通。互联网中更着重人与人之间的互联

当然物联网与互联网也不是完全分割的,可以理解为随着互联网的发展物联网应运而生,物联网是互联网的增强和延伸。

什么是物联网?

上面引入了物联网,也将其与互联网做了对比,那么到底什么是物联网呢?

物联网(IoT,Internet of Things)在互联网的基础上,将用户端延伸和扩展到物与物、物与人的连接。物联网模式中,所有物品都可以与网络连接,并进行通信和场景联动。

物联网是互联网的延伸。互联网通过电脑、移动终端等设备将参与者联系起来,形成的一种全新的信息互换方式。而物联网则是通过传感器、芯片、无线模组使设备联网,进而进行信息互换,实现物物人相联。

物联网三层架构

物联网从整个体系结构来看,可以分为三个层面:

  • 设备层(Device):负责数据采集的各种智能硬件设备,比如传感器设备,控制器等。
  • 网络层(Connect):负责可靠传递,通过将物体接入网络,依托通信技术和通信协议,实现可信的信息交互和共享。通信技术例如NB、LoRa、WIFI,通信协议例如HTTP、TCP、UDP、MQTT、AMQP等
  • 应用层(Manage):负责智能处理,分析和处理海量的感知数据和信息,实现智能化的决策和控制。就是实现具体业务逻辑的地方。

此处引用一张《物联网开发实战》中的图例:

引自《物联网开发实战》

物联网在我们生活中的应用

目前我们普通人对于物联网接触最多的应该就是智能家居了,像家里的空调、冰箱、窗帘、灯具等等

但这仅仅是物联网在智能家居板块的体现,如果按行业划分,主要体现在如下几块

  • 智慧物流:例如菜鸟物流实验室智能搬运、分拣机器人,顺丰的数据灯塔让物流过程可视化。
  • 智能交通:比如电动车厂商推进车联网、美团的共享自行车、共享电动车,gofun的共享汽车等
  • 精准农业:通过物联网相关技术进行农作物长势、自然条件的检测,比如电信推出的山洪预警系统,还有像最近比较火的智慧养猪等等项目
  • 智慧医疗:比如像今年的健康码,比如通过可穿戴设备检测人体器官信息
  • 智慧家居:像小米生态链、智能酒店、智慧安防等等。

总结

如今在通信、互联网、嵌入式等技术的推动下,物联网正在逐渐走进我们的生活、互联网时代下,人与人的距离变小了,而继互联网之后物联网时代则是缩短物与物、物与人之间的距离。

设计模式(2)-工厂模式图文介绍

作者 LarsCheng
2020年8月30日 12:53

工厂模式

工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

模拟需求①

假设现有一个口罩生产工厂,可以生产防霾口罩、医用一次性口罩、N95口罩
客户可以通过口罩直营店根据自己的需求下单购买口罩
使用代码实现这一流程

传统实现方式

根据给出的需求,结合面向对象思想,大概有以下几个类

  • BaseMask 抽象口罩类
  • HazeMask 防霾口罩类
  • MedicalMask 医用口罩类
  • N95Mask N95口罩类
  • MaskStore 直营店类
  • Client 客户类

简单类图如下:

实现代码

HazeMask、MedicalMask、N95Mask继承自BaseMask,分别实现prepare方法,并调用setName方法设置name属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14

public abstract class BaseMask {
protected String name;
public abstract void prepare();
public void processing(){
System.out.println(name+"开始加工...");
}
public void bale(){
System.out.println(name+"打包完成...");
}
public void setName(String name) {
this.name = name;
}
}

MaskStore类,实现了口罩直营店根据用户需求进行下单的流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class MaskStore {
public void order() {
BaseMask mask = null;
int maskType;
do {
maskType = getType();
if (1 == maskType){
mask = new HazeMask();
}else if (2 == maskType){
mask = new MedicalMask();
}else if (3 == maskType){
mask = new N95Mask();
}else {
System.out.println("不支持的产品类型");
break;
}
mask.prepare();
mask.processing();
mask.bale();
} while (true);
}

/**接收用户要下单的产品类型
* 1:防霾口罩
* 2:医用口罩
* 3:n95口罩
* */
private int getType() {
try {
BufferedReader typeReader = new BufferedReader(new InputStreamReader(System.in));
System.out.println("输入需要下单的类型: ");
return Integer.parseInt(typeReader.readLine());
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
}

Client的实现就相对简单,模拟用户下单操作,直接调用直营店暴露的下单order方法

优缺点分析

根据场景需求我们有了如上的代码方案,其中涉及到的类和方法都比较好理解,核心主要是通过用户需要下单的type来进行产品的创建,但优缺点需要细细捋一捋

优点:思路清晰,便于理解
缺点:违反开闭原则,也就是扩展性差,如果添加一个新的口罩类型,涉及到的修改点过多

举个栗子:
如果这时候添加一个新的口罩类型,那所有的口罩直营店类中的代码都需要同步修改

这时候有一种解决方案:将根据类型创建产品的方法单独封装起来,当有新产品加入时,只需要修改单独封装过的这部分代码,而调用方可以做到无感知接入,这种方式也叫做简单工厂模式。但他并不属于23种设计模式,简单工厂仅仅指一种创建类的解决方案

简单工厂模式

相对于传统方案中多出一个简单工厂类SimpleMaskFactory,同时对MaskStore进行了重构,简单类图如下:

代码实现

与传统方案不同的是,之前的口罩产品创建是在MaskStore中,使用简单工厂模式后,将创建口罩产品的工作封装到了SimpleMaskFactory中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

public class SimpleMaskFactory {

public BaseMask createMask(int maskType) {
BaseMask mask = null;
if (1 == maskType) {
mask = new HazeMask();
} else if (2 == maskType) {
mask = new MedicalMask();
} else if (3 == maskType) {
mask = new N95Mask();
}
return mask;
}
}

MaskStore只需要持有工厂类和需要下单的产品类型,发起下单操作即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class MaskStore {
private SimpleMaskFactory factory;

public MaskStore(SimpleMaskFactory factory) {
this.factory = factory;
}

public void order() {
BaseMask mask = null;
int maskType;
do {
maskType = getType();
mask = factory.createMask(maskType);
if (!Objects.isNull(mask)){
mask.prepare();
mask.processing();
mask.bale();
}else {
System.out.println("不支持的产品类型...");
break;
}
} while (true);
}

private int getType() {
try {
BufferedReader typeReader = new BufferedReader(new InputStreamReader(System.in));
System.out.println("----------------");
System.out.println("输入需要下单的类型: ");
return Integer.parseInt(typeReader.readLine());
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
}

Client客户端的调用也更加方便

1
2
3
4
5
public class Client {
public static void main(String[] args) {
new MaskStore(new SimpleMaskFactory()).order();
}
}

模拟需求②

假设现有多个口罩生产工厂,大致分为杭州制造和上海制造,可以生产防霾口罩、医用一次性口罩,
客户可以通过自己的需求下单购买某个地址制造的某一种口罩
使用代码实现这一流程

此时的需求不仅有地域区分,同时还有种类区分,这种场景该如何处理呢?

  • 方案1
    • 使用简单工厂模式,根据地域创建不同的工厂类,通过不同的工厂类来进行不同的产品创建
    • 扩展性差,可维护性差
  • 方案2
    • 使用工厂方法模式,将创建产品的方法抽象化,创建对象的操作交给子类自己来完成,即将对象实例化推迟到子类

工厂方法模式

与简单工厂模式所不同,工厂方法模式将定义一个创建对象的抽象方法,根据实际需求整理到所涉及的类有

  • BaseMask 抽象的口罩类
  • HangzhouHazeMask 杭州制造-防霾口罩
  • HangzhouMedicalMask 杭州制造-医用口罩
  • ShanghaiHazeMask 上海制造-防霾口罩
  • ShanghaiMedicalMask 上海制造-医用口罩
  • BaseMaskFactory 抽象口罩工厂类,定义了一个创建对象的抽象方法,将对象创建延缓到子类进行
  • HangzhouMaskFactory 杭州制造工厂类
  • ShanghaiMaskFactory 上海制造工厂类
  • Client 客户类

简单的类图如下:

代码实现

HangzhouHazeMask、HangzhouMedicalMask、ShanghaiHazeMask、ShanghaiMedicalMask继承自BaseMask,分别实现prepare方法,并调用setName方法设置name属性

HangzhouMaskFactory、ShanghaiMaskFactory继承自BaseMaskFactory类,重写了抽象方法createMask方法实现自己的对象创建逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

public abstract class BaseMaskFactory {
//抽象方法,子类自己实现对象的创建
abstract BaseMask createMask(int maskType);

public BaseMaskFactory() {
BaseMask mask = null;
int maskType;
do {
//1:防霾口罩 2:医用口罩
maskType = getType();
mask = createMask(maskType);
if (!Objects.isNull(mask)) {
mask.prepare();
mask.processing();
mask.bale();
}else {
System.out.println("不支持的产品类型...");
break;
}
} while (true);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13

public class HangzhouMaskFactory extends BaseMaskFactory{
@Override
BaseMask createMask(int maskType) {
BaseMask mask = null;
if (1==maskType){
mask = new HangzhouHazeMask();
}else if (2==maskType){
mask = new HangzhouMedicalMask();
}
return mask;
}
}

此时的客户调用,可以有选择性的指定某一地区来进行下单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Client {

public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请选择要购买的产品产地,1:杭州,2:上海");
int location = Integer.parseInt(scanner.nextLine());
if (1 == location) {
new HangzhouMaskFactory();
} else if (2 == location) {
new ShanghaiMaskFactory();
} else {
System.out.println("暂无该地区产品");
}
}
}

模拟需求③

假设现有两种产品要进行生产:口罩和酒精,并且有杭州和上海两个工厂都可以生产这两种产品
客户可以通过自己的需求下单购买某个地址制造的某一种产品
使用代码实现这一流程

这次的需求不同以往,产品类型出现了多种,即一个工厂可以生产多种不同类型的产品,这种涉及到多个产品簇,比较推荐使用抽象工厂模式

抽象工厂模式

抽象工厂模式是一种为访问类提供一个创建一组相关或相互依赖对象的接口,
且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。

抽象工厂模式是工厂方法模式的升级版本,工厂方法模式中一个工厂只生产一种产品,而在抽象工厂模式中,一个工厂生产多种产品,并且存在多个工厂

抽象工厂模式中有这两个概念

  • 产品等级:产品等级可以理解为同一类产品属于一个等级,比如防霾口罩、与医用外科口罩都属于口罩类,属于一个产品等级,但口罩和酒精明显不是一个产品等级
  • 产品族:同一个具体工厂所生产的位于不同产品等级的所有产品称作一个产品族。比如杭州工厂生产的杭州口罩和酒精就属于一个产品族

上面的需求用抽象工厂模式的思路得到的简单类图如下:

代码实现

其中HangzhouMask、ShanghaiMask都继承自BaseMask,HangzhouAlcohol、ShanghaiAlcohol继承自BaseAlcohol

通过定义抽象工厂接口AbstractMaskFactory,定义创建产品的方法,交由子类工厂进行实现。这里的产品创建方法可以覆盖到所有的产品等级

1
2
3
4
5

public interface AbstractFactory {
BaseMask createMask();
BaseAlcohol createAlcohol();
}
1
2
3
4
5
6
7
8
9
10
11
12
public class HangzhouFactory implements AbstractFactory{

@Override
public BaseMask createMask() {
return new HangzhouHazeMask();
}

@Override
public BaseAlcohol createAlcohol() {
return new HangzhouAlcohol();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13

public class ShanghaiFactory implements AbstractFactory{

@Override
public BaseMask createMask() {
return new ShanghaiHazeMask();
}

@Override
public BaseAlcohol createAlcohol() {
return new ShanghaiAlcohol();
}
}

创建了工厂类后,客户可以通过某一工厂进行指定产品的下单操作,这些逻辑封装在了Store类中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Store {
private AbstractFactory factory;

public Store(AbstractFactory factory) {
this.factory = factory;
}

public void orderMask() {
BaseMask mask = null;
mask = factory.createMask();
if (!Objects.isNull(mask)) {
mask.prepare();
mask.processing();
mask.bale();
} else {
System.out.println("不支持的产品类型...");
}
}

public void orderAlcohol() {
BaseAlcohol alcohol = null;
alcohol = factory.createAlcohol();
if (!Objects.isNull(alcohol)) {
alcohol.prepare();
alcohol.processing();
alcohol.bale();
} else {
System.out.println("不支持的产品类型...");
}
}
}

3种工厂模式的总结

本文一共提到了三种工厂模式,简单工厂模式、工厂方法模式、抽象工厂模式,也根据模拟场景对其进行了简单的说明

从上面的介绍中可以简单做下总结

  • 简单工厂模式
    • 实现对象的创建和对象的使用分离,将对象的创建交给专门的工厂类负责
    • 工厂类不够灵活,增加新的具体产品需要修改工厂类的判断逻辑代码
    • 而且产品较多时,工厂方法代码逻辑将会非常复杂
  • 工厂方法模式
    • 定义一个抽象的核心工厂类,并定义创建产品对象的接口,创建具体产品实例的工作延迟到其工厂子类去完成
    • 系统需要新增一个产品是,无需修改现有系统代码,只需要添加一个具体产品类和其对应的工厂子类
    • 系统的扩展性变得很好,符合面向对象编程的开闭原则
  • 抽象工厂模式
    • 工厂模式的升级版,工厂方法模式中一个工厂负责生产一类产品,而抽象工厂模式中一个工厂可以生产多种产品
    • 扩展性更强,无论是增加工厂,还是增加产品,抽象工厂模式都比工厂方法模式更为便捷

关于工厂方法模式和抽象工厂模式的几点区别如下:

  • 工厂方法模式利用继承,抽象工厂模式利用组合
  • 工厂方法模式产生一个对象,抽象工厂模式产生一族对象
  • 工厂方法模式利用子类创造对象,抽象工厂模式利用接口的实现创造对象

常见的工厂模式的运用

  • JDK中Calendar的getlnstance方法
  • JDBC中的Connection对象的获取
  • Spring中IOC容器创建管理bean对象
  • 反射中Class对象的newlnstance方法

设计模式(1)-带你了解3类8种单例模式

作者 LarsCheng
2020年8月29日 13:53

单例模式的分类

  • 饿汉式
    • 静态常量
    • 静态代码块
  • 懒汉式
    • 线程不安全
    • 线程安全,同步方法
    • 线程安全,同步代码块
    • 双重检查锁
    • 静态内部类
  • 枚举

饿汉式

饿汉式,单例模式的一种类型,对于这个名字可以假想成:

有一天小明买了菜回到家,由于他特别饿,于是就把所有菜都用掉做了满满一桌子菜,而直到最后吃饱,仍然有一些菜从来没尝过,而且由于做的菜太多导致的燃气也用完了。

这里的菜就是我们要使用的对象,而小明就是单例类,燃气就是系统内存。在调用方准备使用对象前,就把所有的对象都实例化好,以供随时调用,但如果实例化工作量过大可能导致内存浪费

饿汉式-静态常量(⭐慎用)

这是最简单的单例模式,主要有以下几点核心思路

  • 私有构造方法
  • 私有静态常量,类加载时初始化常量对象
  • 公有对象获取方法

示例代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class SingletonType01 {
public static void main(String[] args) {
Singleton01 instance1 = Singleton01.getInstance();
Singleton01 instance2 = Singleton01.getInstance();

System.out.println("instance1 == instance2 "+(instance1==instance2));
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}

class Singleton01 {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton01() {
}
/**
* 在类加载时创建私有的静态变量
*/
private final static Singleton01 INSTANCE = new Singleton01();

/**
* 对外提供获取对象的静态方法,
* 外部调用,类名.方法名 Singleton.getInstance()
* @return 返回单例对象
*/
public static Singleton01 getInstance() {
return INSTANCE;
}
}

示例代码本机执行结果:

1
2
3
instance1 == instance2 true
491044090
491044090

主方法中对于两次获取到的对象进行了对比,可以看到两者为同一对象,且hashcode相同

优点:

  • 写法简单,在类装载的时候完成实例化,避免线程同步问题

缺点:

  • 在类装载时就实例化,那可能这个对象从始至终都没有被用到,无形中造成资源浪费,没有懒加载效果

这种单例模式,可以使用,并且无需考虑多线程问题,但是存在内存浪费问题

饿汉式-静态代码块(⭐慎用)

饿汉式静态代码块的实现与静态常量基本类似,唯一不同就是对象的实例化从静态变量转移到了静态代码块中,但其都是在类加载是执行的

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

/**
*
* @author larsCheng
*/
public class SingletonType02 {
public static void main(String[] args) {
Singleton02 instance1 = Singleton02.getInstance();
Singleton02 instance2 = Singleton02.getInstance();

System.out.println("instance1 == instance2 : "+(instance1==instance2));
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}

class Singleton02 {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton02() {
}
/**
* 静态私有变量
*/
private static Singleton02 INSTANCE;

/**
* 将对象的实例化放在了静态代码块中,同样也是类加载时被执行
*/
static {
INSTANCE = new Singleton02();
}
/**
* 对外提供获取对象的静态方法,
* 外部调用,类名.方法名 Singleton.getInstance()
* @return 返回单例对象
*/
public static Singleton02 getInstance() {
return INSTANCE;
}
}

示例代码本机执行结果:

1
2
3
instance1 == instance2 : true
491044090
491044090

可以看出同样是单例对象的效果,所有与饿汉式静态常量写法相比较,其优缺点也一样,都会造成内存浪费

懒汉式

前面提到的两种单例模式都是饿汉式,即无论用不用这个对象,他对会被实例化。

这里要提到的是另一种单例模式-懒汉式,即对象只有在需要使用的时候才进行实例化,同样可以想象成一个小场景

有一天小李特别饿,但是他很懒,不想做饭就到餐馆吃饭,看了菜单从里面选择点了一份牛肉拉面,后厨师傅马上给他做好,小李吃饱后就开心的回家了

虽然描述的比较抽象,小李是是对象使用方,菜单上的每一个菜是一个单例类,后厨师傅是JVM。

当你选定一个对象了之后才会为你立即创建,而不是提前把所有的对象都实例化好。这样实现了懒加载的效果

懒汉式-线程不安全(👎👎👎不可使用)

懒汉式的简易版本,这一实现方式虽然做到了懒加载,但是存在线程安全问题

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

public class SingletonType03 {
public static void main(String[] args) {
Singleton03 instance1 = Singleton03.getInstance();
Singleton03 instance2 = Singleton03.getInstance();

System.out.println("instance1 == instance2 : " + (instance1 == instance2));
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}

class Singleton03 {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton03() {
}

/**
* 静态私有变量
*/
private static Singleton03 INSTANCE;


/**
* 对外提供获取对象的静态方法,此处存在线程安全问题
* 外部调用,类名.方法名 Singleton.getInstance()
*
* @return 返回单例对象
*/
public static Singleton03 getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton03();
}
return INSTANCE;
}
}

示例代码本机执行结果:

1
2
3
instance1 == instance2 : true
491044090
491044090

简单的执行测试结果看似乎并无问题,做到了延迟加载(懒加载),并且实现了单例模式

但是!!!这一切都是单线程的前提下,一旦为多线程环境,在getInstance方法中会有严重的线程安全问题

分析:

  • 假设有两个线程A、B
  • A线程先到,判断INSTANCE为空,进入if内,准备进行对象初始化
  • 此时B线程也到达if判断,发现INSTANCE仍为空(A还未完成对象实例化),B也进入if内。

这种情况下,待A、B执行完后,得到的将是两个对象。这就完全违背了单例模式的初衷!!

所以通常情况下,不推荐使用这种懒汉式的单例模式。因为绝大多数的应用场景都为多线程环境。

而在多线程环境下,这种实现方式完全不算单例模式的范畴,因为它会产生多个对象实例

懒汉式 - 同步方法(👎不推荐)

针对于线程不安全问题,对应则有线程安全的解决方案

即在getInstance方法上加入synchronized关键字,将其改造成同步方法,解决在多线程环境下的线程不安全问题

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Singleton04 {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton04() {
}

/**
* 静态私有变量
*/
private static Singleton04 INSTANCE;

/**
* 对外提供获取对象的静态方法,加入同步关键字,解决线程同步问题
* 外部调用,类名.方法名 Singleton.getInstance()
*
* @return 返回单例对象
*/
public static synchronized Singleton04 getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton04();
}
return INSTANCE;
}
}

如上,虽然解决了线程不安全问题,但是随之而来的是效率问题

分析:

  • 每次调用getInstance方法都需要进行线程同步
  • 实际上造成多个对象被实例化的仅仅只是方法中代码片段

所以总的来说,虽然解决的线程安全问题,但是由于效率不加,且有优化方案,故此种方式也不建议使用

针对同步方法带来的效率问题,有改进方案,但有一种错误的改进方案这里有必要提一下

同步方法改造为同步代码块,尝试减少同步的代码,来提高效率,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Singleton04ErrorSolution {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton04ErrorSolution() {
}

/**
* 静态私有变量
*/
private static Singleton04ErrorSolution INSTANCE;

/**
* 对外提供获取对象的静态方法,对造成线程安全问题的代码块进行同步
* 外部调用,类名.方法名 Singleton.getInstance()
*
* @return 返回单例对象
*/
public static Singleton04ErrorSolution getInstance() {
if (INSTANCE == null) {
synchronized (Singleton04ErrorSolution.class) {
INSTANCE = new Singleton04ErrorSolution();
}
}
return INSTANCE;
}
}

如上代码的本意是将同步方法细化到同步代码块,来进行效率优化,但是这样的改动起到了相反的效果

分析:

  • 对实例化对象的代码片段进行同步,假设A、B两线程执行getInstance方法
  • A线程判断INSTANCE为空后进入if内,准备执行同步代码块,此时B线程也判断INSTANCE为空,也进入了if内部,等待A线程执行完毕
  • A线程执行完同步代码块后,实例化了一个对象,此时B线程开始执行,也创建了一个对象

从上面的分析可以看出,这种改进方案,属于想法正确,但是操作错误,导致不但没有解决效率问题,同时造成线程安全问题,是一定要避免的错误!!

懒汉式-同步代码块(👎不推荐)

基于上文提到的优化思路:将同步方法细化到同步代码块,那正确的改进方案可能会有下面这种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

class Singleton05 {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton05() {
}

/**
* 静态私有变量
*/
private static Singleton05 INSTANCE;

/**
* 对外提供获取对象的静态方法,加入同步关键字,解决线程同步问题
* 外部调用,类名.方法名 Singleton.getInstance()
*
* @return 返回单例对象
*/
public static Singleton05 getInstance() {
synchronized (Singleton05.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton05();
}
}
return INSTANCE;
}
}

从getInstance方法可以看到,使用了同步代码块的方式,并且同步的是if判断和实例化部分的代码

虽然达到了线程安全,但是基本上和同步方法的效率没什么区别,依旧每个线程进来后,都需要等待执行同步代码块。

这种方案只是为了和上面的错误同步代码块方式进行对比。真实业务中也不推荐使用这种方式!!!

双重检查锁(👍推荐使用)

想要实现懒加载,同时保证线程安全,同时提高效率。那么一起来看看双重检查锁的实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Singleton06 {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton06() {
}

/**
* 静态私有变量
* 声明volatile,防止指令重排,导致的空对象异常
*/
private static volatile Singleton06 INSTANCE;

/**
* 对外提供获取对象的静态方法,使用双重检查锁机制,保证同步代码块中的实例化代码只会被执行一次
* 外部调用,类名.方法名 Singleton.getInstance()
*
* @return 返回单例对象
*/
public static Singleton06 getInstance() {
if (INSTANCE == null) {
synchronized (Singleton06.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton06();
}
}
}
return INSTANCE;
}
}

首先先来看看该方案于前几种的不同点

  • 使用synchronized关键字实现同步代码块
  • 同步前同步后两次判断
  • 使用了volatile关键字

分析

在getInstance方法中使用了Double-Check概念,配合同步代码块,保证线程安全。简单分析下其流程

  • A、B、C 3个线程执行getInstance方法
  • A、B线程都通过了第一个if判断,A线程抢到了锁,开始执行同步代码块中的逻辑,B等待
  • A通过了第二个if判断,进行了INSTANCE的实例化操作,A完成操作,释放锁
  • B开始执行同步代码块内容,B未通过第二个if(此时的INSTANCE不为空),直接返回INSTANCE对象,B释放锁
  • 此时C开始执行getInstance方法,C未通过第一个if,直接返回INSTANCE对象

从上面分析过程中可以看到,无论有多少个线程,实例化代码只会被执行一次,意味着只会创建一个对象。

volatile

但是在整个流程中有一个小小的隐患

INSTANCE = new Singleton06();它并非是一个原子操作,事实上,在 JVM 中上述语句至少做了以下这 3 件事:

①第一步:给 INSTANCE 分配内存空间;
②第二步:调用 Singleton06 的构造函数等,来初始化 INSTANCE;
③第三步:将 Singleton06 对象指向分配的内存空间(执行完这步 INSTANCE 就不是 null 了)。

这里的理想执行顺序是 1->2->3,实际在Jvm中执行顺序有可能是1->3->2,也有可能是 1->2->3。
这种现象被称作指令重排也就是说第 2 步和第 3 步的顺序是不能保证的,这就导致了隐患的产生。

在线程A执行INSTANCE = new Singleton06();是,JVM中的执行顺序是1->3->2,先进行分配内存再初始化INSTANCE,若在刚完成内存分配时,线程C开始执行第一个if判断,发现INSTANCE不为空,直接返回INSTANCE对象,此时的INSTANCE明显会出现问题。

在Java内存模型中,volatile 关键字作用可以是保证可见性且禁止指令重排。从而避免由于指令重排导致的异常隐患。

关于 volatile关键字和指令重排相关 可以参考此处

总结

双重检测锁的单例实现方案,可以实现延迟加载,同时线程安全并且效率高,在实际场景中是推荐使用的!

静态内部类(👍推荐使用)

除了双重检查锁被推荐使用外,静态内部类实现单例模式也是被推荐使用的一种
示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

class Singleton07 {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton07() {
}

/**
* 提供一个静态内部类,类中声明一个类型为 Singleton07 的静态属性 INSTANCE
*/
private static class SingletonInstance {
private static final Singleton07 INSTANCE = new Singleton07();
}

/**
* 对外提供获取对象的静态方法,
* 外部调用,类名.方法名 Singleton.getInstance()
*
* @return 返回静态内部类的静态属性
*/
public static Singleton07 getInstance() {
return SingletonInstance.INSTANCE;
}
}

分析

  • 该方案采用了类装载机制来保证初始化实例时只有一个线程,从而保证了线程安全
  • 单例类Singleton07被装载时,静态内部类SingletonInstance是不会实例化的,只有调用getInstance方法时才会触发静态内部类SingletonInstance的装载,从而执行实例化代码
  • 并且静态内部类的静态属性只会在第一次加载类的时候被初始化,所以做到了懒加载

结论

保证了线程安全,使用静态内部类的特点实现懒加载,并且有较高效率,推荐使用

枚举(👍推荐使用)

那么这么多的实现方案,Java中有没有一个公认的最佳枚举实现方案呢,当然有啊,通过枚举来实现
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class SingletonType08 {
public static void main(String[] args) {
String connection1 = Singleton08.INSTANCE.getConnection();
String connection2 = Singleton08.INSTANCE.getConnection();

System.out.println("connection1 == connection2 : " + (connection1 == connection2));
System.out.println(connection2.hashCode());
System.out.println(connection2.hashCode());
}
}

enum Singleton08 {
/***/
INSTANCE;

/**资源对象,此处以字符串示例*/
private String connection = null;

/**
* 在私有构造中实例化单例对象
*/
Singleton08() {
//模拟实例化过程
this.connection = "127.0.0.1";
}

/**
* 对外提供获取资源对象的静态方法
*/
public String getConnection() {
return connection;
}
}

如上代码是通过枚举来实现单例对象的创建

enum有且仅有private的构造器,防止外部的额外构造,这恰好和单例模式吻合,也为保证单例性做了一个铺垫。
枚举类型是线程安全的,并且只会装载一次。

枚举类实现单例模式是 effective java 作者极力推荐的单例实现模式,它保证线程安全,并防止外部反序列化的破坏。

Java时间处理3---Java8中Instant、Duration、Period、Clock介绍

作者 LarsCheng
2020年6月10日 17:06

前言

前面文章对Java中的Date和Calendar类进行了介绍,在Java8以前,Javaer处理时间基本都是使用这两个类。

然鹅在使用过程中一个很尴尬的场景就是Date大部分方法废弃,Calendar又有很多不太友好的设计(月份从0开始)

终于,Java8中提供了一套全新的时间处理库,源码中的目录为java.time,该包中的类都是不可变且线程安全

看上图感觉新的time包下好像有很多都是新的类,感觉看着很头大啊,不过不用担心新提供的处理类中方法设计具有规律性,并且模块清晰,上手较快。

下面对比较常用的类库进行介绍。

本文主要对Instant、Duration、Period、Clock这四个类进行介绍

  • Instant:时间线上的某一时间点
  • Duration:两个时间之间的持续时间,存储秒和纳秒
  • Period:两个日期之间的持续时间,存储年,月和日
  • Clock:表示真实世界的时钟,可通过时钟访问的当前日期和时间

Instant

Instant用于记录时间线上某一瞬间的时间点,顾名思义就是时间戳,但它不同于System.currentTimeMillis();精度为秒

Instant可以精确到纳秒,它的取值范围为:-1000000000-01-01T00:00Z1000000000-12-31T23:59:59.999999999Z

下面看下他的常用方法示例:

  • now(): 获取基于UTC时间的Instant
  • ofEpochMilli(long milli):根据时间戳(毫秒)创建一个Instant实例
  • ofEpochSecond(long second): 根据时间戳(秒)创建一个Instant实例
  • parse(): 根据时间字符串转换为Instant实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//UTC
System.out.println(Instant.now());
//系统时区
System.out.println(Instant.now(Clock.systemDefaultZone()));
//根据时间字符串转换为Instant实例
System.out.println(Instant.parse("2020-06-06T12:12:12Z"));
Instant instant =Instant.parse("2020-06-06T12:12:12Z");
long milli = instant.toEpochMilli();
long second = instant.getEpochSecond();
//给定时间戳转换为Instant实例
System.out.println(Instant.ofEpochMilli(milli));
//给定时间戳转换为Instant实例
System.out.println(Instant.ofEpochSecond(second));
//给定时间戳和纳秒值转换为Instant实例
System.out.println(Instant.ofEpochSecond(second, 111));

输出结果:

1
2
3
4
5
6
2020-07-10T08:37:52.299Z
2020-07-10T08:37:52.380Z
2020-06-06T12:12:12Z
2020-06-06T12:12:12Z
2020-06-06T12:12:12Z
2020-06-06T12:12:12.000000111Z

Duration

Duration通常用秒或者纳秒相结合来表示一个时间量,最高精度为纳秒
通常用作表示两个时间之间的间隔,也称作持续时间,例如1s持续时间表示为PT1S

创建一个Duration实例

  • ofXXX()系列方法: 根据纳秒、毫秒、秒、分、时、天等时间来构造持续时间
  • from(TemporalAmount amount):根据TemporalAmount实例创建Duration对象
  • parse(CharSequence text):根据ISO-8601持续时间格式字符串创建Duration对象
  • between(Temporal startInclusive, Temporal endExclusive):获取两个时间对象之间的持续时间
1
2
3
4
5
6
7
8
9
10
11
System.out.println(Duration.ofNanos(1000));
System.out.println(Duration.ofMillis(1000));
System.out.println(Duration.ofSeconds(30));
System.out.println(Duration.ofSeconds(30,12345));
System.out.println(Duration.ofMinutes(1));
System.out.println(Duration.ofHours(1));
System.out.println(Duration.ofDays(1));
System.out.println(Duration.of(1000, ChronoUnit.MILLIS));
System.out.println(Duration.from(ChronoUnit.MINUTES.getDuration()));
System.out.println(Duration.parse("PT20.345S"));
System.out.println(Duration.between(Instant.parse("2020-06-23T10:15:30.00Z"), Instant.now()));

输出结果

1
2
3
4
5
6
7
8
9
10
11
PT0.000001S
PT1S
PT30S
PT30.000012345S
PT1M
PT1H
PT24H
PT1S
PT1M
PT20.345S
PT406H26M35.814S

Duration常用方法

  • getXXX(): 获取持续时间对象具体的秒数或者毫秒数
  • plusXXX(): 给Duration对象加上指定精度的值
  • minusXXX(): 给Duration对象减去指定精度的值
  • withXXX(): 修改Duration对象的秒数or毫秒数
  • 其他方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Duration d = Duration.parse("PT20.345S");

System.out.println(d.getSeconds());
System.out.println(d.getNano());

System.out.println(d.withNanos(3456789));//修改纳秒值,返回一个新的Duration
System.out.println(d.withSeconds(22));//修改秒值,返回一个新的Duration

System.out.println(d.plusNanos(1));//加1纳秒,返回一个新的Duration
System.out.println(d.plusMillis(100));//加100毫秒,返回一个新的Duration
System.out.println(d.plusSeconds(1));
System.out.println(d.minusNanos(1));//减去1纳秒,返回一个新的Duration
System.out.println(d.minusMillis(100));//减去10毫秒,返回一个新的Duration
System.out.println(d.minusSeconds(1));

System.out.println(d.isZero());//是否为0
System.out.println(Duration.ZERO.isZero());//是否为0
System.out.println(d.isNegative());//是否为负
System.out.println(d.negated());//求负
System.out.println(d.negated().abs());//求绝对值

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
20
345000000
PT20.003456789S
PT22.345S
PT20.345000001S
PT20.445S
PT21.345S
PT20.344999999S
PT20.245S
PT19.345S
false
true
false
PT-20.345S
PT20.345S

Period

与Duration类似都是用来表示持续时间
但是Period是由年月日为单位的时间量,例如1年2个月3天
与Duration相比,Period的用法与之基本相同

初始化Period

  • ofXXX()系列方法: 根据年月日来构造持续时间
  • from(TemporalAmount amount):根据TemporalAmount实例创建Period对象
  • parse(CharSequence text):根据ISO-8601持续时间格式字符串创建Period对象
  • between(LocalDate startDateInclusive, LocalDate endDateExclusive):获取两个日期对象之间的持续时间
1
2
3
4
5
6
7
8
9
System.out.println(Period.of(1, 2, 3));//根据年月日构造Period
System.out.println(Period.ofDays(1));
System.out.println(Period.ofMonths(2));
System.out.println(Period.ofWeeks(3));//根据周数构造
System.out.println(Period.ofYears(1));
System.out.println(Period.from(Period.ofMonths(1)));
System.out.println(Period.parse("P20Y10M5D"));//根据ISO-8601时间格式字符串进行构造
//计算两个日期对象之间的持续时间
System.out.println(Period.between(LocalDate.now().minusYears(1).minusDays(1),LocalDate.now() ));

输出结果

1
2
3
4
5
6
7
8
P1Y2M3D
P1D
P2M
P21D
P1Y
P1M
P20Y10M5D
P1Y1D

Period常用方法

常用方法的使用方式与Duration也基本类似

  • getXXX(): 获取持续时间对象具体的年、月、日
  • plusXXX(): 给Period对象加上指定精度的值
  • minusXXX(): 给Period对象减去指定精度的值
  • withXXX(): 修改Period对象的某一精度值
  • 其他方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Period p = Period.of(1, 2, 3);
//获取年月日
System.out.println(p.getYears()+"年"+p.getMonths()+"月"+p.getDays()+"日");
//重设Period的年月日
System.out.println(p.withYears(3).withMonths(2).withDays(1));
//加上1天
System.out.println(p.plusDays(1));
//减去1天
System.out.println(p.minusDays(1));
//判断是否为0
System.out.println(p.isZero());
//判断是否为负
System.out.println(p.isNegative());
//取负
System.out.println(p.negated());

输出结果

1
2
3
4
5
6
7
1年2月3日
P3Y2M1D
P1Y2M4D
P1Y2M2D
false
false
P-1Y-2M-3D

Clock

Clock表示一个时钟,Clock的实例用于查找当前时刻,可以使用存储的时区来解释当前时刻以查找当前日期和时间。某种程度上可以使用时钟代替System.currentTimeMillis()TimeZone.getDefault()

我们可以自定义创建一个指定滴答间隔的时钟,用来获取需要的时间日期

钟表的滴答间隔(tickDuration):规定了提供下一个读数的时间间隔。比如,滴答间隔为 1 秒的钟表,读数的分辨率就到 1 秒。滴答间隔为 5 秒的钟表,读数的"分辨率" 就到 5 秒。这里,5 秒的"分辨率"是指,当实际时间数据是 0 或 1、2、3、4 秒时,从它那里得到的读数都是 0 秒。当实际时间数据是 5 或 6、7、8、9 秒时,从它那里得到的读数都是 5 秒。

Clock的初始化

1
2
3
4
Clock clock = Clock.systemUTC();
System.out.println(clock.millis());//打印时钟当前毫秒值
System.out.println(System.currentTimeMillis());//打印当前毫秒值
System.out.println(clock.instant().toEpochMilli());//时钟转换为Instant实例并获取时间戳毫秒值

输出结果

1
2
3
1594371253772
1594371253772
1594371253773

自定义Clock的创建

使用tick()方法创建一个滴答间隔为3s的时钟,每1s钟查看一下它的时间

1
2
3
4
5
6
7
8
9
10
//系统默认时区时钟
Clock clock = Clock.systemDefaultZone();
//滴答时间间隔为3秒的时钟
//当实际时间数据是 0 或 1、2秒时,从它那里得到的读数都是 0 秒。当实际时间数据是 3或 4、5秒时,从它那里得到的读数都是 3 秒。
Clock tick = Clock.tick(clock, Duration.ofSeconds(3));

for (int i = 0; i < 10; i++) {
TimeUnit.SECONDS.sleep(1);
System.out.println(clock.instant()+"---> "+tick.instant());
}

输出结果如下,可以看到两个时钟每秒钟的计数是不同的:

1
2
3
4
5
6
7
8
9
10
2020-07-10T08:55:35.182Z---> 2020-07-10T08:55:33Z
2020-07-10T08:55:36.195Z---> 2020-07-10T08:55:36Z
2020-07-10T08:55:37.195Z---> 2020-07-10T08:55:36Z
2020-07-10T08:55:38.196Z---> 2020-07-10T08:55:36Z
2020-07-10T08:55:39.197Z---> 2020-07-10T08:55:39Z
2020-07-10T08:55:40.198Z---> 2020-07-10T08:55:39Z
2020-07-10T08:55:41.198Z---> 2020-07-10T08:55:39Z
2020-07-10T08:55:42.199Z---> 2020-07-10T08:55:42Z
2020-07-10T08:55:43.199Z---> 2020-07-10T08:55:42Z
2020-07-10T08:55:44.200Z---> 2020-07-10T08:55:42Z

使用tickSeconds()tickMinutes()创建时钟

  • tickSeconds(ZoneId zone) : 创建一个滴答间隔为1秒的时钟
  • tickMinutes(ZoneId zone) :创建一个滴答间隔为1分钟的时钟
1
2
3
4
5
6
7
8
9
10
11
//系统默认时区时钟
Clock clock = Clock.systemDefaultZone();
//获取滴答间隔为1秒的钟表
Clock clock1 = Clock.tickSeconds(ZoneId.systemDefault());
//获取滴答间隔为1分钟的钟表
Clock clock2 = Clock.tickMinutes(ZoneId.systemDefault());

for (int i = 0; i < 10; i++) {
TimeUnit.SECONDS.sleep(1);
System.out.println(clock.instant()+"---> "+clock1.instant()+"---> "+clock2.instant());
}

输出结果,从左到右依次为,系统默认时钟—>滴答间隔1秒的时钟---->滴答间隔1分钟的时钟

1
2
3
4
5
6
7
8
9
10
2020-07-10T08:58:58.001Z---> 2020-07-10T08:58:58Z---> 2020-07-10T08:58:00Z
2020-07-10T08:58:59.001Z---> 2020-07-10T08:58:59Z---> 2020-07-10T08:58:00Z
2020-07-10T08:59:00.002Z---> 2020-07-10T08:59:00Z---> 2020-07-10T08:59:00Z
2020-07-10T08:59:01.002Z---> 2020-07-10T08:59:01Z---> 2020-07-10T08:59:00Z
2020-07-10T08:59:02.002Z---> 2020-07-10T08:59:02Z---> 2020-07-10T08:59:00Z
2020-07-10T08:59:03.003Z---> 2020-07-10T08:59:03Z---> 2020-07-10T08:59:00Z
2020-07-10T08:59:04.004Z---> 2020-07-10T08:59:04Z---> 2020-07-10T08:59:00Z
2020-07-10T08:59:05.005Z---> 2020-07-10T08:59:05Z---> 2020-07-10T08:59:00Z
2020-07-10T08:59:06.005Z---> 2020-07-10T08:59:06Z---> 2020-07-10T08:59:00Z
2020-07-10T08:59:07.006Z---> 2020-07-10T08:59:07Z---> 2020-07-10T08:59:00Z

总结

以上是Java8中针对瞬时时间、持续时间、时钟加入的新工具类,可以看到对于时间的概念区分更加细化、这四个基础的时间概念也是Java8中时间处理比较常用的模块,大家不妨上手敲几段代码试试。

一些有意思的问答

作者 LarsCheng
2020年4月20日 09:07

一些有趣的问答,做答时间(03.26)

1. 来做个自我介绍吧,以及有几道快问快答等你接招。

大家好,我是LarsCheng,一枚程序猿,来自古城西安,目前在杭州一家物联网科技公司从事服务端开发工作。平时喜欢电影和文字,热衷于尝试新的事物,从毕业开始坚持通过博客来分享并记录生活,努力让每一天都过得精彩和不同。

2. 你最近都在忙些什么?

除了工作外,最近在捣鼓如何通过容器技术(docker)实现cs游戏服务器的一键部署,这种高龄经典游戏与当下主流技术的碰撞简直不要太刺激。

3. 如果可以在世界上所有人中任意选择,你想邀请谁共进晚餐?

三年前的自己

虽然这世界很大有很多人和事还在等着我,但是有那么一个人在我失落时默默替我承受,在我高兴时只有他感受最深,陪伴我二十多年却很少为他着想,在上个月我收到了一封来自三年前的邮件,信中的字里行间满是稚嫩,如果真的有这个机会,我想与曾经的自己坐下来,一起和这位老朋友聊聊这几年的人和事。

4. 你最喜欢/最常用的微信表情(系统自带的不算)。

5. 你最近一次给/被别人成功安利了啥?
  • 被别人安利:韩剧《请回答1988》

你能想象一个大直男看这部剧处处被戳中泪点嘛?这是我第一次看完一整部韩剧,也是我看过的最温暖的一部剧,它涵盖了所有人的青春,所有人的记忆,整部剧充满了对生活热爱和期待,我也十分愿意把他安利给其他人,这是一部你不舍的快进的优秀影视剧!!!

  • 给别人安利:国产linux系统deepin

如果你厌烦了windows的广告弹窗,如果你像我一样是一个喜欢瞎捣鼓的人,不妨试试国产深度系统Deepin,Deepin是一款专注于日常办公和生活娱乐等基于linux内核,以桌面应用为主的开源GUV/linux的操作系统,支持笔记本电脑,台式机和一体机。使用Deepin已经快要半年了,感觉已经离windows越来越远。

6. 现在还有哪些内容是你上网时一定会点进去看的?

行业内新的技术框架案例分析,广受好评的的生产力工具推荐等

7. 说一个你最近常在思考的问题吧。
  • 如何将每一天过的精彩且不同?

人的一生大概有80年,那就是29200天,如果现在是25岁,那已经过去了31%的时间,你的人生还剩下69%,然而实际上,每一天都是在倒计时,你也无法预测未来还有多少时间。最近常常有人说:明天和意外永远也不知道哪一个先到,在这个充满躁动的大环境中,唯有把握自己的每一天才是最真实的。如何将往后的每一天过得精彩和不同,这是一个十分值得尝试的人生计划。

8. 都说科技改变生活。那你的生活呢,有什么“于是再也回不去了”的改变吗?无论正反,具体聊聊呗。
  • 移动支付

这无疑是近十年来国内最大的技术革新没有之一,时间回到2010年,那个时候过节送礼还很流行送钱包,然而现在呢,基本上没有使用钱包的习惯了,纸币已经到了没有机会花的时代,自从教会了父母使用扫一扫付款,他们也表示再也不用为找零钱而烦恼了。

9. 你工作,你生活,你也慢慢攒下了一批物件,试审视:它们中的哪些成了你的必须品(也就是,哪怕你换工作、换城市,你都打算一直带着的那些器物。)可按重要性排序,最重要的两件不妨展开聊聊。

作为一名断舍离晚期患者,一直秉承着用不到的就没用的原则,却偏偏对于票类有着极度的收藏癖,可能这与科技无关,但是确实是我辗转几个城市都一直不曾舍弃的藏品。大概从大学开始到现在的所有的车票、机票、电影票、景区票都保留至今,这些每一张票背后都是一段青春的记忆。可能慢慢往后就得变成电子票收藏了,这也是科技在进步的体现吧。

10.你还用过什么好物(软件/硬件/服务)是 少有人知道的?求分享![“少有人知道的”划重点]

如果你长期写博客,那么你一定为图床而烦恼过,pic-go就是一款专为文字创作着提供的图床神器,一键自动上传图片并返回Markdown图片地址,再搭配github作为图片仓库,使用体验总结为两个字:真香!

属于阿里旗下的团队协作工具, 如果你使用过一定会被他的ui所吸引。

但它吸引我的是:个人计划安排、日程功能。你可以通过它对你的工作、学习、生活进行规划,把你想做的事提上日程,画上截止日期。

11. 最近又有什么科技产品是你心痒痒想入的?一句话说明你为它找的理由。
  • Macbookpro 2020

当我听说mbp要出14寸并且采用剪刀键盘,那是真的爱了爱了 !

12. 跟上面相反,有什么是你想强烈吐槽、狠狠劝退的么?

思前想后目前用过的产品里面都还算中规中矩,没有让我忍不住要去吐槽的,比较佛系。

13. 工欲善其事,必先利其器。无论是工作还是生活,有什么特好的“磨刀法子”是你巴不得大家都知道的?

合理规划、阶段总结

无论工作还是生活,我的建议就是规划和总结,想清楚自己需要什么,并为之制定计划,通过每个阶段的总结和复盘来审视自己,长此以往你会发现,虽然你只是每天完成了一小部分,但是却离自己的目标越来越近。

14. 科技生活离不开内容,它们可能是书、是游戏、是电影、是音乐,甚至是App。来,请分享你的最爱吧。

书籍类:

  • 《代码大全》:程序员工程师必读
  • 《三体》:世界是偶然的,也是必然的

影视类:

  • 《粉雄救兵》(Queer Eye For The Straight Guy) :通过另一种方式帮助你重塑信心,保持自信
  • 《请回答1988》: 直男力荐,一定会温暖到你的一部优秀作品

APP类:

  • picsew :IOS下的长截屏工具
  • 365 Dots:一款时间规划工具
15. You are what you eat. 你在网上fo过什么有意思的账号?求分享!(可按 有趣—>严肃 排序;平台不限于YouTube/Twitter/B站/微博/播客/公众号)

github仓:

仓库中涵盖了清华大学计算机系大一到大四的基础课程的电子版教材、课后习题、历年试卷、复习资料等,对于计算机相关基础知识的温习和回顾或者有考研意向的同学十分有帮助,同样类似的仓库还有:浙江大学课程攻略共享计划

这一仓库收录了2020年新型冠状病毒肺炎相关的媒体报道和亲历者个人叙述,通过时间线的形式详细记录了COVID-19疫情进展的时间线。虽然只是事件记录型的仓库,但是贵在真实全面!

博客:

阮老师的科技爱好者周刊已经属于每周五必追的网络博客,通过每一期去了解海内外热点事件、最前沿的技术,以及最受欢迎的生产力工具。

通过文字记录和分享自己的生活,希望自己能一直坚持博客输出。

16. 如果要给别人一些美好生活的建议,你会说啥?
  • 与人分享:我从毕业开始搭建了自己的博客小站,并坚持通过它来分享我的工作和生活,在这个过程中不仅加强了自己对所掌握知识的理解,同时也提高了自身的语言组织能力。
  • 投资自己:自身的健康永远是第一位,保持强健的体魄是对未来的自己最好的投资!
17. 如果给你一张白板,可以在上面写上你想抗议的内容,你会写什么?

佛系少年暂时没有要抗议的,如果有那就去来一顿火锅解决问题。

18. #所有人问所有人# 轮到你问问题了!问一个你想问其他人的问题?

你是否想过与多年后的自己进行一次对话?

Java中“附近的人”实现方案讨论及代码实现

作者 LarsCheng
2019年12月18日 19:53

前言

在我们平时使用的许多app中有附近的人这一功能,像微信、qq附近的人,哈罗、街兔附近的车辆。这些功能就在我们日常生活中出现。

像类似于附近的人这一类业务,在Java中是如何实现的呢?

本文就简单介绍下目前的几种解决方案,并提供简单的示例代码

注: 本文仅涉及附近的人这一业务场景的解决方案讨论,并未涉及到相关的技术细节和方案优化,各位看官可以放心阅读。

基本套路和方案

目前业内的解决方案大都依据geoHash展开,考虑到不同的数据量以及不同的业务场景,本文主要讨论以下3种方案

  • Mysql+外接正方形
  • Mysql+geohash
  • Redis+geohash

Mysql+外接正方形

外接矩形的实现方式是相对较为简单的一种方式。

假设给定某用户的位置坐标, 求在该用户指定范围内的其他用户信息

此时可以将位置信息和距离范围简化成平面几何题来求解

实现思路

以当前用户为圆心,以给定距离为半径画圆,那么在这个圆内的所有用户信息就是符合结果的信息,直接检索圆内的用户坐标难以实现,我们可以通过获取这个圆的外接正方形

通过外接正方形,获取经度和纬度的最大最小值,根据最大最小值可以将坐标在正方形内的用户信息搜索出来。

此时在外接正方形中不属于圆形区域的部分就属于多余的部分,这部分用户信息距离当前用户(圆心)的距离必定是大于给定半径的,故可以将其剔除,最终获得指定范围内的附近的人

代码实现

这里只贴出部分核心代码,详细的代码可见源码:NearBySearch

在实现附近的人搜索中,需要根据位置经纬度点,进行一些距离和范围的计算,比如求球面外接正方形的坐标点,球面两坐标点的距离等,可以引入Spatial4j库。

1
2
3
4
5
<dependency>
<groupId>com.spatial4j</groupId>
<artifactId>spatial4j</artifactId>
<version>0.5</version>
</dependency>
  1. 首先创建一张数据表user
1
2
3
4
5
6
7
8
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL COMMENT '名称',
`longitude` double DEFAULT NULL COMMENT '经度',
`latitude` double DEFAULT NULL COMMENT '纬度',
`create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  1. 假设已插入足够的测试数据,只要我们获取到外接正方形的四个关键点,就可以直接直接查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
   private SpatialContext spatialContext = SpatialContext.GEO;    

/**
* 获取附近x米的人
*
* @param distance 距离范围 单位km
* @param userLng 当前经度
* @param userLat 当前纬度
* @return json
*/
@GetMapping("/nearby")
public String nearBySearch(@RequestParam("distance") double distance,
@RequestParam("userLng") double userLng,
@RequestParam("userLat") double userLat) {
//1.获取外接正方形
Rectangle rectangle = getRectangle(distance, userLng, userLat);
//2.获取位置在正方形内的所有用户
List<User> users = userMapper.selectUser(rectangle.getMinX(), rectangle.getMaxX(), rectangle.getMinY(), rectangle.getMaxY());
//3.剔除半径超过指定距离的多余用户
users = users.stream()
.filter(a -> getDistance(a.getLongitude(), a.getLatitude(), userLng, userLat) <= distance)
.collect(Collectors.toList());
return JSON.toJSONString(users);
}
private Rectangle getRectangle(double distance, double userLng, double userLat) {
return spatialContext.getDistCalc()
.calcBoxByDistFromPt(spatialContext.makePoint(userLng, userLat),
distance * DistanceUtils.KM_TO_DEG, spatialContext, null);
}
  1. 这里给出查询的sql
1
2
3
4
5
6
<select id="selectUser" resultMap="BaseResultMap">
SELECT * FROM user
WHERE 1=1
and (longitude BETWEEN ${minlng} AND ${maxlng})
and (latitude BETWEEN ${minlat} AND ${maxlat})
</select>

Mysql+geohash

前面介绍了通过Mysql存储用户的信息和gps坐标,通过计算外接正方形的坐标点来粗略筛选结果集,最终剔除超过范围的用户。

而现在要提到的Mysql+geohash方案,同样是以Mysql为基础,只不过引入了geohash算法,同时在查询上借助索引。

geohash被广泛应用于位置搜索类的业务中,本文不对它进行展开说明,有兴趣的同学可以看一下这篇博客:[GeoHash核心原理解析],这里简单对它做一个描述:

GeoHash算法将经纬度坐标点编码成一个字符串,距离越近的坐标,转换后的geohash字符串越相似,例如下表数据:

用户经纬度Geohash字符串
小明116.402843,39.999375wx4g8c9v
小华116.3967,39.99932wx4g89tk
小张116.40382,39.918118wx4g0ffe

其中根据经纬度计算得到的geohash字符串,不同精度(字符串长度)代表了不同的距离误差。具体的不同精度的距离误差可参考下表:

geohash码长度宽度高度
15,009.4km4,992.6km
21,252.3km624.1km
3156.5km156km
439.1km19.5km
54.9km4.9km
61.2km609.4m
7152.9m152.4m
838.2m19m
94.8m4.8m
101.2m59.5cm
1114.9cm14.9cm
123.7cm1.9cm

实现思路

使用Mysql存储用户信息,其中包括用户的经纬度信息和geohash字符串。

  1. 添加新用户时计算该用户的geohash字符串,并存储到用户表中
  2. 当要查询某一gps附近指定距离的用户信息时,通过比对geohash误差表确定需要的geohash字符串精度
  3. 计算获得某一精度的当前坐标的geohash字符串,通过WHERE geohash Like 'geohashcode%'来查询数据集
  4. 如果geohash字符串的精度远大于给定的距离范围时,查询出的结果集中必然存在在范围之外的数据
  5. 计算两点之间距离,对于超出距离的数据进行剔除。

代码实现

这里只贴出部分核心代码,详细的代码可见源码:NearBySearch

同样的要涉及到坐标点的计算和geohash的计算,开始之前先导入spatial4j

  1. 创建数据表user_geohash,给geohash码添加索引
1
2
3
4
5
6
7
8
9
10
CREATE TABLE `user_geohash` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL COMMENT '名称',
`longitude` double DEFAULT NULL COMMENT '经度',
`latitude` double DEFAULT NULL COMMENT '纬度',
`geo_code` varchar(64) DEFAULT NULL COMMENT '经纬度所计算的geohash码',
`create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `index_geo_hash` (`geo_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  1. 添加用户信息和范围搜索逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
private SpatialContext spatialContext = SpatialContext.GEO;

/***
* 添加用户
* @return
*/
@PostMapping("/addUser")
public boolean add(@RequestBody UserGeohash user) {
//默认精度12位
String geoHashCode = GeohashUtils.encodeLatLon(user.getLatitude(),user.getLongitude());
return userGeohashService.save(user.setGeoCode(geoHashCode).setCreateTime(LocalDateTime.now()));
}


/**
* 获取附近指定范围的人
*
* @param distance 距离范围 单位km
* @param len geoHash的精度
* @param userLng 当前经度
* @param userLat 当前纬度
* @return json
*/
@GetMapping("/nearby")
public String nearBySearch(@RequestParam("distance") double distance,
@RequestParam("len") int len,
@RequestParam("userLng") double userLng,
@RequestParam("userLat") double userLat) {
//1.根据要求的范围,确定geoHash码的精度,获取到当前用户坐标的geoHash码
String geoHashCode = GeohashUtils.encodeLatLon(userLat, userLng, len);
QueryWrapper<UserGeohash> queryWrapper = new QueryWrapper<UserGeohash>()
.likeRight("geo_code",geoHashCode);
//2.匹配指定精度的geoHash码
List<UserGeohash> users = userGeohashService.list(queryWrapper);
//3.过滤超出距离的
users = users.stream()
.filter(a ->getDistance(a.getLongitude(),a.getLatitude(),userLng,userLat)<= distance)
.collect(Collectors.toList());
return JSON.toJSONString(users);
}

/***
* 球面中,两点间的距离
* @param longitude 经度1
* @param latitude 纬度1
* @param userLng 经度2
* @param userLat 纬度2
* @return 返回距离,单位km
*/
private double getDistance(Double longitude, Double latitude, double userLng, double userLat) {
return spatialContext.calcDistance(spatialContext.makePoint(userLng, userLat),
spatialContext.makePoint(longitude, latitude)) * DistanceUtils.DEG_TO_KM;
}

通过上面几步,就可以实现这一业务场景,不仅提高了查询效率,并且保护了用户的隐私,不对外暴露坐标位置。并且对于同一位置的频繁请求,如果是同一个geohash字符串,可以加上缓存,减缓数据库的压力。

边界问题优化

geohash算法将地图分为一个个矩形,对每个矩形进行编码,得到geohash码,但是当前点与待搜索点距离很近但是恰好在两个区域,用上面的方法则就不适用了。

解决这一问题的办法:获取当前点所在区域附近的8个区域的geohash码,一并进行筛选。

如何求解附近的8个区域的geohash码可参考Geohash求当前区域周围8个区域编码的一种思路

了解了思路,这里我们可以使用第三方开源库ch.hsr.geohash来计算,通过maven引入

1
2
3
4
5
<dependency>
<groupId>ch.hsr</groupId>
<artifactId>geohash</artifactId>
<version>1.0.10</version>
</dependency>

对上一章节的nearBySearch方法进行修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

/**
* 获取附近指定范围的人
*
* @param distance 距离范围 单位km
* @param len geoHash的精度
* @param userLng 当前经度
* @param userLat 当前纬度
* @return json
*/
@GetMapping("/nearby")
public String nearBySearch(@RequestParam("distance") double distance,
@RequestParam("len") int len,
@RequestParam("userLng") double userLng,
@RequestParam("userLat") double userLat) {


//1.根据要求的范围,确定geoHash码的精度,获取到当前用户坐标的geoHash码
GeoHash geoHash = GeoHash.withCharacterPrecision(userLat, userLng, len);
//2.获取到用户周边8个方位的geoHash码
GeoHash[] adjacent = geoHash.getAdjacent();

QueryWrapper<UserGeohash> queryWrapper = new QueryWrapper<UserGeohash>()
.likeRight("geo_code",geoHash.toBase32());
Stream.of(adjacent).forEach(a -> queryWrapper.or().likeRight("geo_code",a.toBase32()));

//3.匹配指定精度的geoHash码
List<UserGeohash> users = userGeohashService.list(queryWrapper);
//4.过滤超出距离的
users = users.stream()
.filter(a ->getDistance(a.getLongitude(),a.getLatitude(),userLng,userLat)<= distance)
.collect(Collectors.toList());
return JSON.toJSONString(users);
}

Redis+GeoHash

基于前两种方案,我们可以发现gps这类数据属于读多写少的情况,如果使用redis来实现附近的人,想必效率会大大提高。

自Redis 3.2开始,Redis基于geohash有序集合Zset提供了地理位置相关功能

Redis提供6条命令,来帮助我们我完成大部分业务的需求,关于Redis提供的geohash操作命令介绍可阅读博客:Redis 到底是怎么实现“附近的人”这个功能的呢?

本文主要介绍下,我们示例代码中用到的两个命令:

  • GEOADD key longitude latitude member:将给定的空间元素(纬度、经度、名字)添加到指定的键里面
    • 例如添加小明的经纬度信息:GEOADD location 119.9886618073271630.27465803229662 小明
  • GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count]: 根据给定地理位置坐标获取指定范围内的地理位置集合(附近的人)
    • 例如查询某gps附近500m的用户坐标:GEORADIUS location 119.9886618073271630.27465803229662 500 m WITHCOORD

实现思路

  • 添加用户坐标信息到redis(GEOADD),redis会将经纬度参数值转换为52位的geohash码,
  • Redis以geohash码为score,将其他信息以Zset有序集合存入key中
  • 通过调用GEORADIUS命令,获取指定坐标点某一范围内的数据
  • 因geohash存在精度误差,剔除超过指定距离的数据

实现代码

这里只贴出部分核心代码,详细的代码可见源码:NearBySearch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

@Autowired
private RedisTemplate<String, Object> redisTemplate;

//GEO相关命令用到的KEY
private final static String KEY = "user_info";

public boolean save(User user) {
Long flag = redisTemplate.opsForGeo().add(KEY, new RedisGeoCommands.GeoLocation<>(
user.getName(),
new Point(user.getLongitude(), user.getLatitude()))
);
return flag != null && flag > 0;
}

/**
* 根据当前位置获取附近指定范围内的用户
* @param distance 指定范围 单位km ,可根据{@link org.springframework.data.geo.Metrics} 进行设置
* @param userLng 用户经度
* @param userLat 用户纬度
* @return
*/
public String nearBySearch(double distance, double userLng, double userLat) {
List<User> users = new ArrayList<>();
// 1.GEORADIUS获取附近范围内的信息
GeoResults<RedisGeoCommands.GeoLocation<Object>> reslut =
redisTemplate.opsForGeo().radius(KEY,
new Circle(new Point(userLng, userLat), new Distance(distance, Metrics.KILOMETERS)),
RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
.includeDistance()
.includeCoordinates().sortAscending());
//2.收集信息,存入list
List<GeoResult<RedisGeoCommands.GeoLocation<Object>>> content = reslut.getContent();
//3.过滤掉超过距离的数据
content.forEach(a-> users.add(
new User().setDistance(a.getDistance().getValue())
.setLatitude(a.getContent().getPoint().getX())
.setLongitude(a.getContent().getPoint().getY())));
return JSON.toJSONString(users);
}

方案总结

方案优势缺点
Mysql外接正方形逻辑清晰,实现简单,支持多条件筛选效率较低,不适合大数据量,不支持按距离排序
Mysql+Geohash借助索引有效提高效率,支持多条件筛选不支持按距离排序,存在数据库瓶颈
Redis+Geohash效率高,集成便捷,支持距离排序不适合复杂对象存储,不支持多条件查询

总结以上三种方案,各有优劣,在不同的业务场景下,可选择不同的方案来实现。

当然目前附近的人的解决方案并不仅仅这三种,以上权当是这一功能的入门引子,希望对大家有所帮助。

本文的三种方案均有源码提供,源码地址

参考文章

Redis 到底是怎么实现“附近的人”这个功能的呢?

Geohash求当前区域周围8个区域编码的一种思路

GeoHash核心原理解析

Java时间处理2----时区TimeZone类方法探究(Java8以前)

作者 LarsCheng
2019年11月22日 14:56

本文转载于CSDN博主「Gene Xu」
原文链接:https://blog.csdn.net/goodbye_youth/article/details/81807273

一、TimeZone 类的定义

TimeZone 类位于 java.util 包中,是一个抽象类,主要包含了对于时区的各种操作,可以进行计算时间偏移量或夏令时等操作

二、TimeZone 类常用方法

getAvailableIDs()

  • 获取Java支持的所有时区 ID
1
2
3
System.out.println(Arrays.toString(TimeZone.getAvailableIDs()));

// Asia/Shanghai, Asia/Chongqing, Asia/Hong_Kong, Asia/Macao, ...

getAvailableIDs(int rawOffset)

  • 根据 时间偏移量 来获取时区 ID
1
2
3
4
5
6
7
8
// 东八区时间,与标准时间相差8小时
System.out.println(Arrays.toString(TimeZone.getAvailableIDs(8*60*60*1000)));

// [Asia/Brunei, Asia/Choibalsan, Asia/Chongqing, Asia/Chungking, Asia/Harbin,
// Asia/Hong_Kong, Asia/Irkutsk, Asia/Kuala_Lumpur, Asia/Kuching, Asia/Macao,
// Asia/Macau, Asia/Makassar, Asia/Manila, Asia/Shanghai, Asia/Singapore,
// Asia/Taipei, Asia/Ujung_Pandang, Asia/Ulaanbaatar, Asia/Ulan_Bator,
// Australia/Perth, Australia/West, CTT, Etc/GMT-8, Hongkong, PRC, Singapore]

getDefault()

  • 获取当前系统的默认时区,中国默认为东八区
1
2
3
4
System.out.println(TimeZone.getDefault()); 

// sun.util.calendar.ZoneInfo[id="GMT+08:00",offset=28800000,
// dstSavings=0,useDaylight=false,transitions=0,lastRule=null]

setDefault(TimeZone zone)

  • 设置当前系统的默认时区
1
2
3
4
5
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
System.out.println(TimeZone.getDefault());

// sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,
// dstSavings=0,useDaylight=false,transitions=19,lastRule=null]

getTimeZone(String ID)

  • 根据时区 ID 来获取其对应的时区
1
2
3
4
System.out.println(TimeZone.getTimeZone("GMT+08:00"));

// sun.util.calendar.ZoneInfo[id="GMT+08:00",offset=28800000,
// dstSavings=0,useDaylight=false,transitions=0,lastRule=null]

getTimeZone(ZoneId zoneId)

  • 根据 ZoneId 对象来获取其对应的时区
1
2
3
4
System.out.println(TimeZone.getTimeZone(ZoneId.of("GMT+08:00")));

// sun.util.calendar.ZoneInfo[id="GMT+08:00",offset=28800000,
// dstSavings=0,useDaylight=false,transitions=0,lastRule=null]

getDisplayName()

  • 获取该 TimeZone 对象的时区名称
1
2
TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai");
System.out.println(timeZone.getDisplayName()); // 中国标准时间

getDisplayName(Locale locale)

  • 获取该 TimeZone 对象的时区名称,并根据 Locale 对象进行国际化
1
2
3
TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai");
System.out.println(timeZone.getDisplayName()); // 中国标准时间
System.out.println(timeZone.getDisplayName(Locale.ENGLISH)); // China Standard Time

getDisplayName(boolean daylight, int style)

  • 获取该 TimeZone 对象的时区名称

  • daylight

    • true:指定夏令时名称
    • false:指定标准时间名称
  • style

    • TimeZone.LONG:显示全称
    • TimeZone.SHORT:显示简称
1
2
3
4
TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai");
System.out.println(timeZone.getDisplayName()); // 中国标准时间
System.out.println(timeZone.getDisplayName(false, TimeZone.LONG)); // 中国标准时间
System.out.println(timeZone.getDisplayName(false, TimeZone.SHORT)); // CST (China Standard Time)

getDisplayName(boolean daylight, int style, Locale locale)

  • 获取该 TimeZone 对象的时区名称,并根据 Locale 对象进行国际化

  • daylight

    • true:指定夏令时名称
    • false:指定标准时间名称
  • style

    • TimeZone.LONG:显示全称
    • TimeZone.SHORT:显示简称
1
2
3
TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai");
System.out.println(timeZone.getDisplayName()); // 中国标准时间
System.out.println(timeZone.getDisplayName(false, TimeZone.LONG, Locale.ENGLISH)); // China Standard Time

getID()

  • 获取该 TimeZone 对象的时区 ID
1
2
TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai");
System.out.println(timeZone.getID()); // Asia/Shanghai

setID(String ID)

  • 设置该 TimeZone 对象的时区 ID
1
2
3
TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai");
timeZone.setID("Asia/Chongqing");
System.out.println(timeZone.getID()); // Asia/Chongqing

getOffset(long date)

  • 获取该时间所在时区的时间偏移量
1
2
TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai");
System.out.println(timeZone.getOffset(System.currentTimeMillis())); // 28800000

getDSTSavings()

  • 在夏令时规则生效时,返回相对于标准时间提前的毫秒数

  • 如果此时区不实施夏令时,则为 0

1
2
3
TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai");
// 中国没有夏令时,故为0
System.out.println(timeZone.getDSTSavings()); // 0

getRawOffset()

  • 获取时间原始偏移量,该值不受夏令时的影响,故称为时间原始偏移量
1
2
TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai");
System.out.println(timeZone.getRawOffset()); // 28800000

setRawOffset(int offsetMillis)

  • 设置时间原始偏移量
1
2
3
TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai");
timeZone.setRawOffset(25200000);
System.out.println(timeZone.getRawOffset()); // 25200000

toZoneId()

  • 将 TimeZone 对象转换为 ZoneId 对象
1
2
3
TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai");
ZoneId zoneId = timeZone.toZoneId();
System.out.println(zoneId); // Asia/Shanghai

useDaylightTime()

  • 查询此时区是否使用夏令时
1
2
TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai");
System.out.println(timeZone.useDaylightTime()); // false

inDaylightTime(Date date)

  • 查询给定的日期是否在此时区的夏令时中
1
2
TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai");
System.out.println(timeZone.inDaylightTime(new Date())); // false

hasSameRules(TimeZone other)

  • 如果两时区仅时区 ID 不同,但具有相同的规则和时间偏移量,则返回 true

  • 如果另一个时区为空,则返回 false

1
2
3
TimeZone timeZone1 = TimeZone.getTimeZone("Asia/Shanghai");
TimeZone timeZone2 = TimeZone.getTimeZone("Asia/Chongqing");
System.out.println(timeZone1.hasSameRules(timeZone2)); // true
❌
❌