Windows10 图形界面用的越久,路径依赖越深,从 Windows10 切换到 Apple 的 Big Sur,一时会有诸多不习惯,反之亦然。
作者认为不必要墨守成规,一切都以自己最舒服,能够最大化提高效率为最终目的,各类型的操作系统各有优劣,应取彼之长,补己之短。
如无特别说明,本篇 Windows 系统指的是 Win10 20H2、Mac系统指的是 Big Sur,Unix* 指的是所有Unix、Linux 及其各类发行版,鼠标和键盘指的是市面上最流行的通用键鼠。
运行操作系统的主机配置暂不在本篇讨论范围内。
本文适合读者:
于互联网软件开发者而言,最终的服务往往部署于 Unix* 系统之上。
尽管 Windows Server 的发展一直没有停止,PowerShell 也是开发以及运维利器。
但由于收费,加之能够熟练使用的人很少,在互联网服务器端市场占有量小于 Unix* 系统,而且互联网企业一般都有自己的运维或者,兼职运维。
而且由于历史原因以及系统、环境的配置自由度等原因,Windows Server 在传统企业占据市场份额较多。
Mac 系列天生具有和 Unix* 的亲和力,大部分命令可以和 Unix* 系统无缝衔接,篇幅以及重点所限,操作系统的发展历程以及关联关系暂不展开。
于设计师而言,Mac 系列和 Windows 系列都有很多好用的设计软件,少部分软件为各自平台特有。
其他需要使用计算机的工程人员以及各专业学生等,还是得根据自己的行业特点,个人偏好等多方面考虑,可以装个虚拟机每个都体验一段时间,不过 Mac 系统还是得和 Apple 自家的硬件配合,才能获得最完整的使用体验。
Mac 的特点动画是做的好看,平板、手机、电脑之间能够流畅的协同工作,可以获得一致的体验,可选择的设备不是很多,贵。
Windows 的特点是如果熟练掌握各类系统设置以及快捷键,可以获得目前来说最(违反广告法所以划掉)好的多任务与多桌面体验,可选择的品牌以及设备非常多,硬件性价比高。
软件开发者,可以通过一些搭配来同时使用多种系统,比如 Windows + WSL、Linux + RDP、Mac + RDP或者虚拟机等等。
各系统其他的优点以及缺点,可能涉及到的主观性比较强,暂不展开。
如果把每个设置都截图以及标注,这很繁琐,而且通过搜索引擎关键字 “Mac xxxx”,基本上可以搜索到所有设置的具体操作方法,这里作者就直接纯文字说明配置方案以及原因,不再配图。
首先把默认的滚动方向:自然,取消勾选。
对于长内容来说,右侧的滚动条和内容的方向是反着的。
Mac 的触摸板双指滑动默认是和内容滚动方向一致,作者选择了延续鼠标拖动滚动条的逻辑,即手指下移,内容向上滚动,因为想看的是往往是下面的内容,和目光方向一致。
不过这个设置很容易习惯,无论是以内容为准还是以滚动条为准,只要拖两下就适应了。
如果只使用触摸板倒是不必更改这个设置,作者是由于鼠标和触摸板混用,需要保持一致的逻辑。
其他的三指、四指相关的操作,系统都给了实际的例子参考。
这里推荐一个触摸板增强工具 BetterAndBetter,增加了一些触摸板手势以及键盘快捷键、边角触发等等。
作者认为笔记本键盘和屏幕只适合低频度使用,高频使用的话还是得外接键盘和显示器,104 键很舒服,小键盘好用,不过不如一些 80%、60%的键盘便携。键盘选择不在本篇讨论范围内。
由于 QWER 布局基本上纵横无敌手,如无特别说明,本文中所有的键盘都是此布局。
先说 Ctrl , 可以近似代替 Mac 中的 Command 键,除此之外,Command键还包含一小部分 Windows 键的作用,Option 键基本上可以视为 Alt 键。
有的人觉得左手小指按起来比较乏力,有一种方法是使用左手小拇指根,这样其他手指在键盘上的位置,基本不变。
由于作者习惯使用左手大拇指按空格键, Mac 却是鼓励左手大拇指按 Command,像是最常用的编辑类快捷键, Command + C/V/X,而且外接键盘间距较大,拇指和食指别过来就很难受。
还有一种方式是将没用的 Caps Lock 映射成 Ctrl , 用无名指或者小指按起来就挺顺手,大小写以及中英切换只用一个 Shift 就可以了。
所以作者使用了 Karabiner 这个软件,更改了映射键位。并且在 这个配置分享网站 找了一套 Mac to Windows 的键位映射,个人觉得非常舒适。
触发角是指当鼠标光标移动到屏幕的 4 个拐角时,可以触发特定的动作,Windows 上只有一个回到桌面,Mac 上则是 4 个都可以自定义设置,不过可选的设置项有限。
作者将左上角设置为显示桌面,个人习惯桌面上只放一个垃圾桶,然后整个桌面作为一个短时的 Inbox 或者工作台,这样在日常使用的时候,鼠标往左上方一丢,就可以回到桌面,也可以方便的拖动网页图片到桌面,如果是触摸板的话,张开拇指和其他三指,是相同的功能。
Windows 下则是 Windows + D 快捷键,或者光标悬浮在任务栏和 Windows 徽标相对的底角。
左下角设置为调度中心,甩一下鼠标,可以切换多任务与多桌面。
右上角设置为显示启动台,甩一下鼠标,然后使用滚轮,可以很快的找到自己想要的程序,如果程序太多的话,可以使用 Command + 空格来打开聚焦搜索,外接键盘则是 Windows + 空格。
如果是触摸板的话,捏合拇指和其他三指,两指左右滑动,是相同的功能。
右下角设置为锁屏,当有事离开电脑的时候,鼠标往右下角一丢,直接锁屏,触摸板则是划拉一下就行。这次和快捷键相比,鼠标扳回一局。
Mac 默认的锁屏快捷键是 Ctrl + Command + Q ,会和 Mac 版本的 QQ 冲突,需要改一下就QQ的快捷键, Windows 则是 Windows + L。
二者也都可以通过第三方软件增强触发角的功能。
首先把程序坞里默认自带的乱七八糟不常用的软件移除掉,系统自带软件默认不能卸载,虽然有非常规操作可以卸载,但是有可能影响系统稳定性,还是在启动台建个文件夹,全部拖进去吧,眼不见心不烦。
一整条程序坞中间是有小小的竖线隔开的,鼠标放上去可以拖动更改宽度,系统偏好设置里有更多详细的设置,比如动画什么的,作者习惯将其调整为最小,并且固定放在副屏最右侧,这样不会占用主屏空间,而且可以很快找到活跃的程序。
作者使用 Windows时,通常将任务栏设置为最窄宽度并且固定于屏幕最上方,并且新建工具栏,使常用快捷方式居中放置,新打开的程序也会在里面显示,很是方便。
截图工具是必不可少的,Mac 自带的是 Shift + Command + 3/4/5,一只手实在是太难按了!
作者选择了微信自带的 Alt + A 截图,还有截图工具 jietu,并将其快捷键设置为 Ctrl + Alt + A,这样就很河里。
日常可能经常遇到需要粘贴之前几次复制的东西,而 Ctrl V 默认只保留第一个,这时候如果再去翻原来是从哪里复制的,就太慢了,而且说不定忘记了。
Windows 下使用 Windows + V 可以很合适的应对这种场景,Mac 下暂时没有发现如此轻量而不收费的剪贴板软件,目前作者用的是 PasteNow。
有时候需要在特定的位置新建 text、markdown、word等文件,在图形界面当中,鼠标右键无疑是最快的操作,无论是命令行创建,还是打开编辑器保存的时候再选择路径,都显得很繁琐。
Mac 原生没有此功能,可以使用软件 超级右键 ,配置右键时的新建选项。
Windows 的一个常用快捷键是 Windows + E, 可以打开文件管理器,这个是保持桌面整洁的秘诀之一。
也存在一些桌面管理软件,但作者认为最好的管理是没有文件和快捷方式需要管理,“我的桌面空无一物” :D
Mac 下可以使用 BetterAndBetter 将 Windows + E 设置为打开访达的快捷方式,这样可以快速的管理自己的文件。
日常使用,难免会有一些隐藏在边边角角的废弃文件需要清理,Mac 上的一个很好用的清理工具是 腾讯柠檬,清理之外能够配置菜单栏的小组件,可以显示网速或者内存、CPU占用率等 ,作者十分喜欢这个功能,可以勉强忍受网速慢,但不能忍受不知道当前网速的具体值。
Mac 的三指上滑触摸板可以激活调度中心,选择已经打开的软件,但是在使用鼠标的情况下,比起去 Dock 栏翻找,使用键盘按键显得更快捷一些。
Windows 上可以使用 Alt/Windows + Tab,Ctrl + Windows + B/D/方向键等快捷键自由的切换多任务与多桌面,再加上触摸板的配合,以及鼠标拖拽撞击屏幕边缘进行分屏。
这一点 Mac 差的有点远,尽管触摸板好用,但是单单一个触摸板,完全打不过对方不讲武德的,触摸板+鼠标+快捷键。
只好装一个 AltTab ,或者将左下角的触发角设置为调度中心,勉强维持切换软件这样子。
装了一些软件之后,Mac 顶部默认的菜单栏很容易就满满当当,作者不太喜欢使用自动隐藏的方式,有一些不必要常驻的已经删除了,可还是有很多。
这种情况可以使用一个叫做 Dozer 的菜单栏管理软件,按住 Command + 鼠标 就可以拖动小白点,隐藏任意的菜单栏图标,还可以配置快捷键 Ctrl + Command + B, 随时隐藏和展开。
有时候专注工作需要一些音乐配合,洛雪音乐助手自带国内几大平台音乐软件的源聚合,基本上各类歌曲都可以轻松搜到,还有一大堆歌单。
其他的 Apple Music、Spotify 好像也还行,不过都得和会员配合食用。
免不了接触各类压缩文件,MacZip 可以批量选择压缩包解压,勉强能用,压缩软件这块,Mac 上不如 Windows 上的多且好用,有个 跨平台的 BandZip 压缩软件,挺好用,但是太贵,Windows 一般用 7zip。
这类工具聚合类软件有老牌的 Alfred,可以打造花里胡哨的 workflow,但是没有 Windows 版,而且作者并不是很喜欢看上去过于万金油的软件,因为通用往往意味着专业特性不够强。
新一些的有跨平台的 uTools,各类小工具很好用,比如截完图配合 uTools 上的讯飞 OCR 插件,可以获得非常丝滑的体验。
关于操作系统以及各类软件之战,各大论坛可以说是日经帖,可以预见的是在之后很长的一段时间内,依然会争论不休,因为争论只是技术观冲突的外在表现,而技术观是世界观以及价值观的子集,所以永远会有矛盾。
单说操作系统和软件,这里给读者的建议是,一开始接触时不要带着既有技术观,保持空杯心态。
但是往往由于已接触了同类型而先入为主,这个很难完全做到,不过没关系,选择软件的时候,就根据自己的技术观,自由选择就行。
]]>不管黑猫白猫,能捉老鼠的就是好猫
我发现别人有些文章的关键图片挂掉了,非常影响体验,甚至直接导致接下来的步骤无法进行,虽然我对自己的OSS有长期维护的打算,但是以后谁知道会不会挂掉,所以我决定以后尽量少用图片,关键步骤一律采用文本的形式。
mkdir <folder>cd <folder>vagrant init <image-name>vagrant up
vagrant up 这个命令是去仓库拉取镜像然后安装,由于众所周知的网络原因,所以官方仓库根本根本访问不了,于是去查找box,下载之,手动安装,下面以安装centos 7 为例子
中科大源: http://mirrors.ustc.edu.cn/centos-cloud/centos/7/vagrant/x86_64/images/
小版本我选了2004_01,复制地址,然后
cd vagrant/centos7vagrant init centos/7vagrant box add \ http://mirrors.ustc.edu.cn/centos-cloud/centos/7/vagrant/x86_64/images/CentOS-7-x86_64-Vagrant-2004_01.VirtualBox.box \ --name centos/7vagrant up
PS A:\vagrant> vagrant upBringing machine 'default' up with 'virtualbox' provider...==> default: Importing base box 'centos/7'...==> default: Matching MAC address for NAT networking...==> default: Setting the name of the VM: vagrant_default_1608122873296_34651==> default: Clearing any previously set network interfaces...==> default: Preparing network interfaces based on configuration... default: Adapter 1: nat==> default: Forwarding ports... default: 22 (guest) => 2222 (host) (adapter 1)==> default: Booting VM...==> default: Waiting for machine to boot. This may take a few minutes... default: SSH address: 127.0.0.1:2222 default: SSH username: vagrant default: SSH auth method: private key==> default: Waiting for cleanup before exiting...
出现上面这个说明安装好了,可以愉快的玩耍了
再验证一下,打开终端,执行 vagrant ssh ,这个会将vagrant 作为 username 登录
接下来配置一下虚拟机的网络使之能和宿主机互通。
打开刚才执行 vagrant 命令的目录,我这里是A盘下面的 vagrant
然后,vagrant init 的时候会自动生成一个 Vagrantfile ,这个无后缀的文件
后缀其实只是用来给人类看的,或者方便软件关联用来显示logo,程序真正处理文件的时候,都是根据内容来判断的,如果你愿意的话,可以将.docx文件的后缀去掉,照样可以用 office 打开
将 Vagrantfile 用你喜欢的文本编辑器打开,找到这一行:
config.vm.network "private_network", ip: "192.168.56.10"
将前面的 # 号删除,表示解开注释,这个 ip 的值,保持和宿主机网段一致,然后虚拟机和宿主机互相 ping 两下,看看是否联通。
注意可能会出现虚拟机捕获鼠标的情况出现,此时摁一下右边的 Ctrl 即可。
点开Oracle VM VitualBox ,里面的 CentOS 默认登录名是 root, 登录密码是vagrant,不过我喜欢直接打开 windows terminal , 进入到虚拟机的目录,然后 vagrant ssh ,直接就登录成功了。
先把 CentOS 的源换清华源,参考这个 https://mirrors.tuna.tsinghua.edu.cn/help/centos/
执行sudo yum makecache 的时候是失败了,测试了下发现是 DNS 没有配置导致无法解析域名,所以编辑一下 ifcfg-eth1:
sudo vi /etc/sysconfig/network-scripts/ifcfg-eth1在最后加一行 DNS1=114.114.114.114 sudo service network restart
再次执行 sudo yum makecache ,成功。
接下进行 Docker 的安装,参考官方文档 https://docs.docker.com/engine/install/centos/
执行 sudo yum install -y yum-utils 时发现报错
GPG key retrieval failed: [Errno 14] curl#37 - "Couldn't open file /etc/pki/rpm-gpg/RPM-GPG-KEY-7"
ll 一下 /etc/pki/rpm/rpm-gpg 发现并没有 RPM-GPG-KEY-EPEL-7 这个文件,这个应该是用来验证安装包的公钥,然后我就开始绕了弯路,暂且按下不表,且说结果,我找到了这玩意:
于是复制链接,进入 /etc/pki/rpm/rpm-gpg 目录,wget 一下。
或者把刚才 .repo 里的 gpgkey 的路径换成 https://mirrors.tuna.tsinghua.edu.cn/centos/RPM-GPG-KEY-CentOS-7
也行。
然后继续 sudo yum install -y yum-utils
, Complete!
接下来继续按照 docker 的官方文档执行步骤。
安装完成之后,可以配置镜像加速,我用的是 阿里云 的这个
sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors": ["https://rg1da65z.mirror.aliyuncs.com"] } EOF sudo systemctl daemon-reload sudo systemctl restart docker
一行命令 sudo docker pull mysql:5.7
搞定。
运行 MySQL,同时指定目录和端口映射:
docker run -p 3306:3306 --name mysql \-v /mydata/mysql/log:/var/log/mysql \-v /mydata/mysql/data:/var/lib/mysql \-v /mydata/mysql/conf:/etc/mysql \-e MYSQL_ROOT_PASSWORD=root \-d mysql:5.7
同样一行命令 sudo docker pull redis
搞定。
运行 Redis:
# 1、2两步必须先执行,否则运行 redis 会报错 1. mkdir -p /mydata/redis/conf 2. touch /mydata/redis/conf/redis.conf 3. docker run -p 6379:6379 --name redis -v /mydata/redis/data:/data \ -v /mydata/redis/conf/redis.conf:/etc/redis/redis.conf \ -d redis redis-server /etc/redis/redis.conf
可以改一下 Redis 的配置 redis.conf , 在其中加一行 appendonly yes,开启持久化。
sudo docker update mysql --restart=alwayssudo docker update redis --restart=always
]]>
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例 1:
给定 nums = [3,2,2,3], val = 3,
函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。
你不需要考虑数组中超出新长度后面的元素。
示例 2:
给定 nums = [0,1,2,2,3,0,4,2], val = 2,
函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。
注意这五个元素可为任意顺序。
你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
解法1:
class Solution { public int removeElement(int[] nums, int val) { int i = 0; for(int j = 0; j < nums.length; j++){ if(nums[j] != val){ nums[i] = nums[j]; i++; } } return i; }}
两个指针,i 是慢指针,j 是快指针,遍历一遍数组,过程中快指针遇到的值如果和指定的 val 不相等,那么将其赋值给慢指针 i 。相等时则跳过,最后返回 i 指针,则相当于只保留了不与指定值相等的数组值。返回 i 的长度即可。
解法2:
class Solution { public int removeElement(int[] nums, int val) { int end = nums.length; int i = 0; while(i < end){ if(nums[i] == val){ nums[i] = nums[end-1]; end--; }else{ i++; } } return end; }}
这个解法比解法1更精妙,如果不是题目说顺序可变的话,就用不了了,直接遍历数组,过程中如果遇到某值和指定值相等,那么将数组末尾的值覆盖掉当前值。
然后数组长度--,否则,i ++,开始下一位的校验。
值得注意的是,如果数组末尾的值,正是想要移除的值呢?
这里的下标 i 并没有增加,所以当前 i 指向的值仍然会参加下一次校验。
实在是太妙了。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/remove-element
给定一个数组
nums
,编写一个函数将所有0
移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]输出: [1,3,12,0,0]
说明:
class Solution { public void moveZeroes(int[] nums) { //...省略判空 for (int p = 0, i = 0; i < nums.length; i++) { if (nums[i] != 0) { int temp = nums[i]; nums[i] = nums[p]; nums[p++] = temp; } } }}
不考虑数组为空或者长度为0为1的情况,多加一个if而已。
开始时两个指针都指向第1个元素,然后判断当前值是否为0,如果是0,则 p 指针维持原位置不动,i 指针向后移动一位,当 i 指针指向的元素不为0的时候,
则将其与p指针的下一个元素交换(此时 p 指针的下一个元素一定是0),同时 p 指针向后移动一位。
重复此过程直至到最后一位,
class Solution { public void moveZeroes(int[] nums) { //...省略判空 int index = 0; for(int num:nums){ if(num!=0){ nums[index++]=num; } } while(index<nums.length){ nums[index++] = 0; } }}
这个方法相当于把当前数组整个重新构造了一遍。
可以看作是从前往后数,有一个分界线,此分界之前全不为0,分界之后全为0。
遍历数组,只要当前数不为零,就将此值依次放到分界线之前的位置上。
放完之后,不管后面还剩几个位置,全部置为0即可。
]]>话不多说,开整。
String sentence = "Hello Java14";//假装sentence是个Object类型,可以当成参数传入 if(sentence instanceof String str){ System.out.println(str.length()); }else{ //.... }
控制台会输出 “Hello Java14” 这个字符串的长度 12。
需要注意的是,上述例子中,str的作用域仅限于if,如果对 if 括号里的表达式取反,则只有 else 中可以使用str
String sentence = "Hello Java14"; if(!(sentence instanceof String str)){ //这里不能使用str //System.out.println(str.length()); }else{ System.out.println(str); }
if(sentence instanceof String str && str.length() > 13){ //这里可以使用str System.out.println(str.length()); }else{ //这里不可以使用str System.out.println(str); }
剩下的 ||、&、| 我就不一一试了,只需要记住,sentence instanceof String str 这句本身就包含了一个bool值,而且只有为true的时候,才会强转sentence并且赋值给str , 所以在这个条件可能为假的情况下,当然不能在后面继续调用,这很符合逻辑。
使用一堆if 语句,我个人认为没有使用Swith来的简洁明了。这个可能会有争议,不过观点本来就会各异,可以不用,但是也得会,接下来康康Java14里的Switch是个什么样吧。
var score = "100"; var result = switch (score) { case "100","90" -> "A";//90和100分之间的,被无情抛弃 case "80" -> "B"; default -> throw new IllegalStateException("Unexpected value: " + score); }; System.out.println(result);
可以看到和原来相比,用箭头表达式代替了冒号,而且不需要 break ,并且switch 可以直接返回一个值,还是很方便的。
缺点是目前还不支持范围表达,没有某些语言 我忘记是哪个来着 的 .. 语法糖, 所以只能用 if 来了。
如果箭头右边的是语句块的话,类似这样,用 yield 赋值:
case 50 ->{ if(score > 50){ yield "C"; }else{ yield "D"; } }
String text = """ 第一行 第二行 太香了 什么,kotlin早就有了? 哦 """; for (String s : text.split("\n")) { System.out.println(s); }
再也不用烦人的 +"++"+ ,拼个短信模板,眼睛都要看瞎了,这个文本块和json、html、SQL语句简直是绝配,感觉干掉Mybatis的xml指日可待了。
class作为实体类的时候确实太麻烦了,无聊的toString 和 hashCode,冗长的get set , 还有无聊的构造方法,所以才出现了Lombok和Builder模式,出现这些,其实本身就意味着原来做的还不够好,好在有了 record! 它来了它来了
record Person(String name,Integer age){}
javap看一下编译之后长啥样 :
static final class Person extends java.lang.Record { private final java.lang.String name; private final java.lang.String age; public Person(java.lang.String name, java.lang.String age) { /* compiled code */ } public java.lang.String toString() { /* compiled code */ } public final int hashCode() { /* compiled code */ } public final boolean equals(java.lang.Object o) { /* compiled code */ } public java.lang.String name() { /* compiled code */ } public java.lang.String age() { /* compiled code */ } }
原来如此,编译器默默的在背后帮我们做了这么多事情,record 的特点是没有get、set 方法,提供了参数名作为调用,想使用时直接用person.name()、person.age() 就好了,需要注意的是:
万恶的NPE,以前的提示只有一个行号,如果是调用链的话,比如a.b.c()
, 根本不知道是a和b哪个出了问题,极大的锻炼了猜测能力,而现在只要运行的时候加一个 JVM 参数 -XX: +ShowCodeDetailsInExceptionMessages Npe
,就可以看到具体的提示了,这个太简单了,就不演示了。
jpackage 这个可以将 Java 程序打包成各平台的安装格式,比如MacOS的dmg、Linux的deb,rpm、Windows的msi和exe,不过好像局限性挺多的,不适合太复杂的程序,以后写个小服务啥的,直接打包运行,也不用羡慕Go了。
不过查了一会,也没查到这个打的 Hello World 安装包会有多大,而且会不会把 JRE 打包进去?
我看着繁琐的步骤,懒得去试了。
JEP 是个好东西,实在是追新利器,但是老生常谈的问题是,技术并不是越新越好,自己玩玩可以随便去玩,若想用于生产就得慎重考虑了。
]]>虽然我以前见过手动替换 war 包的,但是当这边原来的人员离职,项目交由我,居然教我手动 copy classes 文件和 JSP 文件的时候,我是崩溃的。。
0202 年了,为什么还会有人这么做?而且,他们竟然能忍个一两年都是一直手动,这点我还是有、服气的。
总结一下他们的操作步骤:
1、使用 Eclipse 本地点击 clean
2、至少2、3分钟,等待 classes 和 JSP 更新到对应存放的目录
3、远程一台 windows 2012 服务器、登上之后再远程另一台 windows 2012 服务器
4、将这两个文件夹里的文件 copy 到远程服务器对应的目录,完成替换
5、手动重启服务器上的 tomcat
整个步骤下来十几二十分钟就没了,遇到一些其他的情况,走个神啥的,部署一下半小时起步。
我无法忍受重复的机器人劳动,于是想了想有何解法,一开始想的是直接用个Jenkins 得了,然后顺便把版本管理工具换成 Git,我也尝试了把项目改换成 Spring Boot + Maven,但是折腾了一天,由于各种奇奇怪怪的问题,暂时失败了。
甚至激进的想过要不要给它重构掉,但是权衡了一波,考虑到种种原因,只好暂时放弃。
于是我申请要一个端口,说弄自动部署。。结果竟然不了了之,行,不给就不给,不给我也能想办法。
我的思路是,既然暂时没办法将编译源码的步骤放到服务器执行,那我就来想办法将他们手动执行的这些给自动化,分析一下这5个操作其实只做了两件事
1、本地代码编译
2、将编译之后的代码放到服务器,并使其运行
我的本地编译有热部署的插件帮我搞定,那么就只要保证到服务器上能直接获得到我本地的编译之后的文件就行了,于是事情变得简单:
将本地编译之后的代码上传到在线的代码仓库,然后在服务器上拉下来就好了。
其实按照规范来讲,这些操作都是要报备的,但是emmmm一言难尽,连测试服务器都不给。
考虑到网络原因,在线代码仓库我选了码云,建了个私有仓库,然后项目根目录底下建了个 git ignore文件:
README.mdREADME.en.mdconfig/lib/META-INF/tld/web.xmlclasses/*.propertiesclasses/*.xmlclasses/*.zipclasses/templatesclasses/ROOTclasses/com/package/common/util/Const.class.idea.settings
把乱七八糟的有些不能删,还有些暂时不敢删的的文件排除出去,使最后提交上去的只有 classes/ 和 pages/ 这两个文件夹里的东西。
然后服务器上建了个文件夹从码云把编译之后的文件拉下来,又写了个bat,replace-classes-pages.bat:
@echo offd:cd /project-name-resources\projectgit pull xcopy classes\com\package\name D:\web\WebRoot\WEB-INF\classes\com\package\name /s /y && xcopy pages D:\web\WebRoot\WEB-INF\pages /s /ynet stop tomcat ping -n 20 127.1 >nulnet start tomcat
这个写得很简陋,而且有改进的空间,写的时候也遇到了各种问题,比如重启 tomcat 一开始写的是net stop tomcat && net start tomcat
直接双击执行bat,一切执行正常,可是当我在程序里调用此bat的时候,只能关闭掉tomcat服务,死活不能给我启动。。巨坑,暂时没想到是为何。
但是还是很麻烦,我每次得在本地从 SVN 拉下来他们更新的代码,然后切换窗口使 IDEA 失去焦点触发热部署,接着到命令行执行 git push 那一套,再远程到服务器上双击bat。
由于远程服务器还不止一个组在用,所以连接还经常被人给挤掉。。。也不知是谁,我太难了。
呼~ 听起来就让人喘不过气来,不过那天晚上再搞就很太晚了,于是我就回去了,起码1.0 版本上了,再也不用我手动压缩文件夹,再拷贝过去解压替换了。
由于是老项目,还没办法重构,所以整个过程十分的曲折。
一开始我直接在项目里用 Java 写了个接口,接收到请求就本地调用 replace-classes-pages.bat,但是老是各种原因失败。我就拉着我们前端用Node起了个服务,然后用shelljs去调用 bat。
java 代码就非常的简单,但是好用:
@Controller@NoAuthpublic class CI { private final Logger logger = LoggerFactory.getLogger(CI.class); @RequestMapping("/ci") @ResponseBody public String ci(HttpServletRequest request) { String password = "this is a secret..."; String auth = request.getHeader("X-Gitee-Token"); if (StringUtils.isNotEmpty(auth)) { if (password.equals(auth)) { String result = HttpUtil.get("http://127.0.0.1:3000"); logger.info("请求ci,result:{}", result); return "okkkkk"; } } return "wtf!"; }}
js代码如下:
var _path = _interopRequireDefault(require("path"));var _fsExtra = _interopRequireDefault(require("fs-extra"));var _express = _interopRequireDefault(require("express"));var _shelljs = _interopRequireDefault(require("shelljs"));function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }const app = (0, _express.default)();app.get('/', (req, res) => { const common_resources_path = "D:/project-name-resources/project"; const common_target_path = "D:/web/WebRoot/WEB-INF"; const class_resources = _path.default.join(common_resources_path, 'classes/com/package/name'); const page_resources = _path.default.join(common_resources_path, 'pages'); const class_target = _path.default.join(common_target_path, 'classes/com/package/name'); const page_target = _path.default.join(common_target_path, 'pages'); _shelljs.default.cd(`d:/project-name-resources/project`).exec(`git pull`); copyDir(class_resources, class_target); copyDir(page_resources, page_target); _shelljs.default.exec(`net stop tomcat && net start tomcat`); res.status(200).json({ content: 'WOW Awesome!!!!!!' });});function copyDir(from, to) { _fsExtra.default.copy(from, to, err => { if (err) { console.log('An error occured while copying the folder.'); return console.error(err); } console.log('Copy completed!'); });}app.listen(3000, () => console.log(`Server running`));
这写得其实也是非常的简陋,然后 shelljs 也是调用 bat 会有问题,就是死活启动不了 tomcat,气得我不行,同时深感自己基础知识匮乏,心有余而力不足。
后又用 PM2 给 Node 项目起了个守护进程,防止挂掉。
于是我的部署流程就变成了,本地热更新代码之后,git push一下,那边就不用我操心了,码云的 Webhook 地址就填的我用 java 写的那个接口。
但这个方案还是存在问题,shelljs 去执行前半部分的流程都好好的,但重启不了 tomcat,出现过好几次,我这边 push 完代码,我就不管了,然后就有人找我说项目挂掉啦,咳咳。
我只好远程登上去,手动启动一下 tomcat 服务。
这不稳定的玩意,还得想办法。
然后我搜了一下如何用 Go 调用 bat 文件,写了这个:
package mainimport ( "fmt" "net/http""os/exec""time""log""os")func main() { http.HandleFunc("/", execBat) http.ListenAndServe(":3000", nil)}func execBat(w http.ResponseWriter, r *http.Request) { // shellPath := "C:/Users/cat/Desktop/test.bat" shellPath := "D:/xxx/replace-classes-pages.bat" command := exec.Command(shellPath) err := command.Start()if nil != err {fmt.Println(err)}fmt.Println("Process PID:", command.Process.Pid)err = command.Wait() if nil != err {fmt.Println(err)}fmt.Println("ProcessState PID:", command.ProcessState.Pid())nowDateTime := time.Now().Format("2006-01-02 15:04:05")fmt.Println("deploy sucess!! at ", nowDateTime)logPath := "D:/xxx/xx-ci.log" logFile,err := os.OpenFile(logPath,os.O_CREATE|os.O_WRONLY|os.O_APPEND, 7777) defer logFile.Close() if err != nil { log.Fatalln("open file error !") }debugLog := log.New(logFile,"[Debug]",log.Llongfile)debugLog.SetFlags(debugLog.Flags() | log.LstdFlags)debugLog.Println("deploy success! Save 10 minutes of your life!!")
这个 exec 执行 bat 文件比 shelljs 好用多了,完美的可以重启 tomcat,并且我加了一行简陋的日志,这样我就可以看到每次的部署记录了,其实 gitee 的WebHooks 后台也是有请求记录的,只是我 Java 写的那个服务没有好好给它返回值,所以得不到确切消息,这个也留待后续优化。
用 go build -ldflags "-H=windowsgui" example.go
生成一个 exe程序,设置成开机自启,再把tomcat服务设置成自启动,这样也不用怕腊鸡 windows 哪天莫名奇妙的自动重启了。
现在就舒服多了,我只要从 SVN 拉一下代码,等 Jrebel 热部署一下,然后 git commit -am "update comment" , git push。就搞定了,整个部署过程从20多分钟,变成了1分钟。
不过目前仍然存在很大问题:由于编译放在了本地做,所以只能我自己的这台主机 push,其他小伙伴们没法部署。这个之后还是得想办法把编译放在服务器上,然后通过大家 push 代码或者 PR 的时候触发 WebHooks。
目前这个项目还存在着太多太多问题:
可以说是非常难受了,而且最关键的是,做这个项目我毫无成就感,甚至想去转Go了,以后就可以再也不用碰这种无聊的业务项目了。
V2.0 方案使用的这段时间,虽然时有问题,但总归是能用的,然后某天,公司突然宣布断网。。开发人员想要上网只能通过一台公共的可上网的机器,各自登录mstsc 账号。。。
那我的把 Git 当作临时网盘使用的方案就泡汤了,因为本地的主机没办法直连外网,而且后来的一个多月派我到客户现场驻场,只能通过公司的给的 vpn 连入公司内网,然后用笔记本 mstsc 我在公司的主机,这又增加了复杂度。
后来发现 Tomcat 配置文件 D:\apache-tomcat-8.5.40\conf\server.xml
的 Host 属性 有两个子配置 :
<Host appBase="webapps" autoDeploy="true" name="localhost" unpackWARs="true">
autoDeploy 和 unpackWARs 见名知意。就是直接把 war 包丢到 tomcat 的 webapps 目录底下,就可以自动解压部署。于是我就想着,要想办法把这个项目打成 war 包!这样就不用每次都上传和下载这么多细碎的小文件了,只要一个 war 包就搞定。
可实际上,我忽略了一个很致命的问题,那就是 血坑的 D:\web\WebRoot\attachment
不知道之前的哪个牲口,将这个项目的所有上传文件!竟然全部!存放在项目根目录下!项目附件虽然目前还没多少数据,9个G,但是持续在增长之中,最关键的是,Tomcat 的 unpackWARs 自动解压 war 包的时候,会用此 war 包中的目录,直接将原来的项目底下的同名目录覆盖掉,这意味着根本就不能将 attachment 文件夹放入 war 包。
而且堂堂的附件文件,这样打包搬运来搬运去,也太愚蠢了,更何况小文件的复制非常慢,无论是从哪个角度,都不能这样干。
那么问题变成了,我要如何在打成 war 包的同时排除一些文件夹,于是我想到了中古神器 Ant。兴冲冲的下载配置并运行,结果啪的一下就报错了,无法顺利的打造 war 包,行,那就复古呗。Ant 的实现方式不就是 用 xml 配置文件的方式来描述打包过程的吗,既然你报错那就不用了,自己实现得了,而且目前只我一人部署,所以并不需要考虑那么多。
还需要考虑的一个问题是,开发环境的 配置文件和服务器上的配置文件存在不一致的情况,这个可以通过更改 applicationContext-common.xml
中PropertyPlaceholderConfigurer 的 locations 属性,将 jdbc.properties 等不一致的配置文件,重新指向到其他的目录,也就是说,不用项目里的这个配置文件了,用的是服务器上某个目录下的配置文件。
<!--引入外部文件 --> <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <list> <value>classpath:jdbc.properties</value><!-- <value>file:${catalina.home}/jdbc.properties</value>--> </list> </property> </bean>
开发机和生产服务器使用不同配置文件的问题解决了,接下来解决打包时要把不需要的文件排除出去的问题。
于是我回归了世界的本源——Terminal 。写了以下的一个脚本 pck.bat:
B:cd project/svn upchoice /t 3 /d y /n >nulexplorer B:\project\project-name\classes\artifacts\nwyychoice /t 60 /d y /n >nulcd /project/project-name/classes/artifacts/out/jar -cvf project.war WEB-INF js css fonts highcharts html images META-INF sjdr index.jsp
既然无法排除某文件夹,那么我就只把需要的文件和文件夹打包好了。
非常的简单粗暴,最大化利用了我目前掌握的所有信息和手上的所有资源,诶,甚至有点暴力美学的感jio是怎么肥事?
有个需要注意的地方是,如果有些已经更改的文件所在的文件夹,不在上述脚本打包的目录列表之内,很简单,加一行就完事了,同时 undeploy.bat
解压之后记得复制一下该文件夹到 tomcat 指定的运行目录。
解释一下这个脚本的执行过程,首先改变目录进入项目编译之后的 out 目录,这里有打包所需的 class、page 文件等,然后执行 svn up 将其他人的代码拉下来,接下来 explorer 打开打包之后 war 包的存放目录,开始等待xx秒,此时由于多出了个窗口,Jrebel 检测到失去焦点,触发自动更新 classes 和 resources,预估等待 xx 秒之后,执行 jar --cvf xxx.war [dir1 dir2]
命令,打包 war 包。
打完 war 包之后又是需要手动操作的部分了,Ctrl C 一下 war 包,到 172.16.1.11 ,双击 backup-war.bat
备份一下 上次的 war 包,自动打开 war 包存放目录,Ctrl V 粘贴。然后双击 deploy.bat
,就可以去倒杯茶喝喝了,等回来就发现部署完成了。
backup-war.bat
的内容如下:
@echo offexplorer D:\project-resources\project-warmove D:\project-resources\project-war\project-backup.war D:\project-resources\project-war\project-backup-backup.warmove D:\project-resources\project-war\project.war D:\project-resources\project-war\project-backup.war
简单的备份了之前两个版本,如果本版部署出现了问题,需要快速回滚,这时候将 project-backup.war 重命名为 project.war , 然后再次执行 undeploy.bat 即可,这个过程其实也可以用脚本做掉。
deploy.bat 用来和 pck.bat 搭配,好家伙,好好的一个 war 包,被我当成了压缩包来用,内容如下:
deploy.bat:
@echo offd:cd /project-resources\project-warjar -xvf nongwei.warxcopy WEB-INF\classes\com\dom D:\web\WebRoot\WEB-INF\classes\com\dom /s /r /y^ && xcopy attachment\excel D:\web\WebRoot\attachment\excel /s /r /y^ && xcopy css D:\web\WebRoot\css /s /r /y^ && xcopy js D:\web\WebRoot\js /s /r /y^ && xcopy html\yzt D:\web\WebRoot\html\yzt /s /r /y^ && xcopy WEB-INF\pages D:\web\WebRoot\WEB-INF\pages /s /r /ynet stop tomcat8 && net start tomcat8
就是无脑解压+复制+重启。
目前的方案基本就是这样,核心思想就是能用脚本做的事情,坚决不要用手工做。
拼命的还技术债,却怎么也还不完。折腾到最后也没折腾出一个全套的方案。这就是不做好技术选型的后果,如果当初立项考虑到了这些,后期根本不用这么折腾,差点就要 自己写一个 版本管理工具和项目部署工具了。我在做的是 Git 和 Ant、Maven、Gradle 被发明之前的事情,这不合理。比直接看人家源码学习效率低多了。而且从生产角度来说,怎么也应该把先进的生产工具拿来使用,提高生产力啊。
]]>IM (即时通信,Instant Messaging)在如今应用相当广泛:
天天吐槽某绿色聊天软件的消息同步是个垃圾,骂完之后我又想到,如果让我搞一个IM,我要如何保证多设备消息同步呢,为何它的群聊限制是500人,而企鹅是2000人,tg则是上万人呢 ?
撤回、已读是如何实现的呢,我如何保证消息精准无重复按顺序送达呢?
不谈什么业务考虑和风控,只是最基本的功能的话,技术上到底是如何搞呢,深感自己知识的匮乏,是时候来学习一波了。
接入服务是消息收发的出入口,主要负责保持连接、解析协议和 维护Session,业务处理服务则用处理各种乱七八糟的业务逻辑,一般会把这俩分开,计算机行业有一句经典名言叫做没有什么是再加一层不能解决的
将接入服务和业务处理服务处理服务解耦,是因为业务逻辑 可能 一定经常变动,而接入服务做的事情相对单一和稳定,而且可以搞业务的专心业务,搞技术的专心技术,提高效率和.......编码愉悦感?
存储服务用于各种数据的持久化,某APP: 我们 不 保证没有存你们的 破 聊天记录呢!
外部接口服务主要用于消息提醒之类的。
不知道用NoSQL或者ES存会不会在某些方面更高效,对这俩和IM还没理解到那个程度,还是先从熟悉的关系型数据库开始吧。
我们来看一个场景,小明给小红发了一条消息:俺喜欢你。
那么我们得有一张消息表,存储消息内容和消息类型,再加上Id和时间
message_id | type | content | time |
---|---|---|---|
000001 | 文本 | 俺喜欢你 | 2020-04-09 00:07:41 |
000008 | 图片 | 爱你.jpg | 2020-04-09 00:07:55 |
000099 | 文本 | 么么哒 | 2020-04-09 00:08:59 |
000111 | 音频 | 月亮代表我的心.mp3 | 2020-04-09 00:09:59 |
然后还得有一张表用来记录接收者和发送者,即消息表增加一条数据的时候,索引表增加两条数据,这两条分别记录了小红和小明,并标记他们是收方还是发送方。
存两条的原因主要是考虑各自索引维护时的独立性:比如一方删除了消息不影响另一方查看类似的需求。
Uid | other_uid | send_or_Receive | message_id |
---|---|---|---|
小明 | 小红 | 0 | 000001 |
小红 | 小明 | 1 | 000001 |
其实还有一些数据:
Uid | other_uid | send_or_Receive | message_id |
---|---|---|---|
小明 | 小花 | 0 | 000008 |
小明 | 小芳 | 0 | 000099 |
小花 | 小明 | 1 | 000008 |
小芳 | 小明 | 1 | 000099 |
小红 | 小王 | 0 | 000111 |
小王 | 小红 | 1 | 000111 |
一般都还有个对话框的界面,这里存储着最近联系人和最新的一条消息,这就需要一个联系人表:
Uid | other_uid | message_id |
---|---|---|
小明 | 小红 | 000001 |
小明 | 小花 | 000008 |
小明 | 小芳 | 000099 |
小红 | 小明 | 000001 |
小花 | 小明 | 000008 |
小芳 | 小明 | 000099 |
小红 | 小王 | 000111 |
小王 | 小红 | 000111 |
可以看出我们的小明同学是个脚踏三只船的海王,而小红同学爱着隔壁小王。
其实最近联系人这个表的数据我们可以直接写SQL从索引表中拿到,但是存数据一时爽,取数据火葬场,徒增SQL的复杂度。而且独立建一个联系人表还有一个潜在好处是之后可以直接把未读数这个字段放到这个表里。不过未读数一般不需要存表,直接扔redis里就完事了。
消息发送很简单,客户端调用一下服务端的send接口即可,但是消息接收稍微麻烦一点,因为从服务器的视角来看,需要区分客户端是否在线。
如果客户端在线,那么服务端和客户端之间有一个长连接,服务端便可通过此连接将消息下发,如果客户端不在线(APP被进程杀掉了、没信号)呢?
此时服务器看看能不能抢救一下,如果通过尝试重连还是不行、那么就会将消息提醒发给第三方服务,比如苹果的APN,咕果的GCM,或者国内一些手机厂商的消息推送通道,或者其他整合好的第三方服务,这些服务会通过系统级别的接口把消息提醒直接推送到用户的通知栏(倘若这个app没被ban的话)。
注意,通过第三方服务发送的只是消息提醒,真正的消息还是等到客户端上线了,与服务端建立起了可靠的长连接之后,才进行数据的传输。这就是有时候一点进聊天界面,疯狂刷出消息的原因。
小红给小明发消息了,此时小明正在和别的妹子聊天,等小明从聊天界面返回到对话框界面的时候,会发现和小红的对话框有个小红点,或者出现+1,此时小明的消息总未读也会+1,OR,夺命连环call 99+
小明点开和小红的对话框之后,小红点消失了,未读数-1,好感度-999
总结一下,发一条消息需要操作三张表
]]>参考资料
开头得配上一张图,我发现Inoreader默认只会抓取第一张图片作为配图
太长不看版:有些杂乱,没有好好分类,不过随便一个都可以很有用。
SQ4R是《心理学导论》这本书中开篇提到的学习心理学的方法,我觉得对其他大部分学科也是有效的,这本书我上传到OSS了, 点击这里 可以打开查看,文件有点大,打开有点慢,扫描版有点糊,可以将就着看。
《如何阅读一本书》也提到过类似的方法, 简单来说, 就是带着问题去读
费曼技巧这玩意几乎大家都听说过吧,如果你能把一个概念说得让一个外行都听得懂,那说明你是真的懂了,小黄鸭调试法有异曲同工之妙。
想起个成语,老妪能解,传说白居易作完诗就念给老太太听,如果听不懂就改到她们能听懂为止,不过乐天的文风确实简单明了,《琵琶行》我超爱的
这个技巧可以用在上面的SQ4R的Review里,不过好些东西确实需要前置知识,而且多用比喻来帮助理解的话,会有个致命的问题,比喻意味着近似,比喻越多,丢失的信息就越多,所以,怎么权衡倒也是个问题。
当你实在看不懂一个定义的时候,不要死磕,要么去找最简单粗暴的例子,哪怕是特例,起码可以让你知道,噢还有这么个回事儿,然后再去找更多的辅助材料,毕竟,能把东西深入浅出的讲个明白,所要求的段位也是挺高的。
或者先放着,如果以后持续学习这个科目的话,重要的东西一定会反复遇到的。
除了黑天鹅,好像还没有啥能逃脱二八法则的制裁。
一下子把这些玩意放在一起说,看起来挺麻烦的亚子,但是仔细那么一品,又发现若想搞事情,这些问题确实都是最基本的,只不过有的时候思维速度“太快”,直接跳到其中的某个步骤里去了。
当感到迷糊的时候,不妨重新梳理一下,兴许就会有新解法。
真诚建议还未读过这篇的小伙伴们看看,提问前必看,堪称经典。看过了也可再看一遍,常读常新。
对于伸手党,真的很想告诉他 STFW!,要不然让我帮你百度一下?
简单来说就是记笔记的时候要化被动为主动,和康奈尔笔记法配合食用更佳。与其当个复读机、PPT党、收藏党,不如好好抓住当时的机会好好过一遍脑子,怕记不住就用录音笔帮着记,及时的总结复习,这是最重要的。
这个来自好几十年前的方法在0202年仍简单好用,懒得话就直接照搬,体验个一两周好处坏处不言自明,我的体验是emmm没啥体验,因为我上学的时候就发现自己记了笔记从来不看。。所以日渐放弃记录笔记,不过现在这个博客就是我新的笔记。输出党超越一切!
PERA 很强,用来管理生活、学习、工作皆可,这也有个视频教程,我目前准备重度使用PERA,建了8个AREA,还有一些Project,至于Task, 你甚至可以直接将滴答清单的网页版拖到notion的一个page里面,这也许是notion敢说all in one 的底气之一吧。你也可以参照这个在notion里新建page来维护你的task, 可以更方便的和其他project实现联动。
Archive存放一些输出的文章,以后做视频或者画画啥的,作品可以都扔在里面,PERA的强大之处在于你可以将网盘或者本地磁盘文件,也用这种方式归类,全这样搞的话,你可以非常快速的找到你想要的东西。
不过由于我是个仓鼠症患者,网盘里乱的一比,本地磁盘也是存了一大堆书啊视频什么的没看,全整理完也得花上个一周说不定都不够,索性就由着它乱着吧。
我喜欢用everything,搜就完事儿了。运用“让营地比你刚来时更干净”规则,每次清理一点,新来的就扔到Inbox里及时归档,这样随着时间的推移,自然会越来越整洁的。
笔记我也有在用有道云,记录一些零零碎碎的东西。看了这篇之后被安利了杜威十进制编码,所以现在我的笔记目录清晰的很。不过还是有待熟悉,习惯了搜索之后,一切都想着要是有一个搜索框就好了。。找房间里的东西也时常想要Ctrl + F
这个博主让我体会到了知识被碾压的“快乐”,博士强者,恐怖如斯,他出的学习观系列视频对我启发很大,当时刚发现的时候整整沉迷了一天,全部刷完了,现在好多内容都忘记了,不过那种震撼的感觉仍在,而且一些理论我很肯定对我已经产生了潜移默化的影响。
不过全盘照搬不可取。择其善者而从之,其不善者而改之,面对任何人、任何话语、任何知识,都是这样。
不知不觉这篇写成了推荐帖。。其实有些大佬这些方法一个都不用,也并不妨碍他们成为大佬,但不可否认的是他们一定有自己的一套体系。
其实其他的方法还有很多,GTD、番茄、Don't beak the chain等等,以及他们的一些组合及变种,最终的目的都是提升自己的生产力!我也正在摸索当中。
慢慢搞吧,知识大厦不是一天建成的,技能树也不能一下长成参天巨木。
]]>不积跬步,无以至千里;不积小流,无以成江海。
Java中比较包装类型的变量值时,要使用.equals()方法。除非那几个值是在缓存范围之内的,可以直接用双等于。
Java中的Integer/ˈɪntɪdʒər /,和String,可以说是非常常见了,今天来看一看Integer的一部分源码。String留着下次写。
先从几行比较数值的代码讲起:
Integer a = 99; Integer b = 99; Integer c = 180; Integer d = 180; System.out.println(a == b); System.out.println(c == d);
这个打印出来的两个结果分别是 true;false
a,b,c,d 分别是4个Integer类型的对象,而 == 比较的是两个对象的地址是否相同,所以上述结果的意思是a和b竟然指向同一个对象??而c和d是不同的两个对象?
同样是Integer ,为何会有不一样的结果呢?
Java的几个包装类都有自己的.valueOf()方法来初始化变量,让我们点进Integer,看一看它的valueOf()
IntegerCache.low? high?
看来问题出在这玩意上了,点进去可以看到:
原来如此,Java中Integer类型变量的值,如果在-128和127之间,就会从一个已经缓存好了的数组中直接取。
而且这个high可以通过 -Djava.lang.Integer.IntegerCache.high = X
进行更改:
然后再次运行,我就得到了两个true。
也可以通过虚拟机参数 -XX:AutoBoxCacheMax = X
来修改这个high 。
为什么缓存这个呢,因为相较于其他的数值,这些数值太常用了,经常 status == 0?吧,经常int i = 0 吧?
]]>SGF2ZSB5b3Ugc2VlbiB0aGUgcG9zdCBvbiBvdXIgSW5zdGFncmFtIGFjY291bnQ/
得益于Utools的Base64解码插件,没费丝毫力气,就get到了解码之后的一句话:
Have you seen the post on our Instagram account?
Instagram? 这是要引流的节奏嘛,到了JetBrains的Ins主页看到了这个帖子
jetbrains
Welcome to the final Quest! You should start on the Kotlin Playground: https://jb.gg/kotlin_quest
P.S. If you don’t know about the #JetBrainsQuest, it’s not too late to find out.
这......talk is cheap ,show me your code?
y1s1, kotlin我不会写
但是我想起来第二弹里面有个凯撒加密之后的密文来着,和这个长得有点儿像,试试去:
猜对了!
We have been working 22/7 on the video for the first episode of the PhpStorm EAP. If we gave you a clue, it would be easy as pi.
然后反手就把刚才playgroud的代码补全了 把TODO()换成3
google一下这句话We have been working 22/7 on the video for the first episode of the PhpStorm
第一条搜索结果是油兔上的一个视频,点进去看看,提示说这个pi ? π ? 3.14 ?
拖进度条到 3 分 14秒处,视频中的代码编辑器里出现了这个:
点击之后来到了一个在线答题的页面, 答题时间总共1分钟,倒计时ing:
好些题目,错了就多就做几遍,耐心一点基本上都能查到答案。
Almost there! The last challenge is in the Tips of the Day of a specific IntelliJ IDEA Community version from our latest build page in Confluence, but… there is a catch. You have to know which version to look for. To find the build number, you need sight beyond sight:
. Not Everything Today Does All You Could Ask. Lessons Learned From Other Relevant Solutions, Possibly Even Another Kind Emerge. Risking Sometimes Being Liberal Or Generous Proves Ordinary Simple Tests Infinitely More Annoying. Get Examining Hidden Initial Designated Early Symbols. They Have Everything Needed, Except Xerox, To Completely Level Up Everything.
第二段的首字母连起来是一句话:
.net day call for speakers blog post image hides then ext clue
google一下,来到这个页面:
直接F12查看图片文件名:
you_are_looking_for_build_201-6303
按照第一段文字的提示去IDEA的Confluence,
搜索201.6303,是它是它就是它 !
下载安装,一气呵成,非常之奶思!
这一步把我卡的死死的。。
放弃了,挺服气的,直接跳过了,这个步骤是获得一道题目,让算第 50000000 个斐波那契数的前四位和后四位,答案是46023125
]]>在第一弹提交之后,收到的邮件的末尾,写了第二弹游戏开始的时间:
果然准时发布了,let's go !
参考了这个帖子以及以下的一些回复,以及这个Gist,感谢大佬们分享的思路。
Time for the next #JetBrainsQuest!.spleh A+lrtC/dmC .thgis fo tuo si ti semitemos ,etihw si txet nehw sa drah kooL .tseretni wohs dluohs uoy ecalp a si ,dessecorp si xat hctuD erehw esac ehT .sedih tseuq fo txen eht erehw si ,deificeps era segaugnal cificeps-niamod tcudorp ehT
这一串文字,一看熟悉的A + lrtC
,经常 Ctrl +A ,Shift +Delete的同学可能就看出来了,这段文字是经过reverse的,用Java 一行 代码就可以搞定
text = new StringBuffer(text).reverse().toString();
于是得到了:
The product domain-specific languages are specified, is where the next of quest hides. The case where Dutch tax is processed, is a place you should show interest. Look hard as when text is white, sometimes it is out of sight. Cmd/Ctrl+A helps.
如果你直接copy第一句话来google一下的话,大概率会和我一样,来到这个页面:
不过这是不对的,我在这个页面逛了好一会。。啥玩意也没有找到,其实应该去的是MPS的产品页
直接搜索 Dech tex 定位到的地方,点进去会看到一个PDF:
乍一看平平无奇,但是还记得推文说的那句吗sometimes it is out of sight. Cmd/Ctrl+A helps
好的那就Ctrl + A全部选中试一下,有些网站 好孩子看不见 。
这块地方有点不对劲,让我们复制到别的地方粘贴一下康康:
This is our 20th year as a company, we have shared numbers in our JetBrains Annual report, sharing the section with 18,650 numbers will progress your quest.
ok ,进度条向前走了一丢丢,距离成功还有一大步!
这句话的提到了JetBrains Annual report,看看去
找到了距今最新发布的JetBrains 2019 Annual, 不过提到的18650是啥意思?
直接搜索18650:
结果是0
这咋整,删掉几个数字试试,扩大范围。搜18试试(这一步是我知道了答案才想到的,一开始没有想到)
其实这些数字加起来,正好是18650,看到那个转发按钮了吗,点它!
竟然藏在分享的编辑栏里,这波可以啊,复制出来看看说的啥:
I have found the JetBrains Quest! Sometimes you just need to look closely at the Haskell language, Hello,World! in the hackathon lego brainstorms project https://blog.jetbrains.com/blog/2019/11/22/jetbrains-7th-annual-hackathon/ #JetBrainsQuest
给了个博客链接,点进去,直接搜索brainstorms
有一幅图,莫非这图里有什么玄机,翻来覆去看了好几遍。。一无所获,遂看提示,原来藏在图片的alt里
d1D j00 kN0w J378r41n2 12 4lW4Y2 H1R1N9? ch3CK 0u7 73h K4r33r2 P493 4nD 533 1f 7H3r3 12 4 J08 F0r J00 0R 4 KW357 cH4LL3n93 70 90 fUr7h3r @ l3457.
这玩意又给我卡住了,觉得像是什么加密的字符,搜索了一圈也搜索不到,一眼看上去压根毫无规律可言嘛。
继续看了gist提示,才知道这是嘤文的火星文,≮о我,厵莱昰這樣孓≯ 。
翻译过来就是
Did you know Jetbrains is always hiring? Check out the kareers(careers) page and see if there is a job for you or for kwest(quest) challenge to go further at least.
我:"你是怎么认识火星文的"
大佬:"就...脑补啊"
666666
在大佬的提示下,我才知道原来获得邀请码的邮件里也藏着个彩蛋:
Y0U H4V3 R3C13V3D 7H15 3M41L B3C4U53 Y0U H4V3 P4RT1C1P4T3D 1N J3TBR41N5 QU35T
You have recieved this email because you have participate in jetbrains quest
动手翻译了一下,还是hin有意思的,而且得到了一个字典,先给自己挖个坑。
扯远了,继续解题。
Jetbrains is always hiring?
去职位发布页面看看, 注意一下careers这个词,就不会迷了路
在页面上搜索quest:
点进去看看:
i'm felling lucky !
看一下啥是cheat at Konami games:
上上下下左右左右BA,原来它还有个专业的名字,叫做Konami Code !
魂斗罗30条命!我来辽!!
上上下下左右左右BA
wow, awesome!
这是我独享的moment !
]]>昨天在V站看到个帖子,题为「JetBrains Quest,解开可获得全家桶三个月免费订阅」,当时没太在意,今天又看到了,突然来了兴致,遂试了一试,参考了帖子底下的一些回复以及这篇文章。
文章的作者由于帖子访问量过高,怕直接全部给出答案会让活动失去了乐趣,于是隐去了一段,不过给了个小tip, 着实让我思考了一会儿,最后灵光一闪,解了出来。
Due to high traffic on this page, I’ve decided to remove the rest of the blog post since I don’t know if JetBrains is ok with this. Also I don’t want to spoil the fun! I will bring back the solution once the quest is over.
JetBrains官方发了一条推:
48 61 76 65 20 79 6f 75 20 73 65 65 6e 20 74 68 65 20 73 6f 75 72 63 65 20 63 6f 64 65 20 6f 66 20 74 68 65 20 4a 65 74 42 72 61 69 6e 73 20 77 65 62 73 69 74 65 3f
这个玩意打眼一瞧,很像是16进制,直接google了一下,
得到了text:
Have you seen the source code of the JetBrains website?
接下来打开 JetBrains的官网,Ctrl + U 或者鼠标右键, 打开查看页面源代码,
Ctrl + F 搜索 Quest,看到了说明,意思是让去官方的产品页面,找一个Joke,同时给了一个线索:Good luck! == Jrrg#oxfn$
, 看不出来这是干嘛的,暂且记下,待会儿应该会有用。
接下来按照指示,去往产品页面, 说明文字提示这个页面需要在chrome的无痕模式下打开,其实这是个Joke,普通模式也可以 ,打开之后你将看到这样一个页面:
找到了,就是这个JK!
点击learn more,弹出了一个页面:
给了个短网址:https://jb.gg/###
###是省略了的参数,答案是500 到 5000之中一共有多少个质数,哈哈哈我也很喜欢质数。
然后继续google一下,找到了一个网站,可以列出1到10000以内的质数
接下来就好办了,一个简单的减法得到答案是574
拼到刚才获得到的那个短网址上:https://jb.gg/574
打开之后来到了PyCharm的说明书,里面藏了个彩蛋:
台球桌上的那张卡片写着 MPS-31816,还画着一个logo,那个logo看起来也是JetBrains的一个产品,直接搜一下看看:
点进去康康:
看到这样一句话:
“The key is to think back to the beginning.” – The JetBrains Quest team
beginning? 等等,一开始我们是不是有个线索还没用到来着,就是这个:Good luck! == Jrrg#oxfn$
然后又一串挺长的鬼画符:
Qlfh$#Li#|rx#duh#uhdglqj#wklv#|rx#pxvw#kdyh#zrunhg#rxw#krz#wr#ghfu|sw#lw1#Wklv#lv#rxu#lvvxh#wudfnhu#ghvljqhg#iru#djloh#whdpv1#Lw#lv#iuhh#iru#xs#wr#6#xvhuv#lq#Forxg#dqg#iru#43#xvhuv#lq#Vwdqgdorqh/#vr#li#|rx#zdqw#wr#jlyh#lw#d#jr#lq#|rxu#whdp#wkhq#zh#wrwdoo|#uhfrpphqg#lw1#|rx#kdyh#ilqlvkhg#wkh#iluvw#Txhvw/#qrz#lw“v#wlph#wr#uhghhp#|rxu#iluvw#sul}h1#Wkh#frgh#iru#wkh#iluvw#txhvw#lv#‟WkhGulyhWrGhyhors†1#Jr#wr#wkh#Txhvw#Sdjh#dqg#xvh#wkh#frgh#wr#fodlp#|rxu#sul}h1#kwwsv=22zzz1mhweudlqv1frp2surpr2txhvw2
我看到这篇文章说提示是Caesar cipher
打开看看:
一股洪荒的气息涌来,直接把上面那串“乱码”copy过来试试:
看到最后一行的时候,我脑子里自动把2
替换成了/
,这很明显是个网址嘛!
于是手动解码成 https://www.jetbrains.com/promo/quest/
哈哈哈,我成功了!这个Code很显然就是那个什么Good Luck后面的 Jrrg#oxfn$
这一串嘛!
于是:
竟然是Wrong answer!这我就很懵了。。
我觉得我好像是漏了点什么,于是又回到那个解凯撒密码的网站仔细琢磨了一下
刚才解码得到好长一串,我只是猜了最后的网址,还有很多没有用到,里面会不会隐藏一些线索呢?
于是我又仔细看了一遍:
```Nice$#If#|ou#are#reading#this#|ou#must#have#worked#out#how#to#decr|pt#it1#This#is#our#issue#tracker#designed#for#agile#teams1#It#is#free#for#up#to#6#users#in#Cloud#and#for#43#users#in#Standalone/#so#if#|ou#want#to#give#it#a#go#in#|our#team#then#we#totall|#recommend#it1#|ou#have#finished#the#first#Quest/#now#it“s#time#to#redeem#|our#first#pri}e1#The#code#for#the#first#quest#is#‟TheDriveToDevelop†1#Go#to#the#Quest#Page#and#use#the#code#to#claim#|our#pri}e1#https=22www1jetbrains1com2promo2quest2```
这些#
太碍眼了,得想办法去掉,我又想起了那串密文,Jrrg#oxfn$
解出来 Good#luck$, 但是原文是Good luck!, 说明加密规则肯定不是这个Shift by 3
所以问题就变成了寻找这个加密规则。
然后我又在DECRYPT CAESAR CODE 这个按钮底下看到了 ROT Ciper
规则原来是 ASCII+3,把刚才那个密文解了看看,啊,顿时感觉英语单词是如此的顺眼!
但是这个网站有解密的字符长度限制,显示的解密结果不全,想来是为了节省算力吧,没关系,记下前半句,接下来把密文随便从中间去掉一半,后半段再次解密
过程我就不放图了,重复了几下,凑出了一小段语义完整的话,中间的重点是这句:
The code for the first quest is TheDriveToDevelop.
找到就是你! TheDriveToDevelop
很愉快的提交啦,然而不幸的是,我并么有收到jetBrains发来的邮件。。我也不知道为啥,不过过程还是挺有意思的。
]]>最近准备学习一波netty ,但是自己对Java的NIO、BIO、AIO还不熟悉,于是先来熟悉熟悉。
先来康康文档: All About Sockets
这文档太啰嗦了。。一时竟不知该从何处下手,随便找个 视频教程,还是个妹子讲的课,这我可就不困了啊,照着就手敲了一遍。
什么@Test @Sl4j 都给我边儿去,main方法和sout才是王道
话不多说,直接上代码和运行结果。
服务端代码:
public static void main(String[] args) { final int PORT = 8899; ServerSocket serverSocket = null; BufferedWriter writer = null; try { serverSocket = new ServerSocket(PORT); System.out.println("服务器已启动!正在监听端口"+PORT); while (true){ Socket socket = serverSocket.accept(); System.out.println("客户端 "+socket.getPort()+" 已连接"); BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); String message = reader.readLine(); if (message != null){ System.out.println("收到一条来自客户端 "+socket.getPort()+" 发送的消息:"+message); } //加了\n readLine才能生效 writer.write("你才是" + message + "\n"); writer.flush(); //清理缓冲区 } } catch (IOException e) { e.printStackTrace(); } finally { if(writer != null){ try { writer.close(); System.out.println("服务器挂掉了。。再见"); } catch (IOException e) { e.printStackTrace(); } } } }
客户端代码:
public static void main(String[] args) { final String Host = "127.0.0.1"; final int Port = 8899; BufferedWriter writer = null; try { Socket socket = new Socket(Host,Port); BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); BufferedReader console = new BufferedReader(new InputStreamReader(System.in)); String message = console.readLine(); writer.write(message+"\n"); writer.flush(); String responseMsg = reader.readLine(); System.out.println("收到了来自服务器的回复:" + responseMsg); } catch (IOException e) { e.printStackTrace(); } finally { if(writer != null){ try { writer.close(); System.out.println("客户端关闭了连接"); } catch (IOException e) { e.printStackTrace(); } } } }
先运行Server代码,此时服务成功启动!:
再运行客户端代码,此时服务端和客户端连接成功!:
在客户端发送消息:
这个教程是一个多线程的版本,其实就是把服务端的代码中可以复用的部分抽出来单独作为一个内部类,继承Thread,然后在while循环里,每当有一个客户端前来访问,都将开启一个线程专门来处理这个客户端的请求
实际上,上面的极简版本代码,完全也可以多复制几个Client, 然后同时运行,Server端代码会一个个的按照顺序收到客户端的请求并返回对应的结果,while循环永不疲倦,除非断电
这便是传说的BIO了,上手了一下,感觉简单的一比,至于底层的原理,无非就是TCP/IP那一套(虽然我也还不太熟悉。。)
接下来会慢慢深入。
typora的PicGo插件不知为何一直失败,上传图片的过程变得不再丝滑了,到处都找不到原因,有些影响心情。。
生活就是这样,永远也不知道下一块巧克力是苦是甜。
]]>不说别人,只说我自己,从第一次知道leetcode距今已有三年了。。然而一直停留在两数之和。
当我遇到一个未知的事情,第一反应是:我怎么这么晚才知道!我应该早点知道的。
我永远嫌弃自己知道的太晚。这是病,得治。
任何事情,当你知道的那一刻,那就是最早的时候。
说来也挺可笑的,我当初还嫌弃力扣是中文版的,逼格不够高,心里想着,等我过两天,就从 leetcode刷起,然而这么久了,没有一个平台能坚持超过3道题的,后来知道了 TC和 CF,走马观花的逛了一下之后,给我整自闭了——这些都是人能解出来的题目吗?
虽然我早就知道自己不是什么天才,就一普通人。但是真的好不甘心啊。
好了,不比比了,话锋一转,说说 GROUP BY
我一度不知道,GROUP BY 的列和SELECT后面的列之间的关系到底是怎样的,今天终于搞明白个123,特此记录下来。
由于我懒得再跑去服务器装个数据库了,就随便找了个 在线练习SQL的网站
界面简陋洁优雅,拥有一个表,名叫world,首先,按照惯例:
😆
不过我随后又发现,这个表的name没有重复的。。所以不适合当作本次的示例,果断跑路,还是借用题目所给的那张表吧,直接写云SQL就完事了,题目如下:
player_id | event_date | device_id |
---|---|---|
1 | 2016-03-01 | 2 |
1 | 2016-05-02 | 2 |
2 | 2017-06-25 | 3 |
3 | 2016-03-02 | 1 |
3 | 2018-07-03 | 4 |
首先我们要搞到某用户某天最早登录的记录,也就是首次登录记录,由“最早”二字可以想到用MIN函数 MIN(event_date)
于是就有了SQL:
SELECT player_id, MIN(event_date) FROM Activity
然而这样会得到如下结果:
player_id | event_date |
---|---|
1 | 2016-03-01 |
1 | 2016-03-01 |
2 | 2016-03-01 |
3 | 2016-03-01 |
3 | 2016-03-01 |
这显然不是我们所想要的,我们观察此表可以发现,表中按照player_id可以分为三组,分别是1,2,3,代表着三个用户。
SQL为我们贴心准备的GROUP BY ,就是用来干这件事情的。
所以上面的SQL可以改成:
SELECT player_id, MIN(event_date) FROM Activity GROUP BY player_id
执行后的结果:
player_id | event_date |
---|---|
1 | 2016-03-01 |
2 | 2017-06-25 |
3 | 2016-03-02 |
yes! it work !
还是上面的例子
你还可以写成SELECT player_id, MIN(event_date) AS event_date FROM Activity GROUP BY player_id,event_date
,可以得到同样的结果
含义就由刚才1,2,3,这三个组,变成了
每个组仍然是唯一的。
GROUP BY 的列和SELECT后面的列之间有个约束,关于解释我找到了这篇文章。
cname 只是每个学生的属性,并不是小组的属性,而 GROUP BY 又是聚合操作,操作的对象就是由多个学生组成的小组,因此,小组的属性只能是平均或者总和等统计性质的属性,询问每个学生的 cname 是可以的,但是询问由多个学生组成的小组的 cname 就没有意义了。对于小组来说,只有 "一共多少学生" 或者 "最大学号是多少?" 这样的问法才是有意义的。
接下来继续解题,有了用户们各自的首次登录记录,答案呼之欲出
解法1:
SELECT device_id,player_id FROM Activity WHERE( player_id, event_date)IN( SELECT player_id,MIN(event_date) FROM Activity GROUP BY player_id)
解法2:
SELECT a.device_id, a.player_idFROMActivity aJOIN(SELECT player_id,MIN(event_date) lastest_date FROM Activity GROUP BY player_id) tON t.player_id = a.player_id AND t.lastest_date = a.event_date
我还看到有人用了下面这种解法,是真的秀,我看了挺久才想明白。
SELECTplayer_id,device_idFROM(SELECTac.*,if(@player=player_id,@row_num:=@row_num+1,@row_num:=1) row_num,@player:=player_idFROMActivity ac,(SELECT @row_num := 0,@player:='') border by player_id,event_date) awhere a.row_num =1作者:quan-jia-an-kang链接:https://leetcode-cn.com/problems/game-play-analysis-ii/solution/tong-ji-de-xie-fa-by-quan-jia-an-kang/来源:力扣(LeetCode)
意思就是把整个表按照 player_id 和 event_date 倒序:
player_id | event_date | device_id |
---|---|---|
1 | 2020-03-05 | 001 |
1 | 2020-03-04 | 001 |
1 | 2020-03-03 | 003 |
2 | 2020-03-03 | 099 |
2 | 2020-03-01 | 150 |
3 | 2020-03-03 | 233 |
这样相当于分成了三组,分别是
各自的组内也是有序的,按时间倒序,所以把每一组的第一个(用row_num=1做标记)给收集起来,就是我们想要的结果集。
以小见大,我对SQL的理解目前仅仅停留在皮毛,连一遍 SQL标准都没认真读过,甚至连天天在用的MySQL的 doc也没读完。。更遑论源码
幸好最近在补数据库系统原理,心里稍微舒服了点。
简单列一个重新学习数据库的todo list吧
这篇文章并不是单纯的题解,更多的是对自己现有知识的总结,以及认识到自己的不足,写完了自己看一遍我都觉得这篇文章写的都是啥玩意。。
不过我现在不能精益求精,否则太容易打击自信了,之前就是一直觉得自己啥都不会,写的东西太腊鸡,才一直不敢写博客。
其实,不论是新手还是老手,都应该写博客。
大胆的写,干就完了!
]]>好用的笔记软件太多,求笔记软件推荐也算是V站的日经贴了。
我自己的历程是
手机自带便签---> 为知笔记---> 有道云笔记---> OneNote---> Notion
目前有道云笔记是主力,正在逐步使用notion和OneNote,三星笔记和小米便签也在用,这几个各自的优缺点就先不提了,等我完全摸索出一套自己的知识管理体系,再另开一篇。
本篇的由来还是要感谢一下有道云笔记,因为它的Markdown编辑器实在是太难用了!非会员不能直接粘贴图片这太不友好了,还经常出现键盘输入毫无反应的问题,我曾一度怀疑是不是我的键盘坏掉了。
俗话说工欲善其事必先利其器,虽然我经常是把各种东西准备好之后还是不想开始干活。。但是也不能让工具成为我写作之路的阻碍。
以前有看到过别人推荐Typora,当时也下载试着用了一下,然而后来就和VS Code一起沦为了我的md文件打开器,今天又看见少数派的一篇 Typora 完全使用详解 ,惊觉这货竟然如此的强,而且界面始终如此简洁好看,于是连下载带安装没花超过三分钟。
由于我比较喜欢写图文结合的文章,所以必须要解决的一个问题是——用哪种图床方案,免费的几个总觉得心里不踏实,而且很容易受到人家平台的策略影响,所以没经过太多纠结,我还是选择用OSS来当作我自己的图床。几家大厂的收费都差不多,而且我这样的互联网小透明,也花不了多少流量,所以还是很划算的,而且放心。
由于Typora的上传图片设置里默认支持的是PicGo,我就去了解了下,意外的好用,其实 uTools里面有老哥开发的图床插件也是很好使的,但是没法和Typora无缝结合。
截完图直接粘贴,点击一下上传,或者用PicGo的快捷键后台上传,然后直接粘贴链接,操作完全没有迟滞,如丝绸般顺滑,i了。
不过这里我遇到个坑,如果用的是windows系统的话,奉劝各位还是尽量不要把软件装在和系统同一个盘符,权限问题太坑了!在此悼念我因此而失去的半个小时。
文件同步可以用坚果云等各种云盘,挑你喜欢的就好,我准备做三个同步,有道云一份,OneDrive一份,github我的博客仓库再来一份,怎么备份都不嫌多~
有了这么友好的编辑体验,以后我肯定会爱上写东西的 !
]]>var dino = Runner.prototype//让小恐龙可以永生dino.gameOver = () => {}//让小恐龙以光速奔跑Runner.instance_.setSpeed(299792458)//让小恐龙飞上天,和太阳肩并肩Runner.instance_.tRex.setJumpVelocity(1000000)
以下争取写的让不会代码的同学也能够看得懂
打开chrome, Ctrl + T 新建标签页,Alt + D或者Ctrl + L 定位到地址栏,输入 chrome://dino
摁一下空格,即可开始游戏。
越往后面速度会越快,当屡次失败,觉得明明可以跳过去,但是死活过不去的时候,不要生气,不要灰心,让我们来hack it
摁一下键盘上的F12,你将会看到这个
如果不是在console,那么点击一下箭头所指的位置,绿色框框就是即将输入代码的地方
接下来复制这两行或者手动输入,然后点击回车
var dino = Runner.prototypedino.gameOver = () => {}
接下来小恐龙就可以不死不灭啦
Runner.instance_.setSpeed(299792458)
这一行可以让小恐龙跑的贼快
Runner.instance_.tRex.setJumpVelocity(1000000)
这一行可以让小恐龙跳的贼高
数值可以任意修改,负数也可以,祝玩耍愉快~
ps:小恐龙的图片也可以换成别的,这里抛砖引玉,更多新奇玩法,等你发现。
]]>这里顺便写一下安装过程吧,环境是win10专业版1909,其他版本差别不大的话应该也没啥问题,而且原理都是一样的,就是下载压缩包,然后放入某个路径,再将这个路径配置到环境变量里,就ok了。
环境变量的作用,就是让你在命令行输入命令的时候,系统知道去哪里找这个命令。
安装过程分为三步:
下载zip,由于官网直接下载巨无比爆炸慢,这里给一份我放在onedrive的安装包,下载完你可以用sha256校验一下,看看是不是原文件。具体怎么校验这里就不写了。
openjdk-13.0.1_windows-x64_bin.zip
我手动改了文件夹的名字,因为原来的名字太长了,然后将这个路径复制一下,接下来摁一下windows键,搜索"环境变量",一般只要输入前两个字就出来了
鼠标聚焦在图中绿色框里,摁一下P,找到Path,双击进入编辑,新建一条将刚才复制的路径写进去,然后点击确定
上图中version出现1.8.0_18是因为这篇文章后半段换了公司电脑写的 /doge
如果失败了,多半是你的环境变量没搞对,不必害怕,多花点时间研究一下怎么肥事吧
]]> public static Integer getAndRemoveBottom(Stack<Integer> stack) { /** *如果stack是空的话,stack.pop()会NPE,这里没有判断的原因是下一个函数调用本函数时,可以确保stack不为空 **/ Integer bottomItem = stack.pop(); if (stack.isEmpty()) { return bottomItem; } else { Integer last = getAndRemoveBottom(stack); stack.push(bottomItem); return last; } }
主函数,每次获得stack的最底部元素,并将其入栈,执行到最后即得到一个逆序的stack
public static void reverse(Stack<Integer> stack){ if(stack.isEmpty()){ return; } Integer bottom = getAndRemoveBottom(stack); reverse(stack); stack.push(bottom); }
接下来是喜闻乐见的测试环节
public static void main(String[] args) { Stack<Integer> s = new Stack<>(); s.push(1); s.push(2); s.push(99); reverse(s); while (!s.isEmpty()){ System.out.println(s.pop()); } }
输出结果
1299
灵魂画手上线,来一波流程图讲解一下详细流程
本来想画图详细分析一波的,但是发现完全讲清楚递归,太难画了。。所以果断鸽了,本文适合有已经知道递归概念的同学,至于咋知道的,就emmmm
]]>欲学多线程,先学JMM。
JMM(Java Memory Model),即Java内存模型,是一个逻辑概念,并不是说物理内存里就是这样的结构,其实内存中的物理地址本身对于操作系统也是一个逻辑地址。
JVM用来存储加载的类信息、常量、静态变量、编译后的代码等数据
Java启动时创建此内存空间,用于存放类的实例,即对象,满了就会OOM(OutMemoryError),垃圾回收主要管理的就是堆内存。
每个线程都有自己的一个虚拟机栈,线程栈由多个栈帧组成,线程中执行的每个方法对应着各自的一个栈帧。栈帧内容包括局部变量表、操作数栈、动态链接、方法返回地址、附加信息等,栈内存默认最大1M,超出则StackOverFlow
用于执行一些native方法,调用操作系统的一些功能等,比如文件读写的底层涉及到文件系统,肯定要经过操作系统的,还比如jvm初始化时候执行的一些C++代码。虚拟机规范并没有规定具体实现,所以不同的虚拟机实现方式不同。
Program Counter Register,记录当前线程执行的字节码的位置,存储的时字节码的指令地址,执行native方法时PC里的值为空,相当于CPU切换执行线程时用于记录的小本本。
public class test{ public static void main(String[] args){ int x = 500; int y = 100; int a = x / y; int b = 50; System.out.println(a + b); }}
这段代码编译之后会变成什么鬼样子呢,来康康。
第一步先将java编译成class,因为.class是给jvm看的,人类并不能看懂,所以第二步将人类能够看懂的信息保存至test.txt
javac test.javajavap -v test.class > test.txt
打开test.txt,wow,awesome,这是记事本独享的moment这里直接贴出来吧
Classfile /C:/Users/rice/Desktop/test.class Last modified 2019-10-15; size 412 bytes MD5 checksum 10535501b08a5c0e05319708975b247d Compiled from "test.java"public class test minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPERConstant pool: #1 = Methodref #5.#14 // java/lang/Object."<init>":()V #2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream; #3 = Methodref #17.#18 // java/io/PrintStream.println:(I)V #4 = Class #19 // test #5 = Class #20 // java/lang/Object #6 = Utf8 <init> #7 = Utf8 ()V #8 = Utf8 Code #9 = Utf8 LineNumberTable #10 = Utf8 main #11 = Utf8 ([Ljava/lang/String;)V #12 = Utf8 SourceFile #13 = Utf8 test.java #14 = NameAndType #6:#7 // "<init>":()V #15 = Class #21 // java/lang/System #16 = NameAndType #22:#23 // out:Ljava/io/PrintStream; #17 = Class #24 // java/io/PrintStream #18 = NameAndType #25:#26 // println:(I)V #19 = Utf8 test #20 = Utf8 java/lang/Object #21 = Utf8 java/lang/System #22 = Utf8 out #23 = Utf8 Ljava/io/PrintStream; #24 = Utf8 java/io/PrintStream #25 = Utf8 println #26 = Utf8 (I)V{ public test(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 1: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=5, args_size=1 0: sipush 500 3: istore_1 4: bipush 100 6: istore_2 7: iload_1 8: iload_2 9: idiv 10: istore_3 11: bipush 50 13: istore 4 15: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 18: iload_3 19: iload 4 21: iadd 22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 25: return LineNumberTable: line 3: 0 line 4: 4 line 5: 7 line 6: 11 line 7: 15 line 8: 25}SourceFile: "test.java"
这里有好多字节码指令看不懂,没关系,以后慢慢学。
这整段代码可以分为三个部分来看,先来讲讲第一部分,也即是最前面的几行,分别是版本号和访问标志,版本号的规则JDK5、6、7、8分别对应49、50、51、52。
public class test minor version: 0 //次版本号 major version: 52 //主版本号 flags: ACC_PUBLIC, ACC_SUPER //访问标志
常用的访问标志如下表
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0X0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令 |
ACC_INTERFACE | 0x0200 | 标志这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型 |
ACC_ANNOTATION | 0x2000 | 标志这是一个注解 |
ACC_ENUM | 0x4000 | 标志这是一个枚举 |
ACC_SYNTHETIC | 0x1000 | 标志这个类并非由用户产生的 |
接下来偷懒直接截个图,左边是class文件,右边是一些静态常量的枚举。
public test(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 1: 0
上面几行,就是传说的,一直不用我们自己写的,jvm帮我们加的,默认的无参构造函数,呼呼~
然后还有一部分就是最重要的入口函数,main方法了
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=5, args_size=1 0: sipush 500 3: istore_1 4: bipush 100 6: istore_2 7: iload_1 8: iload_2 9: idiv 10: istore_3 11: bipush 50 13: istore 4 15: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 18: iload_3 19: iload 4 21: iadd 22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 25: return LineNumberTable: line 3: 0 line 4: 4 line 5: 7 line 6: 11 line 7: 15 line 8: 25
指令顺序执行,整个过程无非就是不停的将数值入栈、出栈、运算、结果再入栈,最后的剩下的,就是结果,这些助记符的意思这里就不挨个解释了,等下附上指令码表,不过别忘了这个class文件的text是我们通过javap得到的,jvm实际执行的,可不是这些简明易懂的助记符,而是实实在在的16进制指令。
执行过程中,计数器帮我们记住指令执行到的位置,当遇到调用新的方法时,JVM会保留犯罪现场(将当前栈帧压入虚拟机栈),去执行新的任务(创建新的栈帧),也就是在另一个方法里如此循环,直到执行完所有方法,返回最后的结果。
好多都是平时知道一些但总也说不明白,这次总结了一下清晰多了,平时的代码都是IDE帮着编译,这次自己动手感觉竟然有点不错?(我看你是工作不饱和)
]]>
入手《七周七语言》挺久的了,一直拖延没看,今天开始,本书第一个介绍的是Lua,早之前听说过可以用来写游戏脚本,这次尝试一下
如无特别说明,以下命令均在 centOS 7 执行
curl -R -O http://www.lua.org/ftp/lua-5.3.0.tar.gztar zxf lua-5.3.0.tar.gzcd lua-5.3.0make linux testmake install
过程中可能会出现fatal error: readline/readline.h: No such file or directory
错误,这是因为缺少依赖包造成的
redhat 系列下这个软件包叫 readline-devel ubuntu 下叫 readline-dev 细分又分为 libreadline5-dev 和 libreadline6-dev
这不是本文重点,所以只要解决问题就行了,执行
yum install readline-devel
安装成功后继续
make linux testmake install
lua -v
出现这个Lua的版本号Lua 5.3.0 Copyright (C) 1994-2015 Lua.org, PUC-Rio
代表安装成功了,接下来可以愉快的玩耍了。
Hello World镇楼,输入lua进入REPL(Read-Eval-Print Loop)
print("Hello World!!!")或者"Hello world"
以上内容可能会显得冗长,因为并没有区分本篇是给小白看的,还是当作自己的笔记,不过秉着烂开始原则,直接先写了,后期搞明白定位了文风再变。
Lua没有;
作为结束符,也不用拿着游标卡尺量空格的缩进,甚至两个语句之间的区分只要一个空格就行,只要你不怕别人看了你的代码掀桌子,这样写也是可以的。
print("no time") print("for love")
详细的命名规范以及词法、语法、流程控制等,可以参考以下几个站点的教程,都写得很详细。
以下就直接照着书上徒手敲一遍,保存成.lua文件,执行一下
function call_twice(f)ff = function(num)return f(f(num))endretirn ffendfunction triple(n)return n*3endtest = call_twice(triple)=test(5)
输出结果是45,果然好看的代码根本无需解释(因为太短),一看就明白了