java唯一字符串ID生成方案

工作中经常会有生成唯一字符串的需求。通常最容易想到的是UUID。UUID的唯一性毋庸置疑,但是32位的长度也容易让人退避三舍。也曾经想过参考《短网址生成方案》来生成一串ID,但是试验了一下发现唯一性不太好。

最终采用的方案是时钟方案,简单来说就是用当前时间戳做唯一ID。

采用时间戳做ID,秒或毫秒都容易产生重复,换成纳秒在单节点上就没问题了。参考百度百科关于纳秒的描述就能清楚为什么纳秒级别的时间戳不会产生重复:

光在真空中一纳秒仅传播0.3米。个人电脑的微处理器执行一道指令(如将两数相加)约需2至4纳秒。

我们生成一条唯一ID所需的CPU指令绝不止一道,因此用纳秒作单机唯一ID是绰绰有余的。在测试中发现,即使是千分之一纳秒也足够我们在PC机上生成唯一ID了。

至于长度:对原始值做一次Base62处理,长度就能缩减到令人满意的程度。

不多废话,直接上代码:

这里用千分之一纳秒做基数(经测试,基数在10w分之一纳秒内都是安全的),再加上1~99的顺序号来生成唯一ID。最终可以保证在大于10纳秒(近似)的时间区间内不会产生重复值。

为了缩减长度,对字符串做了Base62处理。在处理前又将纳秒数值做了一次翻转处理。不难想象,如果直接使用原始值来做Base62处理,因为时钟的特征,最终生成的值的前几位都是相同的。

来看一下这个程序生成的ID:

八位的长度,唯一且整齐。下面是一个单元测试:

这里只对10240的规模做了测试。因为唯一ID是基于时钟生成的,所以测试时整体规模的大小不影响ID的唯一性(和短链接方案不一样)。但是太小了也不行——顺序号会发挥作用。10240算是一个中庸的值,足够暴露问题,也不会有太多的冗余。

可以在github看到相关的代码 GitHub / UniqId

仍然需要强调一下:这个方案只能保证在(当前)单机上的唯一性,如果是集群范围内建议采用其他方案,或者加上一两位机器ID。

Java 抽象工具类

在SpringBoot的源码中有看到使用abstract关键字定义的工具类,如:

使用abstract关键字的目的猜测应该是为了避免实例化。

同样为了避免实例化,在jdk中定义的工具类则通常是使用私有化构造器来实现的:

目的都是为了避免实例化,这两种方案无非是“茴”字的不同写法而已。

我自己比较偏好第二种写法,原因有二:

  1. abstract关键字在语义上的作用不是简单的为了避免实例化,更何况说是用来定义工具类了
  2. 阿里p3c的规范要求Java工具类使用第二种方案(安装了相关插件,每次都被提醒很头疼的)

但对于第二种方案我通常习惯更多做一些事情,比如下例:

在工具类定义时添加了final关键字避免被继承修改;在私有构造器里面抛出了异常以防止私有构造器被反射调用。当然这也有可能是我杞人忧天了。如果真的有心要做些什么,通常也是防不住的。

在StackOverflow上也有对这个问题的讨论:Should Helper/Utility Classes be abstract?。有趣的是乱入了一些C# Developer,让我们有机会一窥其他语言中的特色。

End!

Java AES加密

做360广告的对接需要对密码进行AES加密,下面是点睛平台文档的描述:

(AES模式为CBC,加密算法MCRYPT_RIJNDAEL_128)对MD5加密后的密码实现对称加密。秘钥是apiSecret 的前16位,向量是后16位,加密结果为64位数字和小写字母。

用Java实现AES需要依赖Java加密扩展(The Java Cryptography Extension,简称JCE)的支持——主要是在javax下面的一些包。根据描述需要使用的算法为“AES/CBC/NoPadding”,实现方案如下:

这里使用的SecretKeySpecAlgorithmParameterSpecIvParameterSpec等类都是JCE提供的,通常在JVM环境下可以直接使用。Hex.encodeHexString()方法则是由apache-commons-codec提供的。如果不想多引入一个依赖也可以使用下面的方法:

下面是为这个加密方法写的单元测试:

这里的代码大体上能够满足360广告的对接需求了。但是因为jdk11偶尔对一些javax扩展包的不支持,我有些不太喜欢这个方案。另外在一些资料中也了解到jdk对AES 256加密是有一些限制的,要响应相关限制需要引入一个授权文件或者更换jdk,这就有些难接受了。种种原因吧,我需要一个替换方案。

最开始我以为在apache-common-codec中会有相关方案,但是结果是让人失望的。不过还好,最终我找到了Bouncy Castle。以下是关于Bouncy Castle的一些描述:

Bouncy Castle 是一种用于Java平台的开放源码的轻量级密码算法包。它支持大量的密码算法,并提供 JCE 1.2.1 的实现。Bouncy Castle是轻量级的,从J2SE 1.4到J2ME(包括MIDP)平台,它都可以运行。它是在MIDP上运行的唯一完整的密码术包。

使用Bouncy Castle提供的能力必然需要先引入相关的依赖。针对不同的jdk版本,Bouncy Castle都有提供对应的Cryptography Provider。比如我使用的是JDK1.8,对应的就是bcprov-jdk15to18,相关的依赖如下:

基于Bouncy Castle实现的360点睛平台AES加密处理如下:

因为360点睛平台要求使用的加密key没有超过256位,所以两个方案都是行得通的。

我比较喜欢Bouncy Castle这个方案,这个方案相对较轻量,并且不依赖JCE。但是这个方案的不足之处也恰恰在于此:Java中的SSL层,JSSE和XML加密库都依赖到JCE,而且AES Key长度的校验是在Cipher类中进行的,在这些场景下Bouncy Castle也很难起到作用。

参考文档

 

一个极简的PHP框架:zero-framwork

如标题所示,最近这两个月学着用PHP写了个东西,顺道产出了一个框架:zero-framework。

其实用PHP写东西这个事情去年就开始了,期间研究过typecho,体验了ThinkPHP还有两三个非常粗糙的所谓的极简框架(比我的zero-framework还要粗糙)。在多番折腾后最终用CodeIgniter写了一个CMS应用。

总体来说,CodeIgniter的轻量级和灵活性还是非常对我的胃口的。但是有两个地方不太容易接受:数据库操作上的种种自以为是和对IDE(PHP Storm)的不友好。尤其是后者,每次想查看一个方法的代码时都无法自由跳转实在是让人憋闷。所以后来干脆用java把整个功能又重新实现了一遍。

本来以为事情就这样了,但是维护VPS运行环境实在不让人省心,加上php虚拟主机的诱人价格,所以又将目光转回了PHP。不过这次我计划不使用任何框架,直接使用原生的PHP。另外也隐约想到在我的产品完成的时候,也许会产生一个框架。毕竟所谓框架最初也只是开发习惯的一个集合而已。

在整合了路由功能和数据库操作能力后,框架的雏形就已经有了。随着开发的进展、结束以及上线,zero-framework逐步成形——按我期望的样子。受CodeIgniter影响颇深,此外也不能说没有Java SpringMVC的作用,zero分成了Controller / Service / Model三层,也就是网络/业务/数据库三层。为了能在IDE中清晰地追踪方法调用,每层的对象都是显示创建的。显式创建对象这个事情并不是一种好的方案,而像ci那样通过load来避免重复创建对象会更好些。但是为了对IDE更友好些,最终采用了前者,因此在开发时也需要格外注意减少对象重复创建。zero的数据库操作使用了ActiveRecord的形式,不止提供了常规的快捷操作方案,也支持自定义SQL灵活查询。此外因为有多主题的需求,提供了主题切换方案。zero也就是这些特点了。毕竟初学PHP,而且只使用它完成了一个相对较小的需求,zero肯定有许多不完备的地方。

zero-framework的项目地址在 GITHUB/zero-framwork。基于zero完成的产品是 Buffalo,一个小说网站或CMS网站。有和我面临同样需求的同学可以参考下,毕竟有源码也有实例不是。

再聊些对PHP的看法,先夸优点:PHP不愧是最好的编程语言,入门相对简单(初学者就能产出一个框架),天生对WEB非常友好,类脚本语言修改和调试起来都非常简单快捷,应用hook可以灵活地实现AOP操作。不足之处是它提供的魔术方法很容易被滥用,弱类型语言也比较难得到IDE的充分支持,此外就是debug的繁琐了,PHP缺少一个足够好用的debug工具。另外作为一个Javaer,刚开始使用PHP时,对于内存操作这块儿充满了各种疑问。

也就这些可以说说了。最后还想废话一下:因为洁癖或者说是习惯就抛弃既有的成熟框架,反而花费大量时间和精力去造一个新的轮子,这并不是一种值得提倡的做法。但是人生苦短,代码既然能让我得到一点快乐,为何不稍稍允许自己任性一点呢。

JetBrains DataGrip的JavaFx错误提示及修复

这两天(20200409)手欠升级了DataGrip(JetBrains的数据处理工具),没想到升级完成后一打开就报了下面的错误:

Tried to use preview panel provider (JavaFX WebView), but it is unavailable. Reverting to default.

后来发现是markdown文档插件的问题。关键是这个Markdown插件是JetBrains官方提供的,没想到居然也会报错。

因为吃过一次类似的亏,所以后来注意到错误提示中的JavaFx字样后第一反应就是“是不是DataGrip嵌入的JRE升级到了JDK11”。

查看DataGrip的about信息,看到JDK版本是11.05。看样子就是这个问题了。

第一个思路是将JavaFx的相关依赖放到Markdown插件的lib目录下。去官网下载了JavaFx,解压后将lib目录下的jar文件放到Markdown插件的lib目录下,重启,继续报错。该方案失败!

第二个思路很直接,既然放在插件目录下不行,那就放到DataGrip的依赖目录下。这次尝试将JavaFx的jar放到DataGrip安装目录下的lib目录下。重启DataGrip,打开Markdown文件后没有报错,我一度以为我成功了,但是接下来就发现DataGrip卡死了。只好杀死进程,删除放到DataGrip安装路径lib目录下的JavaFx相关jar后再想其他办法。

第三个方案是在JetBrains社区找到的,是可行的。所以简要介绍下步骤:

  1. 安装插件Choose Runtime,重启DataGrip
  2. 在菜单栏中选择Help->Find Action(快捷键Ctrl+Shift+A)打开快捷窗口,在Actions选项卡下输入Choose Runtime,然后Enter调用ChooseRuntime插件
  3. 选择JRE,可以直接选择自己安装的JRE,也可以选择下载JetBrains提供的JRE并安装,完成后DataGrip会重启

重启后打开Markdown文件不再报错。问题得到解决。不过,美中不足的是发现打开的DataGrip多了一个难看的Windows标题栏。解决的方法也比较简单,换一个本机安装的JetBrains其它产品(比如IDEA或PyCharm)的嵌入JRE就可以了。

JetBrains也有提供独立的JRE,点击此处进行下载。

修改“idea.config.path\<product>.exe.jdk”文件也可以完成JRE的配置。

JetBrains嵌入的JRE在本机的地址为产品安装目录下的jbr目录,比如我机器上DataGrip嵌入的JRE路径如下:

就这样!End!

springboot入门10 – 修改banner

这个内容有点儿水了。但是将springboot启动时的banner修改一下是个蛮好玩的事情。比如,不知道什么时候,我们组的springboot应用的banner就被改成了这个样子:

据说改了之后BUG真的少了耶!(*/ω\*)

修改方式也比较简单,创建一个名为banner.txt的文件,写入图标字符,然后将这个文件放到resource目录下。搞定了。就这样。

不过,springboot还是提供了一些配置信息的。下面是可以在banner.txt中使用的一些替换宏:

  • ${AnsiColor.BRIGHT_RED}:设置控制台中输出内容的颜色
  • ${application.version}:用来获取MANIFEST.MF文件中的版本号
  • ${application.formatted-version}:格式化后的${application.version}版本信息
  • ${spring-boot.version}:Spring Boot的版本号
  • ${spring-boot.formatted-version}:格式化后的${spring-boot.version}版本信息

还有一些其它替换宏,在idea中编辑banner.txt文档的时候这些都有动态提示。

还有一些在application中使用的配置:

最后记录几个生成ascii字符图像的网站(话说这也是我写这篇文的初衷):

  • 图像转字符图像:https://www.degraeve.com/img2txt.php
  • 文字转字符图像:http://patorjk.com/software/taag

End!

rz上传出错解决办法

先记录下rz / sz 的安装指令:

使用rz上传文件使用的是这个指令:

这个指令传递小文件问题不大,但是传稍微大点儿的文件传到一半就会崩溃,会在终端输出一些莫名其妙的内容,同时也会导致终端不可用。

解决方式是使用的时候添加参数b

涉及到的几个参数意义如下:

这个问题以前遇到过,也解决了,但是过了一段时间再遇到就忘了。所以记录下来。

Linux下修改时区

我使用的机器操作系统是centOS8。所以这里使用的指令也是centOS8上的指令(不过也没差别)。

1. 首先要确定时区

确定时区需要执行tzselect指令

输入完成指令后提示选择大洲。我这里选了4-亚洲。

然后就是选择 国家->市,确认等操作:

2. 配置时区

执行完成选择后并不会直接生效,还需要我们做些操作。根据提示,需要将下面的内容添加到profile文件中:

执行vim指令打开profile文件:

profile文件尾部追加上面的配置信息。然后执行source指令使配置生效。

执行date查看时间:

可以看到和当前时间是一致的。说明配置已经生效。

End!

Linux 下安装MySQL并迁移备份

简单记录下在centOS上安装MySQL(MariaDB)的过程。

这里我并没有选择特定的MySQL版本,使用的是源默认提供的版本。

1. 执行安装命令:

执行如下指令安装MySQL数据库。

注意这里同时安装了mysqlmysql-server

在输出一长串信息后,期间可能会需要输入y表示确认,MySQL就安装好了。

安装结束后还需要手动启动MySQL。

2. 启动MySQL

执行如下指令启动MySQL。

启动MySQL后会输出日志到/var/log/mysql/mysql.log

在日志中可以看到下面这一句:

日志提示mysql root用户的初始密码是空的。据说有的时候会生成一个随机密码写入到日志中,但这次安装明显不是这样的。

另外在日志中也可以看到mysql的版本是8.0.17

然后,执行如下指令,设置MySQL数据库开机启动

3. 设置root用户密码

执行如下命令设置root用户密码:

执行命令后会提示输入密码并确认。

4. 修改数据库时区

执行如下命令后输入密码进入MySQL数据库:

切换到目标数据库:

查看数据库时间:

查看时区设置:

  • system_time_zone 表示系统使用的时区是EDT即北美的东部夏令时(-4h)。
  • time_zone 表示 MySQL 采用的是系统的时区。

之前以为如果在安装MySQL就通过tzselect并在profile中修改了时区设置会起到作用,后来测试发现是无效的。 ╮(╯▽╰)╭

临时修改时区执行如下指令:

永久修改时区需要修改mysql配置文件。执行如下指令打开MySQL配置文件:

在条目[mysqld]下添加时区配置信息:

当然直接编辑/etc/my.cnf也不是不行,不过讲究点儿还是好的。

重启MySQL服务:

再看时间会看到时区修改成功。

5. 创建用户并授权

为MySQL数据库添加一个新用户zhyea,并将密码设置为zhyPass,指令如下:

授予用户zhyea对数据库chobit所有权限:

6. 数据备份

如需要执行数据迁移,先回到原数据库服务器上,进入MySQL命令行,执行如下命令完成数据备份:

用户为zhyea;密码为zhyPass;目标数据库 chobit。备份文件存储到了/root/zhyea/zhy.sql这个位置。

下载备份文件zhy.sql

7. 数据迁移

将备份的sql文件 zhy.sql 挪到现服务器上。可以考虑使用rz指令上传数据文件,如未安装该工具可以用如下指令安装:

进入MySQL命令行,切换到目标数据库,执行如下命令完成数据迁移:

8. 删除已安装的版本

执行如下指令:

一切搞定。

End!

springboot入门09 – 实现伪静态

最近想了下springboot前端路径的伪静态实现。

通过百度最容易找到的方案是使用urlrewritefilter这个依赖。不过一想到要为这么一件事情就添加一个依赖,还要再添加一个配置文件,还要挨个写一遍所有的路径映射就觉得头疼,所以pass。

跟踪了一下springboot WEB请求处理的过程,找到了一个关键类:UrlPathHelper。在获取Handler之前,由这个类负责解析请求路径。正好可以在WebMvcConfigurer中配置UrlPathHelper类的实例,这就给了我们动些手脚的空间。

更贴心的是,springboot获取请求路径的第一选择不是调用HttpServletRequest.getRequestURI()方法或者HttpServletRequest.getServletPath()方法,而是从HttpServletRequestattribute中获取,这样能省下不少力气。

来看下实现方式。

1. 实现伪静态路径解析类

伪静态路径解析类是UrlPathHelper类的一个子类,通过重写getRequestUri()方法实现了伪静态路径的处理。

代码如下:

代码中的核心部分是下面两句:

在后面处理中,springboot会优先从Attribute中获取请求路径。

2. 添加WebMvcConfigurer配置类

然后需要在WebMvcConfigurer中配置PseudoStaticPathHelper实例。

实现方式如下:

然后——这样就可以了。

不写示例应用了。在 Calf 这个项目中有类似的用法,有需要就看看。

End!