-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.json
More file actions
1 lines (1 loc) · 756 KB
/
content.json
File metadata and controls
1 lines (1 loc) · 756 KB
1
{"meta":{"title":"happyJared - 博客","subtitle":"保持勤學習的心,做好寫代碼這事。我有一个公众号:超级码里奥。","description":"Java developer, like Python and Javascript too,为人乐观、积极、向上、勤奋、和善、好相处,讲卫生,爱学习,爱码字,爱技术,爱生活","author":"happyJared","url":"https://blog.mariojd.cn"},"pages":[{"title":"來來去去停停走走匆匆","date":"2018-10-01","updated":"2019-04-18","comments":true,"path":"about/index.html","permalink":"https://blog.mariojd.cn/about/index.html","excerpt":"","keywords":"","text":"关于我坐标南方名都羊城半路出家行动派程序猿沟通Java Python Linux生活&&代码微洁癖小患者闲暇阅读||夜跑||骑行 联系俺对博客有任何的纠错/补充/疑问,欢迎直接在文章下方评论区留言如果想更深入的交流/探讨/尬聊,欢迎Email: dong1013@foxmail.com 座右铭 保持勤學習的心,做好寫代碼這事 站点说 分享学习、思考及行动后的那点皮毛","content":"<h3 id=\"关于我\"><a href=\"#关于我\" class=\"headerlink\" title=\"关于我\"></a>关于我</h3><p>坐标南方名都<code>羊城</code><br>半路出家行动派程序猿<br>沟通<code>Java Python Linux</code><br>生活<code>&&</code>代码微洁癖小患者<br>闲暇阅读<code>||</code>夜跑<code>||</code>骑行</p>\n<h3 id=\"联系俺\"><a href=\"#联系俺\" class=\"headerlink\" title=\"联系俺\"></a>联系俺</h3><p>对博客有任何的<code>纠错</code>/<code>补充</code>/<code>疑问</code>,欢迎直接在文章下方评论区留言<br>如果想更深入的<code>交流</code>/<code>探讨</code>/<code>尬聊</code>,欢迎Email: <code>dong1013@foxmail.com</code></p>\n<h3 id=\"座右铭\"><a href=\"#座右铭\" class=\"headerlink\" title=\"座右铭\"></a>座右铭</h3><blockquote>\n<p>保持勤學習的心,做好寫代碼這事</p>\n</blockquote>\n<h3 id=\"站点说\"><a href=\"#站点说\" class=\"headerlink\" title=\"站点说\"></a>站点说</h3><blockquote>\n<p>分享学习、思考及行动后的那点皮毛</p>\n</blockquote>\n"},{"title":"歲月這把無情的殺豬刀","date":"2019-04-01","updated":"2020-07-31","comments":true,"path":"life/index.html","permalink":"https://blog.mariojd.cn/life/index.html","excerpt":"","keywords":"","text":"读书、电影(2020) 阅读,该是为人这辈子努力培养的习惯 (#\\^.^#) Book 序号 书名 作者 阅读时间 0 失乐园 渡边淳一 2020.03 1 紫阳花日记 渡边淳一 2020.04 2 白夜行 东野圭吾 2020.04 3 钱从哪里来 唐涯 2020.05 4 82 年生的金智英 赵南柱 2020.05 5 非暴力沟通 马歇尔·卢森堡 2020.05 6 了不起的我 陈海贤 2020.05 Movie 序号 电影名 观影时间 0 大约在冬季 2020.01 1 家有喜事 2020.01 2 囧妈 2020.01 3 十二夜 2020.02 4 追风筝的人 2020.02 5 沉睡魔咒 2020.02 6 沉睡魔咒2 2020.02 7 极限逃生 2020.02 8 时空恋旅人 ❤❤ 2020.02 9 极限职业 2020.02 10 驱魔人 ❤❤ 2020.02 11 素媛 2020.02 12 熔炉 2020.02 13 叶问4 2020.02 14 你看起来好像很好吃 2020.02 15 假如爱有天意 2020.02 16 活死人黎明 2020.02 17 温暖的尸体/血肉之躯 2020.03 18 海蒂和爷爷 2020.03 19 普罗旺斯的夏天 2020.03 20 小丑回魂 2020.03 21 爱在黎明破晓前 2020.03 22 爱在日落余晖时 2020.03 23 误杀 2020.03 24 给桃子的信 ❤❤ 2020.03 25 爱在午夜降临前 2020.03 26 82年生的金智英 2020.03 27 辩护人 2020.03 28 我想吃掉你的胰脏 2020.04 29 大赢家 2020.04 30 西西里的美丽传说 2020.04 31 终结者6:黑暗命运 2020.04 32 楚门的世界 2020.04 33 穿条纹睡衣的男孩 2020.05 34 无敌破坏王 2020.05 35 无敌破坏王2:大闹互联网 2020.05 36 蝴蝶效应 2020.05 37 青蛇 2020.05 38 小偷家族 2020.05 39 春光乍泄 2020.05 40 风之谷 2020.05 41 萤火虫之墓 2020.06 42 冰川时代 2020.06 43 可可西里 2020.06 44 重庆森林 2020.06 45 阿飞正传 2020.06 46 新龙门客栈 2020.06 47 英雄本色 2020.06 48 东邪西毒 2020.06 49 无间道 2020.06 50 昨日青空 2020.07 51 未麻的部屋 2020.07 52 红辣椒 2020.07 53 哪吒闹海 2020.07 54 追随 2020.07 55 后天 2020.07 56 怪兽电力公司 2020.07 57 穿越时空的少女 2020.07 58 头脑特工队 2020.07 59 天气之子 2020.07 60 驯龙高手 2020.07 61 纵横四海 2020.07 62 彗星来的那一夜 2020.07 63 花样年华 2020.07 64 驴得水 2020.07 65 玛丽和马克思 2020.07 66 天使爱美丽 2020.07 读书、电影(2019) 岁月不饶人,我亦未曾饶过岁月 Book 序号 书名 作者 阅读时间 0 今日简史 [以色列] 尤瓦尔·赫拉利(Yuval Noah Harari) 2019.01 1 未来简史 [以色列] 尤瓦尔·赫拉利(Yuval Noah Harari) 2019.01 2 习惯的力量 [美] 查尔斯·都希格 2019.01 3 韭菜的自我修养 李笑来 2019.01 4 财富自由之路 李笑来 2019.02 5 中国为什么有前途 罗振宇 2019.02 6 财务自由之路 (德) 博多·舍费尔 2019.02 7 把时间当作朋友 李笑来 2019.02 8 黑天鹅:如何应对不可预知的未来 [美] 塔勒布 2019.03 9 薛兆丰经济学讲义 薛兆丰 2019.03 10 被劫持的私生活:性、婚姻与爱情的历史 肉唐僧 2019.03 11 工作前5年,决定你一生的财富 三公子 2019.03 12 您厉害,您赚得多 方三文 2019.03 13 活出生命的意义 [美] 维克多·E.弗兰克尔 2019.03 14 小狗钱钱 [德] 博多·舍费尔 2019.03 15 分布式服务框架原理与实践 李林锋 2019.03 16 Redis 设计与实现 黄健宏 2019.03 17 Redis 实战 [美] 约西亚 L.卡尔森(Josiah,L.,Carlson) 2019.04 18 Spring 源码深度解析 郝佳 2019.04 19 魔鬼搭讪学 魔鬼咨询师 2019.05 20 魔鬼聊天术 阮琦 2019.05 21 魔鬼约会学 魔鬼咨询师 2019.05 22 魔鬼经济学1:揭示隐藏在表象之下的真实世界 [美]史蒂芬•列维特 / [美]史蒂芬•都伯纳 2019.05 23 伟大的中国工业革命 文一 2019.06 24 投资中最简单的事 邱国鹭 2019.07 25 超级版图:全球供应链、超级城市与新商业文明的崛起 帕拉格·康纳 2019.07 26 投资最重要的事 [美] 霍华德·马克斯 2019.08 27 吾国教育病理 郑也夫 2019.09 Movie 序号 电影名 观影时间 0 疯狂的外星人 2019.02 1 黑豹 2019.03 2 神奇动物:格林德沃之罪 2019.03 3 叶问外传:张天志 2019.03 4 爱丽丝梦游仙境 2019.03 5 爱丽丝梦游仙境2镜中奇遇记 2019.03 6 白蛇:缘起 2019.03 7 海王 2019.03 8 哥斯拉 2019.03 9 神探蒲松龄 2019.03 10 大空头 2019.03 11 一吻定情 2019.03 12 大人物 2019.03 13 飞驰人生 2019.03 14 朝花夕誓 2019.04 15 新喜剧之王 2019.04 16 大话西游3 2019.04 17 大黄蜂 2019.04 18 廉政风云 2019.04 19 死侍1、2 2019.04.14 20 过春天 2019.04.19 21 调音师 2019.04.21 22 绿皮书 2019.05.02 23 龙猫 2019.05.03 24 哈尔的移动城堡 2019.05.11 25 千与千寻 2019.05.12 26 起风了 2019.05.13 27 寻找梦幻岛 2019.05.14 28 天空之城 2019.05.15 29 魔女宅急便 2019.05.16 30 比悲伤更悲伤的故事 2019.05.17 31 悬崖上的金鱼公主 2019.05.21 32 流浪地球 2019.05.22 33 红猪 2019.05.23 34 何以为家 2019.05.26 35 侧耳倾听 2019.05.28 36 幽灵公主 2019.05.30 37 夏目友人帐:结缘空蝉 2019.06.03 38 猫的报恩 2019.06.05 39 借东西的小人阿莉埃蒂 2019.06.06 40 下一任:前任 2019.06.07 41 萤火之森 2019.06.08 42 哪吒之魔童降世 2019.07.27 43 爱宠大机密 2019.08.04 44 穿普拉达的女王 2019.08.16 45 寄生虫 2019.08.17 46 哥斯拉2:怪兽之王 2019.09.14 47 复仇者联盟4 2019.09.22 48 爱宠大机密2 2019.09.27 49 使徒行者 2019.09.28 50 使徒行者2 2019.09.30 51 阿丽塔:战斗天使 2019.10.01 52 X战警:黑凤凰 2019.10.01 53 大侦探皮卡丘 2019.10.02 54 速度与激情:特别行动 2019.10.03 55 银河补习班 2019.10.04 56 五月天人生无限公司 2019.10.19 57 五月天诺亚方舟 2019.10.20 58 五月天追梦 2019.10.20 59 诛仙I 2019.10.26 60 徒手攀岩 2019.10.27 61 愤怒的小鸟2 2019.10.27 62 友情以上 2019.11.10 63 胡桃夹子和四个王国 2019.11.10 64 海上钢琴师 2019.11.17 65 小丑 2019.11.18 66 攀登者 2019.11.24 67 少年的你 2019.11.29 68 雪人奇缘 2019.12.08 69 我和我的祖国 2019.12.14 70 中国机长 2019.12.22 读书、电影(2018 前) 早些年,读过的书和看过的电影 ┭┮﹏┭┮ Book 序号 书名 作者 0 Java核心技术 卷I:基础知识 [美] 凯 S.霍斯特曼(Cay S. Horstmann) 1 阿里巴巴Java开发手册 杨冠宝 2 算法图解 [美] 巴尔加瓦(Aditya Bhargava) 3 Java核心技术卷II:高级特性 [美] 凯 S.霍斯特曼(Cay S. Horstmann) 4 Python核心编程 [美] 卫斯理 春(Wesley Chun) 5 富爸爸穷爸爸 [美] 罗伯特·清崎 6 人类简史:从动物到上帝 [以色列] 尤瓦尔·赫拉利(Yuval Noah Harari) 7 人性的弱点 [美] 戴尔·卡耐基 8 时间简史 [英] 史蒂芬·霍金 9 原则 [美] 瑞·达利欧 10 软技能 代码之外的生存指南 [美] 约翰 Z.森梅兹(John Z.Sonmez) 11 高效程序员的45个习惯:敏捷开发修炼之道 [美] Venkat Subramaniam,[美] Andy Hunt 12 时间管理 如何充分利用你的24小时 [美] 吉姆·兰德尔(Jim Randel) 13 科技之巅 麻省理工科技评论 14 科技之巅2 麻省理工科技评论 15 精通Python爬虫框架Scrapy [美] 迪米特里奥斯 考奇斯-劳卡斯(Dimitrios Kouzis-Loukas) 16 Spring微服务实战 [美] 约翰·卡内尔(John Carnell) 17 淘宝技术这十年 子柳 18 Java并发编程:核心方法与框架 高洪岩 19 华为内训 黄继伟 20 任正非传 孙力科 21 从Paxos到Zookeeper分布式一致性原理与实践 倪超 22 任正非:华为的冬天 陈广 23 解忧程序员 安晓辉 24 程序员的成长课 安晓辉,周鹏 25 Python编程快速上手 让繁琐工作自动化 [美] Al Sweigart(斯维加特) 26 Netty权威指南 李林锋 著 27 Java 8实战 [英] 厄马(Raoul-Gabriel Urma) 28 大型分布式网站架构设计与实践 陈康贤 29 Selenium自动化测试指南 赵卓 30 番茄工作法图解:简单易行的时间管理方法 [瑞典] 史蒂夫·诺特伯格(Staffan Noteberg) 31 Java 8函数式编程 [英] Richard Warburton 32 代码整洁之道 程序员的职业素养 [美] 罗伯特·C.马丁(Robert C.Martin) 33 硅谷之谜:浪潮之巅 续集 吴军 34 大数据时代的算法:机器学习、人工智能及其典型实例 刘凡平 35 浪潮之巅 上·下册 吴军 36 尽在双11 阿里巴巴技术演进与超越 阿里巴巴集团双11技术团队 37 区块链:价值互联网的基石 赵刚 38 Spring Cloud与Docker微服务架构实战 周立 39 Netty实战 [美] 诺曼·毛瑞尔(Norman Maurer),马文·艾伦·沃尔夫泰尔(Marvin Allen Wolfthal) 40 微服务架构与实践 王磊 41 大话设计模式 程杰 42 Jenkins权威指南 [美] John Ferguson Smart(约翰·弗格森·斯马特) 43 大型网站系统与Java中间件实践 曾宪杰 44 数学之美 吴军 45 Java并发编程的艺术 方腾飞,魏鹏,程晓明 46 Java并发编程实战 [美] 盖茨 47 大话数据结构 程杰 48 亿级流量网站架构核心技术 张开涛 49 Spring实战 [美] Craig Walls 沃尔斯 50 Docker实战 [美] Jeff Nickoloff(杰夫·尼克罗夫) 51 Java编程思想 [美] Bruce Eckel 52 Linux私房菜:基础学习篇 鸟哥 53 深入分析Java Web 技术内幕 许令波 54 Effective Java [美] 约书亚·布洛克(Joshua Bloch) 55 大型网站技术架构 李智慧 Movie 电影名 导演 指环王1:魔戒再现 彼得·杰克逊 指环王2:双塔奇兵 彼得·杰克逊 指环王3:王者无敌 彼得·杰克逊 肖申克的救赎 弗兰克·德拉邦特 霸王别姬 陈凯歌 阿甘正传 罗伯特·泽米吉斯 泰坦尼克号 詹姆斯·卡梅隆 忠犬八公的故事 拉斯·霍尔斯道姆 机器人总动员 安德鲁·斯坦顿 放牛班的春天 克里斯托夫·巴拉蒂 疯狂动物城 拜伦·霍华德 / 瑞奇·摩尔 / 杰拉德·布什 当幸福来敲门 加布里埃莱·穆奇诺 怦然心动 罗伯·莱纳 触不可及 奥利维埃·纳卡什 / 埃里克·托莱达诺 少年派的奇幻漂流 李安 摔跤吧!爸爸 涅提·蒂瓦里 飞屋环游记 彼特·道格特 / 鲍勃·彼德森 寻梦环游记 李昂克里奇 / 阿德里安·莫利纳 让子弹飞 姜文 阿凡达 詹姆斯·卡梅隆 加勒比海盗1:黑珍珠号的诅咒 戈尔·维宾斯基 加勒比海盗2:聚魂棺 戈尔·维宾斯基 加勒比海盗3:世界的尽头 戈尔·维宾斯基 加勒比海盗4:惊涛怪浪 罗伯·马歇尔 加勒比海盗5:死无对证 艾斯彭·山德伯格 / 乔阿吉姆·罗恩尼 喜剧之王 周星驰 / 李力持 神偷奶爸1 皮埃尔·柯芬 / 克里斯·雷纳德 神偷奶爸2 皮埃尔·柯芬 / 克里斯·雷纳德 神偷奶爸3 凯尔·巴尔达 / 皮埃尔·柯芬 / 埃里克·吉隆 疯狂原始人 柯克·德·米科 / 克里斯·桑德斯 小萝莉的猴神大叔 卡比尔·汗 你的名字 新海诚 秒速5厘米 新海诚 言叶之庭 新海诚 疯狂的石头 宁浩 功夫 周星驰 终结者1:未来战士 詹姆斯·卡梅隆 终结者2:审判日 詹姆斯·卡梅隆 终结者3:机器的觉醒 乔纳森·莫斯托 终结者4:救世主 约瑟夫·麦克金提·尼彻 终结者5:创世纪 阿兰·泰勒 哈利波特1:神秘的魔法石 克里斯·哥伦布 哈利波特2:消失的密室 克里斯·哥伦布 哈利波特3:阿兹卡班的逃犯 阿方索·卡隆 哈利波特4:火杯的考验 迈克·内威尔 哈利波特5:凤凰会的密令 大卫·叶茨 哈利波特6:混血王子的背叛 大卫·叶茨 哈利波特7:死神的圣物1.2 大卫·叶茨 爱乐之城 达米恩·查泽雷 北京遇上西雅图 薛晓路 七月与安生 曾国祥 失恋33天 滕华涛 夏洛特烦恼 闫非 / 彭大魔 后会无期 韩寒 乘风破浪 韩寒 解忧杂货店 韩杰 志明与春娇1 彭浩翔 志明与春娇2 彭浩翔 春娇救志明 彭浩翔 前任攻略 田羽生 前任2:备胎反击战 田羽生 前任3:再见前任 田羽生 我不是药神 文牧野 李茶的姑妈 吴昱翰 动物世界 韩延 邪不压正 姜文 影 张艺谋 西虹市首富 闫非 / 彭大魔 一出好戏 黄渤 神奇动物在哪里 大卫·叶茨 神奇动物在哪里2:格林德沃之罪 大卫·叶茨 悲伤逆流成河 落落 狄仁杰之通天帝国 徐克 狄仁杰之神都龙王 徐克 狄仁杰之四大天王 徐克 风语咒 刘阔 超时空同居 苏伦 摩天营救 罗森·马歇尔·瑟伯 后来的我们 刘若英 胖子行动队 包贝尔 我是江小白 剧场版 金承仁 21克拉 何念 釜山行 延尚昊 头号玩家 史蒂文·斯皮尔伯格 毒液:致命守护者 鲁本·弗雷斯彻 环太平洋 吉尔莫·德尔·托罗 环太平洋:雷霆再起 斯蒂文·S·迪奈特 异形 雷德利·斯科特 异形2 詹姆斯·卡梅隆 异形3 大卫·芬奇 异形4 让-皮埃尔·热内 异形:契约 雷德利·斯科特 狂暴巨兽 布拉德·佩顿 爱丽丝梦游仙境 蒂姆·波顿 爱丽丝梦游仙境2:镜中奇遇记 詹姆斯·博宾 烟花 新房昭之 / 武内宣之 战狼 吴京 战狼2 吴京 红海行动 林超贤 奇门遁甲 袁和平 巨齿鲨 乔·德特杜巴 捉妖记 许诚毅 捉妖记2 许诚毅 无名之辈 饶晓志 羞羞的铁拳 宋阳 / 张吃鱼 唐人街探案 陈思诚 唐人街探案2 陈思诚 起跑线 萨基特·乔杜里 二代妖精之今生有幸 肖洋 无问西东 李芳芳 三生三世十里桃花 赵小丁 / 安东尼·拉默里纳拉 逆时营救 尹鸿承 那些年,我们一起追的女孩 九把刀 小时代 瞿友宁 小时代2:青木时代 郭敬明 小时代3:刺金时代 郭敬明 小时代4:灵魂尽头 郭敬明 爵迹 郭敬明 既然青春留不住 田蒙 我们的十年 马伟豪 / 刘海 X战警 布莱恩·辛格 X战警2 布莱恩·辛格 X战警3:背水一战 布莱特·拉特纳 X战警:第一战 马修·沃恩 X战警:逆转未来 布莱恩·辛格 X战警:天启 布莱恩·辛格 金刚狼 加文·胡德 金刚狼2 詹姆斯·曼高德 金刚狼3:殊死一战 詹姆斯·曼高德 僵尸世界大战 马克·福斯特 美人鱼 周星驰 机器之血 张立嘉 无双 庄文强 速度与激情 罗伯·科恩 速度与激情2 约翰·辛格顿 速度与激情3 林诣彬 速度与激情4 林诣彬 速度与激情5 林诣彬 速度与激情6 Furious 6 林诣彬 速度与激情7 温子仁 速度与激情8 F·加里·格雷 金刚:骷髅岛 乔丹·沃格特-罗伯茨 青春期 管晓杰 青春失乐园 管晓杰 青春期3 管晓杰 恐怖直播 金秉祐 生化危机 保罗·安德森 生化危机2:启示录 亚历山大·维特 生化危机3:灭绝 拉塞尔·穆卡希 生化危机4:战神再生 保罗·安德森 生化危机5:惩罚 保罗·安德森 生化危机6:终章 保罗·安德森 变形金刚 迈克尔·贝 变形金刚2 迈克尔·贝 变形金刚3 迈克尔·贝 变形金刚4:绝迹重生 迈克尔·贝 变形金刚5:最后的骑士 迈克尔·贝 摩天楼 金志勋 海扁王 马修·沃恩 海扁王2 杰夫·瓦德洛 新乌龙院之笑闹江湖 朱延平 爱情公寓电影版 韦正 比得兔 威尔·古勒 厕所英雄 什里·那拉扬·辛 芳华 冯小刚 超能陆战队 Don Hall 克里斯·威廉 死神来了 黄毅瑜 死神来了2 大卫·R·艾里斯 死神来了3 黄毅瑜 死神来了4 大卫·R·艾里斯 死神来了5 史蒂文·奎里 巨额来电 彭顺 功守道 文章 明月几时有 许鞍华 声之形 山田尚子 死亡笔记 亚当·温加德 死亡笔记:最后的名字 金子修介 死亡笔记:L改变世界 中田秀夫 拆弹专家 邱礼涛 傲娇与偏见 李海蜀 / 黄彦威 魔兽 邓肯·琼斯 正义联盟 扎克·施奈德 饥饿游戏 盖瑞·罗斯 饥饿游戏2:星火燎原 弗朗西斯·劳伦斯 饥饿游戏3:嘲笑鸟(上) 弗朗西斯·劳伦斯 饥饿游戏3:嘲笑鸟(下) 弗朗西斯·劳伦斯 暮光之城 凯瑟琳·哈德威克 暮光之城2:新月 克里斯·韦兹 暮光之城3:月食 大卫·斯雷德 暮光之城4:破晓(上) 比尔·康顿 暮光之城4:破晓(下) 比尔·康顿 普罗米修斯 雷德利·斯科特 穿靴子的猫 克里斯·米勒 穿靴子的猫:萌猫三剑客 许诚毅 雷神 肯尼思·布拉纳 雷神2:黑暗世界 阿兰·泰勒 雷神3:诸神黄昏 伊加·维迪提 复仇者联盟 乔斯·韦登 复仇者联盟2:奥创纪元 乔斯·韦登 复仇者联盟3:无限战争 安东尼·罗素 / 乔·罗素 奇异博士 斯科特·德瑞克森 银河护卫队 詹姆斯·古恩 银河护卫队2 詹姆斯·古恩 绿巨人 李安 无敌浩克 路易斯·莱特里尔 愤怒的小鸟 费格尔·雷利 流感 金成洙 汉江怪物 奉俊昊 铁线虫入侵 朴正宇 海云台 尹济均 摩天楼 金志勋 潘多拉 朴正祐 隧道 金成勋 我是传奇 弗朗西斯·劳伦斯 动漫 追动漫,大概是这代人矢志不渝的使命 O(∩_∩)O Anime 动漫 出品 画江湖之灵主 若森数字 画江湖之不良人 若森数字 画江湖之杯莫停 若森数字 画江湖之换世门生 若森数字 秦时明月 玄机科技 天行九歌 玄机科技 武庚纪 玄机科技 天谕 玄机科技 斗罗大陆 玄机科技 我是江小白 两点十分 雪鹰领主 企鹅影视 星辰变 腾讯阅文 斗破苍穹 腾讯阅文 武动乾坤 腾讯阅文 少年歌行 中影年年 万界神主 若鸿文化 万界仙踪 若鸿文化 少年锦衣卫 柏言映画 太乙仙魔录之灵飞纪 糖心文化 游戏王 日本动漫 拳皇命运 龙族动漫","content":"<h2 id=\"读书、电影(2020)\"><a href=\"#读书、电影(2020)\" class=\"headerlink\" title=\"读书、电影(2020)\"></a>读书、电影(2020)</h2><blockquote>\n<p>阅读,该是为人这辈子努力培养的习惯 (#\\^.^#)</p>\n</blockquote>\n<h3 id=\"Book\"><a href=\"#Book\" class=\"headerlink\" title=\"Book\"></a>Book</h3><table>\n<thead>\n<tr>\n<th>序号</th>\n<th>书名</th>\n<th>作者</th>\n<th>阅读时间</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0</td>\n<td>失乐园</td>\n<td>渡边淳一</td>\n<td>2020.03</td>\n</tr>\n<tr>\n<td>1</td>\n<td><del>紫阳花日记</del></td>\n<td>渡边淳一</td>\n<td>2020.04</td>\n</tr>\n<tr>\n<td>2</td>\n<td>白夜行</td>\n<td>东野圭吾</td>\n<td>2020.04</td>\n</tr>\n<tr>\n<td>3</td>\n<td>钱从哪里来</td>\n<td>唐涯</td>\n<td>2020.05</td>\n</tr>\n<tr>\n<td>4</td>\n<td>82 年生的金智英</td>\n<td>赵南柱</td>\n<td>2020.05</td>\n</tr>\n<tr>\n<td>5</td>\n<td>非暴力沟通</td>\n<td>马歇尔·卢森堡</td>\n<td>2020.05</td>\n</tr>\n<tr>\n<td>6</td>\n<td>了不起的我</td>\n<td>陈海贤</td>\n<td>2020.05</td>\n</tr>\n</tbody>\n</table>\n<h3 id=\"Movie\"><a href=\"#Movie\" class=\"headerlink\" title=\"Movie\"></a>Movie</h3><table>\n<thead>\n<tr>\n<th>序号</th>\n<th>电影名</th>\n<th>观影时间</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0</td>\n<td>大约在冬季</td>\n<td>2020.01</td>\n</tr>\n<tr>\n<td>1</td>\n<td>家有喜事</td>\n<td>2020.01</td>\n</tr>\n<tr>\n<td>2</td>\n<td>囧妈</td>\n<td>2020.01</td>\n</tr>\n<tr>\n<td>3</td>\n<td>十二夜</td>\n<td>2020.02</td>\n</tr>\n<tr>\n<td>4</td>\n<td>追风筝的人</td>\n<td>2020.02</td>\n</tr>\n<tr>\n<td>5</td>\n<td>沉睡魔咒</td>\n<td>2020.02</td>\n</tr>\n<tr>\n<td>6</td>\n<td>沉睡魔咒2</td>\n<td>2020.02</td>\n</tr>\n<tr>\n<td>7</td>\n<td>极限逃生</td>\n<td>2020.02</td>\n</tr>\n<tr>\n<td>8</td>\n<td>时空恋旅人 ❤❤</td>\n<td>2020.02</td>\n</tr>\n<tr>\n<td>9</td>\n<td>极限职业</td>\n<td>2020.02</td>\n</tr>\n<tr>\n<td>10</td>\n<td>驱魔人 ❤❤</td>\n<td>2020.02</td>\n</tr>\n<tr>\n<td>11</td>\n<td>素媛</td>\n<td>2020.02</td>\n</tr>\n<tr>\n<td>12</td>\n<td>熔炉</td>\n<td>2020.02</td>\n</tr>\n<tr>\n<td>13</td>\n<td>叶问4</td>\n<td>2020.02</td>\n</tr>\n<tr>\n<td>14</td>\n<td>你看起来好像很好吃</td>\n<td>2020.02</td>\n</tr>\n<tr>\n<td>15</td>\n<td>假如爱有天意</td>\n<td>2020.02</td>\n</tr>\n<tr>\n<td>16</td>\n<td>活死人黎明</td>\n<td>2020.02</td>\n</tr>\n<tr>\n<td>17</td>\n<td>温暖的尸体/血肉之躯</td>\n<td>2020.03</td>\n</tr>\n<tr>\n<td>18</td>\n<td>海蒂和爷爷</td>\n<td>2020.03</td>\n</tr>\n<tr>\n<td>19</td>\n<td>普罗旺斯的夏天</td>\n<td>2020.03</td>\n</tr>\n<tr>\n<td>20</td>\n<td><del>小丑回魂</del></td>\n<td>2020.03</td>\n</tr>\n<tr>\n<td>21</td>\n<td>爱在黎明破晓前</td>\n<td>2020.03</td>\n</tr>\n<tr>\n<td>22</td>\n<td>爱在日落余晖时</td>\n<td>2020.03</td>\n</tr>\n<tr>\n<td>23</td>\n<td>误杀</td>\n<td>2020.03</td>\n</tr>\n<tr>\n<td>24</td>\n<td>给桃子的信 ❤❤</td>\n<td>2020.03</td>\n</tr>\n<tr>\n<td>25</td>\n<td>爱在午夜降临前</td>\n<td>2020.03</td>\n</tr>\n<tr>\n<td>26</td>\n<td>82年生的金智英</td>\n<td>2020.03</td>\n</tr>\n<tr>\n<td>27</td>\n<td>辩护人</td>\n<td>2020.03</td>\n</tr>\n<tr>\n<td>28</td>\n<td>我想吃掉你的胰脏</td>\n<td>2020.04</td>\n</tr>\n<tr>\n<td>29</td>\n<td>大赢家</td>\n<td>2020.04</td>\n</tr>\n<tr>\n<td>30</td>\n<td>西西里的美丽传说</td>\n<td>2020.04</td>\n</tr>\n<tr>\n<td>31</td>\n<td>终结者6:黑暗命运</td>\n<td>2020.04</td>\n</tr>\n<tr>\n<td>32</td>\n<td>楚门的世界</td>\n<td>2020.04</td>\n</tr>\n<tr>\n<td>33</td>\n<td>穿条纹睡衣的男孩</td>\n<td>2020.05</td>\n</tr>\n<tr>\n<td>34</td>\n<td>无敌破坏王</td>\n<td>2020.05</td>\n</tr>\n<tr>\n<td>35</td>\n<td>无敌破坏王2:大闹互联网</td>\n<td>2020.05</td>\n</tr>\n<tr>\n<td>36</td>\n<td>蝴蝶效应</td>\n<td>2020.05</td>\n</tr>\n<tr>\n<td>37</td>\n<td>青蛇</td>\n<td>2020.05</td>\n</tr>\n<tr>\n<td>38</td>\n<td>小偷家族</td>\n<td>2020.05</td>\n</tr>\n<tr>\n<td>39</td>\n<td>春光乍泄</td>\n<td>2020.05</td>\n</tr>\n<tr>\n<td>40</td>\n<td>风之谷</td>\n<td>2020.05</td>\n</tr>\n<tr>\n<td>41</td>\n<td>萤火虫之墓</td>\n<td>2020.06</td>\n</tr>\n<tr>\n<td>42</td>\n<td>冰川时代</td>\n<td>2020.06</td>\n</tr>\n<tr>\n<td>43</td>\n<td>可可西里</td>\n<td>2020.06</td>\n</tr>\n<tr>\n<td>44</td>\n<td>重庆森林</td>\n<td>2020.06</td>\n</tr>\n<tr>\n<td>45</td>\n<td>阿飞正传</td>\n<td>2020.06</td>\n</tr>\n<tr>\n<td>46</td>\n<td>新龙门客栈</td>\n<td>2020.06</td>\n</tr>\n<tr>\n<td>47</td>\n<td>英雄本色</td>\n<td>2020.06</td>\n</tr>\n<tr>\n<td>48</td>\n<td>东邪西毒</td>\n<td>2020.06</td>\n</tr>\n<tr>\n<td>49</td>\n<td>无间道</td>\n<td>2020.06</td>\n</tr>\n<tr>\n<td>50</td>\n<td>昨日青空</td>\n<td>2020.07</td>\n</tr>\n<tr>\n<td>51</td>\n<td>未麻的部屋</td>\n<td>2020.07</td>\n</tr>\n<tr>\n<td>52</td>\n<td>红辣椒</td>\n<td>2020.07</td>\n</tr>\n<tr>\n<td>53</td>\n<td>哪吒闹海</td>\n<td>2020.07</td>\n</tr>\n<tr>\n<td>54</td>\n<td>追随</td>\n<td>2020.07</td>\n</tr>\n<tr>\n<td>55</td>\n<td>后天</td>\n<td>2020.07</td>\n</tr>\n<tr>\n<td>56</td>\n<td>怪兽电力公司</td>\n<td>2020.07</td>\n</tr>\n<tr>\n<td>57</td>\n<td>穿越时空的少女</td>\n<td>2020.07</td>\n</tr>\n<tr>\n<td>58</td>\n<td>头脑特工队</td>\n<td>2020.07</td>\n</tr>\n<tr>\n<td>59</td>\n<td>天气之子</td>\n<td>2020.07</td>\n</tr>\n<tr>\n<td>60</td>\n<td>驯龙高手</td>\n<td>2020.07</td>\n</tr>\n<tr>\n<td>61</td>\n<td>纵横四海</td>\n<td>2020.07</td>\n</tr>\n<tr>\n<td>62</td>\n<td>彗星来的那一夜</td>\n<td>2020.07</td>\n</tr>\n<tr>\n<td>63</td>\n<td>花样年华</td>\n<td>2020.07</td>\n</tr>\n<tr>\n<td>64</td>\n<td>驴得水</td>\n<td>2020.07</td>\n</tr>\n<tr>\n<td>65</td>\n<td>玛丽和马克思</td>\n<td>2020.07</td>\n</tr>\n<tr>\n<td>66</td>\n<td>天使爱美丽</td>\n<td>2020.07</td>\n</tr>\n</tbody>\n</table>\n<h2 id=\"读书、电影(2019)\"><a href=\"#读书、电影(2019)\" class=\"headerlink\" title=\"读书、电影(2019)\"></a>读书、电影(2019)</h2><blockquote>\n<p>岁月不饶人,我亦未曾饶过岁月</p>\n</blockquote>\n<h3 id=\"Book-1\"><a href=\"#Book-1\" class=\"headerlink\" title=\"Book\"></a>Book</h3><table>\n<thead>\n<tr>\n<th>序号</th>\n<th>书名</th>\n<th>作者</th>\n<th>阅读时间</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0</td>\n<td>今日简史</td>\n<td>[以色列] 尤瓦尔·赫拉利(Yuval Noah Harari)</td>\n<td>2019.01</td>\n</tr>\n<tr>\n<td>1</td>\n<td>未来简史</td>\n<td>[以色列] 尤瓦尔·赫拉利(Yuval Noah Harari)</td>\n<td>2019.01</td>\n</tr>\n<tr>\n<td>2</td>\n<td>习惯的力量</td>\n<td>[美] 查尔斯·都希格</td>\n<td>2019.01</td>\n</tr>\n<tr>\n<td>3</td>\n<td>韭菜的自我修养</td>\n<td>李笑来</td>\n<td>2019.01</td>\n</tr>\n<tr>\n<td>4</td>\n<td>财富自由之路</td>\n<td>李笑来</td>\n<td>2019.02</td>\n</tr>\n<tr>\n<td>5</td>\n<td>中国为什么有前途</td>\n<td>罗振宇</td>\n<td>2019.02</td>\n</tr>\n<tr>\n<td>6</td>\n<td>财务自由之路</td>\n<td>(德) 博多·舍费尔</td>\n<td>2019.02</td>\n</tr>\n<tr>\n<td>7</td>\n<td>把时间当作朋友</td>\n<td>李笑来</td>\n<td>2019.02</td>\n</tr>\n<tr>\n<td>8</td>\n<td>黑天鹅:如何应对不可预知的未来</td>\n<td>[美] 塔勒布</td>\n<td>2019.03</td>\n</tr>\n<tr>\n<td>9</td>\n<td>薛兆丰经济学讲义</td>\n<td>薛兆丰</td>\n<td>2019.03</td>\n</tr>\n<tr>\n<td>10</td>\n<td>被劫持的私生活:性、婚姻与爱情的历史</td>\n<td>肉唐僧</td>\n<td>2019.03</td>\n</tr>\n<tr>\n<td>11</td>\n<td>工作前5年,决定你一生的财富</td>\n<td>三公子</td>\n<td>2019.03</td>\n</tr>\n<tr>\n<td>12</td>\n<td>您厉害,您赚得多</td>\n<td>方三文</td>\n<td>2019.03</td>\n</tr>\n<tr>\n<td>13</td>\n<td>活出生命的意义</td>\n<td>[美] 维克多·E.弗兰克尔</td>\n<td>2019.03</td>\n</tr>\n<tr>\n<td>14</td>\n<td>小狗钱钱</td>\n<td>[德] 博多·舍费尔</td>\n<td>2019.03</td>\n</tr>\n<tr>\n<td>15</td>\n<td>分布式服务框架原理与实践</td>\n<td>李林锋</td>\n<td>2019.03</td>\n</tr>\n<tr>\n<td>16</td>\n<td>Redis 设计与实现</td>\n<td>黄健宏</td>\n<td>2019.03</td>\n</tr>\n<tr>\n<td>17</td>\n<td>Redis 实战</td>\n<td>[美] 约西亚 L.卡尔森(Josiah,L.,Carlson)</td>\n<td>2019.04</td>\n</tr>\n<tr>\n<td>18</td>\n<td>Spring 源码深度解析</td>\n<td>郝佳</td>\n<td>2019.04</td>\n</tr>\n<tr>\n<td>19</td>\n<td>魔鬼搭讪学</td>\n<td>魔鬼咨询师</td>\n<td>2019.05</td>\n</tr>\n<tr>\n<td>20</td>\n<td>魔鬼聊天术</td>\n<td>阮琦</td>\n<td>2019.05</td>\n</tr>\n<tr>\n<td>21</td>\n<td>魔鬼约会学</td>\n<td>魔鬼咨询师</td>\n<td>2019.05</td>\n</tr>\n<tr>\n<td>22</td>\n<td>魔鬼经济学1:揭示隐藏在表象之下的真实世界</td>\n<td>[美]史蒂芬•列维特 / [美]史蒂芬•都伯纳</td>\n<td>2019.05</td>\n</tr>\n<tr>\n<td>23</td>\n<td>伟大的中国工业革命</td>\n<td>文一</td>\n<td>2019.06</td>\n</tr>\n<tr>\n<td>24</td>\n<td>投资中最简单的事</td>\n<td>邱国鹭</td>\n<td>2019.07</td>\n</tr>\n<tr>\n<td>25</td>\n<td>超级版图:全球供应链、超级城市与新商业文明的崛起</td>\n<td>帕拉格·康纳</td>\n<td>2019.07</td>\n</tr>\n<tr>\n<td>26</td>\n<td>投资最重要的事</td>\n<td>[美] 霍华德·马克斯</td>\n<td>2019.08</td>\n</tr>\n<tr>\n<td>27</td>\n<td>吾国教育病理</td>\n<td>郑也夫</td>\n<td>2019.09</td>\n</tr>\n</tbody>\n</table>\n<h3 id=\"Movie-1\"><a href=\"#Movie-1\" class=\"headerlink\" title=\"Movie\"></a>Movie</h3><table>\n<thead>\n<tr>\n<th>序号</th>\n<th>电影名</th>\n<th>观影时间</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0</td>\n<td>疯狂的外星人</td>\n<td>2019.02</td>\n</tr>\n<tr>\n<td>1</td>\n<td>黑豹</td>\n<td>2019.03</td>\n</tr>\n<tr>\n<td>2</td>\n<td>神奇动物:格林德沃之罪</td>\n<td>2019.03</td>\n</tr>\n<tr>\n<td>3</td>\n<td>叶问外传:张天志</td>\n<td>2019.03</td>\n</tr>\n<tr>\n<td>4</td>\n<td>爱丽丝梦游仙境</td>\n<td>2019.03</td>\n</tr>\n<tr>\n<td>5</td>\n<td>爱丽丝梦游仙境2镜中奇遇记</td>\n<td>2019.03</td>\n</tr>\n<tr>\n<td>6</td>\n<td>白蛇:缘起</td>\n<td>2019.03</td>\n</tr>\n<tr>\n<td>7</td>\n<td>海王</td>\n<td>2019.03</td>\n</tr>\n<tr>\n<td>8</td>\n<td>哥斯拉</td>\n<td>2019.03</td>\n</tr>\n<tr>\n<td>9</td>\n<td>神探蒲松龄</td>\n<td>2019.03</td>\n</tr>\n<tr>\n<td>10</td>\n<td>大空头</td>\n<td>2019.03</td>\n</tr>\n<tr>\n<td>11</td>\n<td>一吻定情</td>\n<td>2019.03</td>\n</tr>\n<tr>\n<td>12</td>\n<td>大人物</td>\n<td>2019.03</td>\n</tr>\n<tr>\n<td>13</td>\n<td>飞驰人生</td>\n<td>2019.03</td>\n</tr>\n<tr>\n<td>14</td>\n<td>朝花夕誓</td>\n<td>2019.04</td>\n</tr>\n<tr>\n<td>15</td>\n<td>新喜剧之王</td>\n<td>2019.04</td>\n</tr>\n<tr>\n<td>16</td>\n<td>大话西游3</td>\n<td>2019.04</td>\n</tr>\n<tr>\n<td>17</td>\n<td>大黄蜂</td>\n<td>2019.04</td>\n</tr>\n<tr>\n<td>18</td>\n<td>廉政风云</td>\n<td>2019.04</td>\n</tr>\n<tr>\n<td>19</td>\n<td>死侍1、2</td>\n<td>2019.04.14</td>\n</tr>\n<tr>\n<td>20</td>\n<td>过春天</td>\n<td>2019.04.19</td>\n</tr>\n<tr>\n<td>21</td>\n<td>调音师</td>\n<td>2019.04.21</td>\n</tr>\n<tr>\n<td>22</td>\n<td>绿皮书</td>\n<td>2019.05.02</td>\n</tr>\n<tr>\n<td>23</td>\n<td>龙猫</td>\n<td>2019.05.03</td>\n</tr>\n<tr>\n<td>24</td>\n<td>哈尔的移动城堡</td>\n<td>2019.05.11</td>\n</tr>\n<tr>\n<td>25</td>\n<td>千与千寻</td>\n<td>2019.05.12</td>\n</tr>\n<tr>\n<td>26</td>\n<td>起风了</td>\n<td>2019.05.13</td>\n</tr>\n<tr>\n<td>27</td>\n<td>寻找梦幻岛</td>\n<td>2019.05.14</td>\n</tr>\n<tr>\n<td>28</td>\n<td>天空之城</td>\n<td>2019.05.15</td>\n</tr>\n<tr>\n<td>29</td>\n<td>魔女宅急便</td>\n<td>2019.05.16</td>\n</tr>\n<tr>\n<td>30</td>\n<td>比悲伤更悲伤的故事</td>\n<td>2019.05.17</td>\n</tr>\n<tr>\n<td>31</td>\n<td>悬崖上的金鱼公主</td>\n<td>2019.05.21</td>\n</tr>\n<tr>\n<td>32</td>\n<td>流浪地球</td>\n<td>2019.05.22</td>\n</tr>\n<tr>\n<td>33</td>\n<td>红猪</td>\n<td>2019.05.23</td>\n</tr>\n<tr>\n<td>34</td>\n<td>何以为家</td>\n<td>2019.05.26</td>\n</tr>\n<tr>\n<td>35</td>\n<td>侧耳倾听</td>\n<td>2019.05.28</td>\n</tr>\n<tr>\n<td>36</td>\n<td>幽灵公主</td>\n<td>2019.05.30</td>\n</tr>\n<tr>\n<td>37</td>\n<td>夏目友人帐:结缘空蝉</td>\n<td>2019.06.03</td>\n</tr>\n<tr>\n<td>38</td>\n<td>猫的报恩</td>\n<td>2019.06.05</td>\n</tr>\n<tr>\n<td>39</td>\n<td>借东西的小人阿莉埃蒂</td>\n<td>2019.06.06</td>\n</tr>\n<tr>\n<td>40</td>\n<td>下一任:前任</td>\n<td>2019.06.07</td>\n</tr>\n<tr>\n<td>41</td>\n<td>萤火之森</td>\n<td>2019.06.08</td>\n</tr>\n<tr>\n<td>42</td>\n<td>哪吒之魔童降世</td>\n<td>2019.07.27</td>\n</tr>\n<tr>\n<td>43</td>\n<td>爱宠大机密</td>\n<td>2019.08.04</td>\n</tr>\n<tr>\n<td>44</td>\n<td>穿普拉达的女王</td>\n<td>2019.08.16</td>\n</tr>\n<tr>\n<td>45</td>\n<td>寄生虫</td>\n<td>2019.08.17</td>\n</tr>\n<tr>\n<td>46</td>\n<td>哥斯拉2:怪兽之王</td>\n<td>2019.09.14</td>\n</tr>\n<tr>\n<td>47</td>\n<td>复仇者联盟4</td>\n<td>2019.09.22</td>\n</tr>\n<tr>\n<td>48</td>\n<td>爱宠大机密2</td>\n<td>2019.09.27</td>\n</tr>\n<tr>\n<td>49</td>\n<td>使徒行者</td>\n<td>2019.09.28</td>\n</tr>\n<tr>\n<td>50</td>\n<td>使徒行者2</td>\n<td>2019.09.30</td>\n</tr>\n<tr>\n<td>51</td>\n<td>阿丽塔:战斗天使</td>\n<td>2019.10.01</td>\n</tr>\n<tr>\n<td>52</td>\n<td>X战警:黑凤凰</td>\n<td>2019.10.01</td>\n</tr>\n<tr>\n<td>53</td>\n<td>大侦探皮卡丘</td>\n<td>2019.10.02</td>\n</tr>\n<tr>\n<td>54</td>\n<td>速度与激情:特别行动</td>\n<td>2019.10.03</td>\n</tr>\n<tr>\n<td>55</td>\n<td>银河补习班</td>\n<td>2019.10.04</td>\n</tr>\n<tr>\n<td>56</td>\n<td>五月天人生无限公司</td>\n<td>2019.10.19</td>\n</tr>\n<tr>\n<td>57</td>\n<td>五月天诺亚方舟</td>\n<td>2019.10.20</td>\n</tr>\n<tr>\n<td>58</td>\n<td>五月天追梦</td>\n<td>2019.10.20</td>\n</tr>\n<tr>\n<td>59</td>\n<td>诛仙I</td>\n<td>2019.10.26</td>\n</tr>\n<tr>\n<td>60</td>\n<td>徒手攀岩</td>\n<td>2019.10.27</td>\n</tr>\n<tr>\n<td>61</td>\n<td>愤怒的小鸟2</td>\n<td>2019.10.27</td>\n</tr>\n<tr>\n<td>62</td>\n<td>友情以上</td>\n<td>2019.11.10</td>\n</tr>\n<tr>\n<td>63</td>\n<td>胡桃夹子和四个王国</td>\n<td>2019.11.10</td>\n</tr>\n<tr>\n<td>64</td>\n<td>海上钢琴师</td>\n<td>2019.11.17</td>\n</tr>\n<tr>\n<td>65</td>\n<td>小丑</td>\n<td>2019.11.18</td>\n</tr>\n<tr>\n<td>66</td>\n<td>攀登者</td>\n<td>2019.11.24</td>\n</tr>\n<tr>\n<td>67</td>\n<td>少年的你</td>\n<td>2019.11.29</td>\n</tr>\n<tr>\n<td>68</td>\n<td>雪人奇缘</td>\n<td>2019.12.08</td>\n</tr>\n<tr>\n<td>69</td>\n<td>我和我的祖国</td>\n<td>2019.12.14</td>\n</tr>\n<tr>\n<td>70</td>\n<td>中国机长</td>\n<td>2019.12.22</td>\n</tr>\n</tbody>\n</table>\n<h2 id=\"读书、电影(2018-前)\"><a href=\"#读书、电影(2018-前)\" class=\"headerlink\" title=\"读书、电影(2018 前)\"></a>读书、电影(2018 前)</h2><blockquote>\n<p>早些年,读过的书和看过的电影 ┭┮﹏┭┮</p>\n</blockquote>\n<h3 id=\"Book-2\"><a href=\"#Book-2\" class=\"headerlink\" title=\"Book\"></a>Book</h3><table>\n<thead>\n<tr>\n<th>序号</th>\n<th>书名</th>\n<th>作者</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0</td>\n<td>Java核心技术 卷I:基础知识</td>\n<td>[美] 凯 S.霍斯特曼(Cay S. Horstmann)</td>\n</tr>\n<tr>\n<td>1</td>\n<td>阿里巴巴Java开发手册</td>\n<td>杨冠宝</td>\n</tr>\n<tr>\n<td>2</td>\n<td>算法图解</td>\n<td>[美] 巴尔加瓦(Aditya Bhargava)</td>\n</tr>\n<tr>\n<td>3</td>\n<td>Java核心技术卷II:高级特性</td>\n<td>[美] 凯 S.霍斯特曼(Cay S. Horstmann)</td>\n</tr>\n<tr>\n<td>4</td>\n<td>Python核心编程</td>\n<td>[美] 卫斯理 春(Wesley Chun)</td>\n</tr>\n<tr>\n<td>5</td>\n<td>富爸爸穷爸爸</td>\n<td>[美] 罗伯特·清崎</td>\n</tr>\n<tr>\n<td>6</td>\n<td>人类简史:从动物到上帝</td>\n<td>[以色列] 尤瓦尔·赫拉利(Yuval Noah Harari)</td>\n</tr>\n<tr>\n<td>7</td>\n<td>人性的弱点</td>\n<td>[美] 戴尔·卡耐基</td>\n</tr>\n<tr>\n<td>8</td>\n<td>时间简史</td>\n<td>[英] 史蒂芬·霍金</td>\n</tr>\n<tr>\n<td>9</td>\n<td>原则</td>\n<td>[美] 瑞·达利欧</td>\n</tr>\n<tr>\n<td>10</td>\n<td>软技能 代码之外的生存指南</td>\n<td>[美] 约翰 Z.森梅兹(John Z.Sonmez)</td>\n</tr>\n<tr>\n<td>11</td>\n<td>高效程序员的45个习惯:敏捷开发修炼之道</td>\n<td>[美] Venkat Subramaniam,[美] Andy Hunt</td>\n</tr>\n<tr>\n<td>12</td>\n<td>时间管理 如何充分利用你的24小时</td>\n<td>[美] 吉姆·兰德尔(Jim Randel)</td>\n</tr>\n<tr>\n<td>13</td>\n<td>科技之巅</td>\n<td>麻省理工科技评论</td>\n</tr>\n<tr>\n<td>14</td>\n<td>科技之巅2</td>\n<td>麻省理工科技评论</td>\n</tr>\n<tr>\n<td>15</td>\n<td>精通Python爬虫框架Scrapy</td>\n<td>[美] 迪米特里奥斯 考奇斯-劳卡斯(Dimitrios Kouzis-Loukas)</td>\n</tr>\n<tr>\n<td>16</td>\n<td>Spring微服务实战</td>\n<td>[美] 约翰·卡内尔(John Carnell)</td>\n</tr>\n<tr>\n<td>17</td>\n<td>淘宝技术这十年</td>\n<td>子柳</td>\n</tr>\n<tr>\n<td>18</td>\n<td>Java并发编程:核心方法与框架</td>\n<td>高洪岩</td>\n</tr>\n<tr>\n<td>19</td>\n<td>华为内训</td>\n<td>黄继伟</td>\n</tr>\n<tr>\n<td>20</td>\n<td>任正非传</td>\n<td>孙力科</td>\n</tr>\n<tr>\n<td>21</td>\n<td>从Paxos到Zookeeper分布式一致性原理与实践</td>\n<td>倪超</td>\n</tr>\n<tr>\n<td>22</td>\n<td>任正非:华为的冬天</td>\n<td>陈广</td>\n</tr>\n<tr>\n<td>23</td>\n<td>解忧程序员</td>\n<td>安晓辉</td>\n</tr>\n<tr>\n<td>24</td>\n<td>程序员的成长课</td>\n<td>安晓辉,周鹏</td>\n</tr>\n<tr>\n<td>25</td>\n<td>Python编程快速上手 让繁琐工作自动化</td>\n<td>[美] Al Sweigart(斯维加特)</td>\n</tr>\n<tr>\n<td>26</td>\n<td>Netty权威指南</td>\n<td>李林锋 著</td>\n</tr>\n<tr>\n<td>27</td>\n<td>Java 8实战</td>\n<td>[英] 厄马(Raoul-Gabriel Urma)</td>\n</tr>\n<tr>\n<td>28</td>\n<td>大型分布式网站架构设计与实践</td>\n<td>陈康贤</td>\n</tr>\n<tr>\n<td>29</td>\n<td>Selenium自动化测试指南</td>\n<td>赵卓</td>\n</tr>\n<tr>\n<td>30</td>\n<td>番茄工作法图解:简单易行的时间管理方法</td>\n<td>[瑞典] 史蒂夫·诺特伯格(Staffan Noteberg)</td>\n</tr>\n<tr>\n<td>31</td>\n<td>Java 8函数式编程</td>\n<td>[英] Richard Warburton</td>\n</tr>\n<tr>\n<td>32</td>\n<td>代码整洁之道 程序员的职业素养</td>\n<td>[美] 罗伯特·C.马丁(Robert C.Martin)</td>\n</tr>\n<tr>\n<td>33</td>\n<td>硅谷之谜:浪潮之巅 续集</td>\n<td>吴军</td>\n</tr>\n<tr>\n<td>34</td>\n<td>大数据时代的算法:机器学习、人工智能及其典型实例</td>\n<td>刘凡平</td>\n</tr>\n<tr>\n<td>35</td>\n<td>浪潮之巅 上·下册</td>\n<td>吴军</td>\n</tr>\n<tr>\n<td>36</td>\n<td>尽在双11 阿里巴巴技术演进与超越</td>\n<td>阿里巴巴集团双11技术团队</td>\n</tr>\n<tr>\n<td>37</td>\n<td>区块链:价值互联网的基石</td>\n<td>赵刚</td>\n</tr>\n<tr>\n<td>38</td>\n<td>Spring Cloud与Docker微服务架构实战</td>\n<td>周立</td>\n</tr>\n<tr>\n<td>39</td>\n<td>Netty实战</td>\n<td>[美] 诺曼·毛瑞尔(Norman Maurer),马文·艾伦·沃尔夫泰尔(Marvin Allen Wolfthal)</td>\n</tr>\n<tr>\n<td>40</td>\n<td>微服务架构与实践</td>\n<td>王磊</td>\n</tr>\n<tr>\n<td>41</td>\n<td>大话设计模式</td>\n<td>程杰</td>\n</tr>\n<tr>\n<td>42</td>\n<td>Jenkins权威指南</td>\n<td>[美] John Ferguson Smart(约翰·弗格森·斯马特)</td>\n</tr>\n<tr>\n<td>43</td>\n<td>大型网站系统与Java中间件实践</td>\n<td>曾宪杰</td>\n</tr>\n<tr>\n<td>44</td>\n<td>数学之美</td>\n<td>吴军</td>\n</tr>\n<tr>\n<td>45</td>\n<td>Java并发编程的艺术</td>\n<td>方腾飞,魏鹏,程晓明</td>\n</tr>\n<tr>\n<td>46</td>\n<td>Java并发编程实战</td>\n<td>[美] 盖茨</td>\n</tr>\n<tr>\n<td>47</td>\n<td>大话数据结构</td>\n<td>程杰</td>\n</tr>\n<tr>\n<td>48</td>\n<td>亿级流量网站架构核心技术</td>\n<td>张开涛</td>\n</tr>\n<tr>\n<td>49</td>\n<td>Spring实战</td>\n<td>[美] Craig Walls 沃尔斯</td>\n</tr>\n<tr>\n<td>50</td>\n<td>Docker实战</td>\n<td>[美] Jeff Nickoloff(杰夫·尼克罗夫)</td>\n</tr>\n<tr>\n<td>51</td>\n<td>Java编程思想</td>\n<td>[美] Bruce Eckel</td>\n</tr>\n<tr>\n<td>52</td>\n<td>Linux私房菜:基础学习篇</td>\n<td>鸟哥</td>\n</tr>\n<tr>\n<td>53</td>\n<td>深入分析Java Web 技术内幕</td>\n<td>许令波</td>\n</tr>\n<tr>\n<td>54</td>\n<td>Effective Java</td>\n<td>[美] 约书亚·布洛克(Joshua Bloch)</td>\n</tr>\n<tr>\n<td>55</td>\n<td>大型网站技术架构</td>\n<td>李智慧</td>\n</tr>\n</tbody>\n</table>\n<h3 id=\"Movie-2\"><a href=\"#Movie-2\" class=\"headerlink\" title=\"Movie\"></a>Movie</h3><table>\n<thead>\n<tr>\n<th>电影名</th>\n<th>导演</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>指环王1:魔戒再现</td>\n<td>彼得·杰克逊</td>\n</tr>\n<tr>\n<td>指环王2:双塔奇兵</td>\n<td>彼得·杰克逊</td>\n</tr>\n<tr>\n<td>指环王3:王者无敌</td>\n<td>彼得·杰克逊</td>\n</tr>\n<tr>\n<td>肖申克的救赎</td>\n<td>弗兰克·德拉邦特</td>\n</tr>\n<tr>\n<td>霸王别姬</td>\n<td>陈凯歌</td>\n</tr>\n<tr>\n<td>阿甘正传</td>\n<td>罗伯特·泽米吉斯</td>\n</tr>\n<tr>\n<td>泰坦尼克号</td>\n<td>詹姆斯·卡梅隆</td>\n</tr>\n<tr>\n<td>忠犬八公的故事</td>\n<td>拉斯·霍尔斯道姆</td>\n</tr>\n<tr>\n<td>机器人总动员</td>\n<td>安德鲁·斯坦顿</td>\n</tr>\n<tr>\n<td>放牛班的春天</td>\n<td>克里斯托夫·巴拉蒂</td>\n</tr>\n<tr>\n<td>疯狂动物城</td>\n<td>拜伦·霍华德 / 瑞奇·摩尔 / 杰拉德·布什</td>\n</tr>\n<tr>\n<td>当幸福来敲门</td>\n<td>加布里埃莱·穆奇诺</td>\n</tr>\n<tr>\n<td>怦然心动</td>\n<td>罗伯·莱纳</td>\n</tr>\n<tr>\n<td>触不可及</td>\n<td>奥利维埃·纳卡什 / 埃里克·托莱达诺</td>\n</tr>\n<tr>\n<td>少年派的奇幻漂流</td>\n<td>李安</td>\n</tr>\n<tr>\n<td>摔跤吧!爸爸</td>\n<td>涅提·蒂瓦里</td>\n</tr>\n<tr>\n<td>飞屋环游记</td>\n<td>彼特·道格特 / 鲍勃·彼德森</td>\n</tr>\n<tr>\n<td>寻梦环游记</td>\n<td>李昂克里奇 / 阿德里安·莫利纳</td>\n</tr>\n<tr>\n<td>让子弹飞</td>\n<td>姜文</td>\n</tr>\n<tr>\n<td>阿凡达</td>\n<td>詹姆斯·卡梅隆</td>\n</tr>\n<tr>\n<td>加勒比海盗1:黑珍珠号的诅咒</td>\n<td>戈尔·维宾斯基</td>\n</tr>\n<tr>\n<td>加勒比海盗2:聚魂棺</td>\n<td>戈尔·维宾斯基</td>\n</tr>\n<tr>\n<td>加勒比海盗3:世界的尽头</td>\n<td>戈尔·维宾斯基</td>\n</tr>\n<tr>\n<td>加勒比海盗4:惊涛怪浪</td>\n<td>罗伯·马歇尔</td>\n</tr>\n<tr>\n<td>加勒比海盗5:死无对证</td>\n<td>艾斯彭·山德伯格 / 乔阿吉姆·罗恩尼</td>\n</tr>\n<tr>\n<td>喜剧之王</td>\n<td>周星驰 / 李力持</td>\n</tr>\n<tr>\n<td>神偷奶爸1</td>\n<td>皮埃尔·柯芬 / 克里斯·雷纳德</td>\n</tr>\n<tr>\n<td>神偷奶爸2</td>\n<td>皮埃尔·柯芬 / 克里斯·雷纳德</td>\n</tr>\n<tr>\n<td>神偷奶爸3</td>\n<td>凯尔·巴尔达 / 皮埃尔·柯芬 / 埃里克·吉隆</td>\n</tr>\n<tr>\n<td>疯狂原始人</td>\n<td>柯克·德·米科 / 克里斯·桑德斯</td>\n</tr>\n<tr>\n<td>小萝莉的猴神大叔</td>\n<td>卡比尔·汗</td>\n</tr>\n<tr>\n<td>你的名字</td>\n<td>新海诚</td>\n</tr>\n<tr>\n<td>秒速5厘米</td>\n<td>新海诚</td>\n</tr>\n<tr>\n<td>言叶之庭</td>\n<td>新海诚</td>\n</tr>\n<tr>\n<td>疯狂的石头</td>\n<td>宁浩</td>\n</tr>\n<tr>\n<td>功夫</td>\n<td>周星驰</td>\n</tr>\n<tr>\n<td>终结者1:未来战士</td>\n<td>詹姆斯·卡梅隆</td>\n</tr>\n<tr>\n<td>终结者2:审判日</td>\n<td>詹姆斯·卡梅隆</td>\n</tr>\n<tr>\n<td>终结者3:机器的觉醒</td>\n<td>乔纳森·莫斯托</td>\n</tr>\n<tr>\n<td>终结者4:救世主</td>\n<td>约瑟夫·麦克金提·尼彻</td>\n</tr>\n<tr>\n<td>终结者5:创世纪</td>\n<td>阿兰·泰勒</td>\n</tr>\n<tr>\n<td>哈利波特1:神秘的魔法石</td>\n<td>克里斯·哥伦布</td>\n</tr>\n<tr>\n<td>哈利波特2:消失的密室</td>\n<td>克里斯·哥伦布</td>\n</tr>\n<tr>\n<td>哈利波特3:阿兹卡班的逃犯</td>\n<td>阿方索·卡隆</td>\n</tr>\n<tr>\n<td>哈利波特4:火杯的考验</td>\n<td>迈克·内威尔</td>\n</tr>\n<tr>\n<td>哈利波特5:凤凰会的密令</td>\n<td>大卫·叶茨</td>\n</tr>\n<tr>\n<td>哈利波特6:混血王子的背叛</td>\n<td>大卫·叶茨</td>\n</tr>\n<tr>\n<td>哈利波特7:死神的圣物1.2</td>\n<td>大卫·叶茨</td>\n</tr>\n<tr>\n<td>爱乐之城</td>\n<td>达米恩·查泽雷</td>\n</tr>\n<tr>\n<td>北京遇上西雅图</td>\n<td>薛晓路</td>\n</tr>\n<tr>\n<td>七月与安生</td>\n<td>曾国祥</td>\n</tr>\n<tr>\n<td>失恋33天</td>\n<td>滕华涛</td>\n</tr>\n<tr>\n<td>夏洛特烦恼</td>\n<td>闫非 / 彭大魔</td>\n</tr>\n<tr>\n<td>后会无期</td>\n<td>韩寒</td>\n</tr>\n<tr>\n<td>乘风破浪</td>\n<td>韩寒</td>\n</tr>\n<tr>\n<td>解忧杂货店</td>\n<td>韩杰</td>\n</tr>\n<tr>\n<td>志明与春娇1</td>\n<td>彭浩翔</td>\n</tr>\n<tr>\n<td>志明与春娇2</td>\n<td>彭浩翔</td>\n</tr>\n<tr>\n<td>春娇救志明</td>\n<td>彭浩翔</td>\n</tr>\n<tr>\n<td>前任攻略</td>\n<td>田羽生</td>\n</tr>\n<tr>\n<td>前任2:备胎反击战</td>\n<td>田羽生</td>\n</tr>\n<tr>\n<td>前任3:再见前任</td>\n<td>田羽生</td>\n</tr>\n<tr>\n<td>我不是药神</td>\n<td>文牧野</td>\n</tr>\n<tr>\n<td>李茶的姑妈</td>\n<td>吴昱翰</td>\n</tr>\n<tr>\n<td>动物世界</td>\n<td>韩延</td>\n</tr>\n<tr>\n<td>邪不压正</td>\n<td>姜文</td>\n</tr>\n<tr>\n<td>影</td>\n<td>张艺谋</td>\n</tr>\n<tr>\n<td>西虹市首富</td>\n<td>闫非 / 彭大魔</td>\n</tr>\n<tr>\n<td>一出好戏</td>\n<td>黄渤</td>\n</tr>\n<tr>\n<td>神奇动物在哪里</td>\n<td>大卫·叶茨</td>\n</tr>\n<tr>\n<td>神奇动物在哪里2:格林德沃之罪</td>\n<td>大卫·叶茨</td>\n</tr>\n<tr>\n<td>悲伤逆流成河</td>\n<td>落落</td>\n</tr>\n<tr>\n<td>狄仁杰之通天帝国</td>\n<td>徐克</td>\n</tr>\n<tr>\n<td>狄仁杰之神都龙王</td>\n<td>徐克</td>\n</tr>\n<tr>\n<td>狄仁杰之四大天王</td>\n<td>徐克</td>\n</tr>\n<tr>\n<td>风语咒</td>\n<td>刘阔</td>\n</tr>\n<tr>\n<td>超时空同居</td>\n<td>苏伦</td>\n</tr>\n<tr>\n<td>摩天营救</td>\n<td>罗森·马歇尔·瑟伯</td>\n</tr>\n<tr>\n<td>后来的我们</td>\n<td>刘若英</td>\n</tr>\n<tr>\n<td>胖子行动队</td>\n<td>包贝尔</td>\n</tr>\n<tr>\n<td>我是江小白 剧场版</td>\n<td>金承仁</td>\n</tr>\n<tr>\n<td>21克拉</td>\n<td>何念</td>\n</tr>\n<tr>\n<td>釜山行</td>\n<td>延尚昊</td>\n</tr>\n<tr>\n<td>头号玩家</td>\n<td>史蒂文·斯皮尔伯格</td>\n</tr>\n<tr>\n<td>毒液:致命守护者</td>\n<td>鲁本·弗雷斯彻</td>\n</tr>\n<tr>\n<td>环太平洋</td>\n<td>吉尔莫·德尔·托罗</td>\n</tr>\n<tr>\n<td>环太平洋:雷霆再起</td>\n<td>斯蒂文·S·迪奈特</td>\n</tr>\n<tr>\n<td>异形</td>\n<td>雷德利·斯科特</td>\n</tr>\n<tr>\n<td>异形2</td>\n<td>詹姆斯·卡梅隆</td>\n</tr>\n<tr>\n<td>异形3</td>\n<td>大卫·芬奇</td>\n</tr>\n<tr>\n<td>异形4</td>\n<td>让-皮埃尔·热内</td>\n</tr>\n<tr>\n<td>异形:契约</td>\n<td>雷德利·斯科特</td>\n</tr>\n<tr>\n<td>狂暴巨兽</td>\n<td>布拉德·佩顿</td>\n</tr>\n<tr>\n<td>爱丽丝梦游仙境</td>\n<td>蒂姆·波顿</td>\n</tr>\n<tr>\n<td>爱丽丝梦游仙境2:镜中奇遇记</td>\n<td>詹姆斯·博宾</td>\n</tr>\n<tr>\n<td>烟花</td>\n<td>新房昭之 / 武内宣之</td>\n</tr>\n<tr>\n<td>战狼</td>\n<td>吴京</td>\n</tr>\n<tr>\n<td>战狼2</td>\n<td>吴京</td>\n</tr>\n<tr>\n<td>红海行动</td>\n<td>林超贤</td>\n</tr>\n<tr>\n<td>奇门遁甲</td>\n<td>袁和平</td>\n</tr>\n<tr>\n<td>巨齿鲨</td>\n<td>乔·德特杜巴</td>\n</tr>\n<tr>\n<td>捉妖记</td>\n<td>许诚毅</td>\n</tr>\n<tr>\n<td>捉妖记2</td>\n<td>许诚毅</td>\n</tr>\n<tr>\n<td>无名之辈</td>\n<td>饶晓志</td>\n</tr>\n<tr>\n<td>羞羞的铁拳</td>\n<td>宋阳 / 张吃鱼</td>\n</tr>\n<tr>\n<td>唐人街探案</td>\n<td>陈思诚</td>\n</tr>\n<tr>\n<td>唐人街探案2</td>\n<td>陈思诚</td>\n</tr>\n<tr>\n<td>起跑线</td>\n<td>萨基特·乔杜里</td>\n</tr>\n<tr>\n<td>二代妖精之今生有幸</td>\n<td>肖洋</td>\n</tr>\n<tr>\n<td>无问西东</td>\n<td>李芳芳</td>\n</tr>\n<tr>\n<td>三生三世十里桃花</td>\n<td>赵小丁 / 安东尼·拉默里纳拉</td>\n</tr>\n<tr>\n<td>逆时营救</td>\n<td>尹鸿承</td>\n</tr>\n<tr>\n<td>那些年,我们一起追的女孩</td>\n<td>九把刀</td>\n</tr>\n<tr>\n<td>小时代</td>\n<td>瞿友宁</td>\n</tr>\n<tr>\n<td>小时代2:青木时代</td>\n<td>郭敬明</td>\n</tr>\n<tr>\n<td>小时代3:刺金时代</td>\n<td>郭敬明</td>\n</tr>\n<tr>\n<td>小时代4:灵魂尽头</td>\n<td>郭敬明</td>\n</tr>\n<tr>\n<td>爵迹</td>\n<td>郭敬明</td>\n</tr>\n<tr>\n<td>既然青春留不住</td>\n<td>田蒙</td>\n</tr>\n<tr>\n<td>我们的十年</td>\n<td>马伟豪 / 刘海</td>\n</tr>\n<tr>\n<td>X战警</td>\n<td>布莱恩·辛格</td>\n</tr>\n<tr>\n<td>X战警2</td>\n<td>布莱恩·辛格</td>\n</tr>\n<tr>\n<td>X战警3:背水一战</td>\n<td>布莱特·拉特纳</td>\n</tr>\n<tr>\n<td>X战警:第一战</td>\n<td>马修·沃恩</td>\n</tr>\n<tr>\n<td>X战警:逆转未来</td>\n<td>布莱恩·辛格</td>\n</tr>\n<tr>\n<td>X战警:天启</td>\n<td>布莱恩·辛格</td>\n</tr>\n<tr>\n<td>金刚狼</td>\n<td>加文·胡德</td>\n</tr>\n<tr>\n<td>金刚狼2</td>\n<td>詹姆斯·曼高德</td>\n</tr>\n<tr>\n<td>金刚狼3:殊死一战</td>\n<td>詹姆斯·曼高德</td>\n</tr>\n<tr>\n<td>僵尸世界大战</td>\n<td>马克·福斯特</td>\n</tr>\n<tr>\n<td>美人鱼</td>\n<td>周星驰</td>\n</tr>\n<tr>\n<td>机器之血</td>\n<td>张立嘉</td>\n</tr>\n<tr>\n<td>无双</td>\n<td>庄文强</td>\n</tr>\n<tr>\n<td>速度与激情</td>\n<td>罗伯·科恩</td>\n</tr>\n<tr>\n<td>速度与激情2</td>\n<td>约翰·辛格顿</td>\n</tr>\n<tr>\n<td>速度与激情3</td>\n<td>林诣彬</td>\n</tr>\n<tr>\n<td>速度与激情4</td>\n<td>林诣彬</td>\n</tr>\n<tr>\n<td>速度与激情5</td>\n<td>林诣彬</td>\n</tr>\n<tr>\n<td>速度与激情6 Furious 6</td>\n<td>林诣彬</td>\n</tr>\n<tr>\n<td>速度与激情7</td>\n<td>温子仁</td>\n</tr>\n<tr>\n<td>速度与激情8</td>\n<td>F·加里·格雷</td>\n</tr>\n<tr>\n<td>金刚:骷髅岛</td>\n<td>乔丹·沃格特-罗伯茨</td>\n</tr>\n<tr>\n<td>青春期</td>\n<td>管晓杰</td>\n</tr>\n<tr>\n<td>青春失乐园</td>\n<td>管晓杰</td>\n</tr>\n<tr>\n<td>青春期3</td>\n<td>管晓杰</td>\n</tr>\n<tr>\n<td>恐怖直播</td>\n<td>金秉祐</td>\n</tr>\n<tr>\n<td>生化危机</td>\n<td>保罗·安德森</td>\n</tr>\n<tr>\n<td>生化危机2:启示录</td>\n<td>亚历山大·维特</td>\n</tr>\n<tr>\n<td>生化危机3:灭绝</td>\n<td>拉塞尔·穆卡希</td>\n</tr>\n<tr>\n<td>生化危机4:战神再生</td>\n<td>保罗·安德森</td>\n</tr>\n<tr>\n<td>生化危机5:惩罚</td>\n<td>保罗·安德森</td>\n</tr>\n<tr>\n<td>生化危机6:终章</td>\n<td>保罗·安德森</td>\n</tr>\n<tr>\n<td>变形金刚</td>\n<td>迈克尔·贝</td>\n</tr>\n<tr>\n<td>变形金刚2</td>\n<td>迈克尔·贝</td>\n</tr>\n<tr>\n<td>变形金刚3</td>\n<td>迈克尔·贝</td>\n</tr>\n<tr>\n<td>变形金刚4:绝迹重生</td>\n<td>迈克尔·贝</td>\n</tr>\n<tr>\n<td>变形金刚5:最后的骑士</td>\n<td>迈克尔·贝</td>\n</tr>\n<tr>\n<td>摩天楼</td>\n<td>金志勋</td>\n</tr>\n<tr>\n<td>海扁王</td>\n<td>马修·沃恩</td>\n</tr>\n<tr>\n<td>海扁王2</td>\n<td>杰夫·瓦德洛</td>\n</tr>\n<tr>\n<td>新乌龙院之笑闹江湖</td>\n<td>朱延平</td>\n</tr>\n<tr>\n<td>爱情公寓电影版</td>\n<td>韦正</td>\n</tr>\n<tr>\n<td>比得兔</td>\n<td>威尔·古勒</td>\n</tr>\n<tr>\n<td>厕所英雄</td>\n<td>什里·那拉扬·辛</td>\n</tr>\n<tr>\n<td>芳华</td>\n<td>冯小刚</td>\n</tr>\n<tr>\n<td>超能陆战队</td>\n<td>Don Hall 克里斯·威廉</td>\n</tr>\n<tr>\n<td>死神来了</td>\n<td>黄毅瑜</td>\n</tr>\n<tr>\n<td>死神来了2</td>\n<td>大卫·R·艾里斯</td>\n</tr>\n<tr>\n<td>死神来了3</td>\n<td>黄毅瑜</td>\n</tr>\n<tr>\n<td>死神来了4</td>\n<td>大卫·R·艾里斯</td>\n</tr>\n<tr>\n<td>死神来了5</td>\n<td>史蒂文·奎里</td>\n</tr>\n<tr>\n<td>巨额来电</td>\n<td>彭顺</td>\n</tr>\n<tr>\n<td>功守道</td>\n<td>文章</td>\n</tr>\n<tr>\n<td>明月几时有</td>\n<td>许鞍华</td>\n</tr>\n<tr>\n<td>声之形</td>\n<td>山田尚子</td>\n</tr>\n<tr>\n<td>死亡笔记</td>\n<td>亚当·温加德</td>\n</tr>\n<tr>\n<td>死亡笔记:最后的名字</td>\n<td>金子修介</td>\n</tr>\n<tr>\n<td>死亡笔记:L改变世界</td>\n<td>中田秀夫</td>\n</tr>\n<tr>\n<td>拆弹专家</td>\n<td>邱礼涛</td>\n</tr>\n<tr>\n<td>傲娇与偏见</td>\n<td>李海蜀 / 黄彦威</td>\n</tr>\n<tr>\n<td>魔兽</td>\n<td>邓肯·琼斯</td>\n</tr>\n<tr>\n<td>正义联盟</td>\n<td>扎克·施奈德</td>\n</tr>\n<tr>\n<td>饥饿游戏</td>\n<td>盖瑞·罗斯</td>\n</tr>\n<tr>\n<td>饥饿游戏2:星火燎原</td>\n<td>弗朗西斯·劳伦斯</td>\n</tr>\n<tr>\n<td>饥饿游戏3:嘲笑鸟(上)</td>\n<td>弗朗西斯·劳伦斯</td>\n</tr>\n<tr>\n<td>饥饿游戏3:嘲笑鸟(下)</td>\n<td>弗朗西斯·劳伦斯</td>\n</tr>\n<tr>\n<td>暮光之城</td>\n<td>凯瑟琳·哈德威克</td>\n</tr>\n<tr>\n<td>暮光之城2:新月</td>\n<td>克里斯·韦兹</td>\n</tr>\n<tr>\n<td>暮光之城3:月食</td>\n<td>大卫·斯雷德</td>\n</tr>\n<tr>\n<td>暮光之城4:破晓(上)</td>\n<td>比尔·康顿</td>\n</tr>\n<tr>\n<td>暮光之城4:破晓(下)</td>\n<td>比尔·康顿</td>\n</tr>\n<tr>\n<td>普罗米修斯</td>\n<td>雷德利·斯科特</td>\n</tr>\n<tr>\n<td>穿靴子的猫</td>\n<td>克里斯·米勒</td>\n</tr>\n<tr>\n<td>穿靴子的猫:萌猫三剑客</td>\n<td>许诚毅</td>\n</tr>\n<tr>\n<td>雷神</td>\n<td>肯尼思·布拉纳</td>\n</tr>\n<tr>\n<td>雷神2:黑暗世界</td>\n<td>阿兰·泰勒</td>\n</tr>\n<tr>\n<td>雷神3:诸神黄昏</td>\n<td>伊加·维迪提</td>\n</tr>\n<tr>\n<td>复仇者联盟</td>\n<td>乔斯·韦登</td>\n</tr>\n<tr>\n<td>复仇者联盟2:奥创纪元</td>\n<td>乔斯·韦登</td>\n</tr>\n<tr>\n<td>复仇者联盟3:无限战争</td>\n<td>安东尼·罗素 / 乔·罗素</td>\n</tr>\n<tr>\n<td>奇异博士</td>\n<td>斯科特·德瑞克森</td>\n</tr>\n<tr>\n<td>银河护卫队</td>\n<td>詹姆斯·古恩</td>\n</tr>\n<tr>\n<td>银河护卫队2</td>\n<td>詹姆斯·古恩</td>\n</tr>\n<tr>\n<td>绿巨人</td>\n<td>李安</td>\n</tr>\n<tr>\n<td>无敌浩克</td>\n<td>路易斯·莱特里尔</td>\n</tr>\n<tr>\n<td>愤怒的小鸟</td>\n<td>费格尔·雷利</td>\n</tr>\n<tr>\n<td>流感</td>\n<td>金成洙</td>\n</tr>\n<tr>\n<td>汉江怪物</td>\n<td>奉俊昊</td>\n</tr>\n<tr>\n<td>铁线虫入侵</td>\n<td>朴正宇</td>\n</tr>\n<tr>\n<td>海云台</td>\n<td>尹济均</td>\n</tr>\n<tr>\n<td>摩天楼</td>\n<td>金志勋</td>\n</tr>\n<tr>\n<td>潘多拉</td>\n<td>朴正祐</td>\n</tr>\n<tr>\n<td>隧道</td>\n<td>金成勋</td>\n</tr>\n<tr>\n<td>我是传奇</td>\n<td>弗朗西斯·劳伦斯</td>\n</tr>\n</tbody>\n</table>\n<h2 id=\"动漫\"><a href=\"#动漫\" class=\"headerlink\" title=\"动漫\"></a>动漫</h2><blockquote>\n<p>追动漫,大概是这代人矢志不渝的使命 O(∩_∩)O</p>\n</blockquote>\n<h3 id=\"Anime\"><a href=\"#Anime\" class=\"headerlink\" title=\"Anime\"></a>Anime</h3><table>\n<thead>\n<tr>\n<th>动漫</th>\n<th>出品</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>画江湖之灵主</td>\n<td>若森数字</td>\n</tr>\n<tr>\n<td>画江湖之不良人</td>\n<td>若森数字</td>\n</tr>\n<tr>\n<td>画江湖之杯莫停</td>\n<td>若森数字</td>\n</tr>\n<tr>\n<td>画江湖之换世门生</td>\n<td>若森数字</td>\n</tr>\n<tr>\n<td>秦时明月</td>\n<td>玄机科技</td>\n</tr>\n<tr>\n<td>天行九歌</td>\n<td>玄机科技</td>\n</tr>\n<tr>\n<td>武庚纪</td>\n<td>玄机科技</td>\n</tr>\n<tr>\n<td>天谕</td>\n<td>玄机科技</td>\n</tr>\n<tr>\n<td>斗罗大陆</td>\n<td>玄机科技</td>\n</tr>\n<tr>\n<td>我是江小白</td>\n<td>两点十分</td>\n</tr>\n<tr>\n<td>雪鹰领主</td>\n<td>企鹅影视</td>\n</tr>\n<tr>\n<td>星辰变</td>\n<td>腾讯阅文</td>\n</tr>\n<tr>\n<td>斗破苍穹</td>\n<td>腾讯阅文</td>\n</tr>\n<tr>\n<td>武动乾坤</td>\n<td>腾讯阅文</td>\n</tr>\n<tr>\n<td>少年歌行</td>\n<td>中影年年</td>\n</tr>\n<tr>\n<td>万界神主</td>\n<td>若鸿文化</td>\n</tr>\n<tr>\n<td>万界仙踪</td>\n<td>若鸿文化</td>\n</tr>\n<tr>\n<td>少年锦衣卫</td>\n<td>柏言映画</td>\n</tr>\n<tr>\n<td>太乙仙魔录之灵飞纪</td>\n<td>糖心文化</td>\n</tr>\n<tr>\n<td>游戏王</td>\n<td>日本动漫</td>\n</tr>\n<tr>\n<td>拳皇命运</td>\n<td>龙族动漫</td>\n</tr>\n</tbody>\n</table>\n"}],"posts":[{"title":"Spring Boot 内嵌 Web 容器介绍及常见运行参数","slug":"Spring Boot 内嵌 Web 容器介绍及常见运行参数","date":"2019-05-15","updated":"2019-06-02","comments":true,"path":"spring-boot-container-configuration-example.html","link":"","permalink":"https://blog.mariojd.cn/spring-boot-container-configuration-example.html","excerpt":"","keywords":"","text":"Spring Boot 支持的内嵌容器有 Tomcat(默认) 、Jetty 、Undertow 和 Reactor Netty (v2.0+), 借助可插拔(SPI)机制的实现,开发者可以轻松进行容器间的切换。 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <!-- 排除默认的Tomcat依赖 --> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <!-- 引入Jetty 或 Undertow 或 Reactor Netty --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jetty</artifactId> </dependency> <!-- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-reactor-netty</artifactId> </dependency> --> </dependencies> Spring Boot 配置有很多,具体可以参考这里。下面列出一些常见的、与容器相关的配置: 地址: server.port=8080 # Server HTTP port.server.address= # Network address to which the server should bind. 压缩: server.compression.enabled=false # Whether response compression is enabled.server.compression.excluded-user-agents= # Comma-separated list of user agents for which responses should not be compressed.server.compression.mime-types=text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json,application/xml # Comma-separated list of MIME types that should be compressed.server.compression.min-response-size=2KB # Minimum "Content-Length" value that is required for compression to be performed. 错误: server.error.include-exception=false # Include the "exception" attribute.server.error.include-stacktrace=never # When to include a "stacktrace" attribute.server.error.path=/error # Path of the error controller.server.error.whitelabel.enabled=true # Whether to enable the default error page displayed in browsers in case of a server error. 其他: server.connection-timeout= # Time that connectors wait for another HTTP request before closing the connection. When not set, the connector's container-specific default is used. Use a value of -1 to indicate no (that is, an infinite) timeout.server.http2.enabled=false # Whether to enable HTTP/2 support, if the current environment supports it.server.max-http-header-size=8KB # Maximum size of the HTTP message header. 我们还可以借助编程的方式来修改这些配置项,即通过实现 WebServerFactoryCustomizer<T> 接口方法: 压缩配置 @SpringBootApplicationpublic class SpringBootConfigurationApplication implements WebServerFactoryCustomizer<JettyServletWebServerFactory> { public static void main(String[] args) { SpringApplication.run(SpringBootConfigurationApplication.class, args); } @Override public void customize(JettyServletWebServerFactory factory) { Compression compression = new Compression(); compression.setEnabled(true); compression.setMinResponseSize(DataSize.ofBytes(512)); factory.setCompression(compression); }}","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"spring boot","slug":"spring-boot","permalink":"https://blog.mariojd.cn/tags/spring-boot/"}]},{"title":"Spring Boot 使用 Druid 连接池","slug":"Spring Boot 使用 Druid 连接池","date":"2019-04-15","updated":"2019-04-16","comments":true,"path":"druid-spring-boot-example.html","link":"","permalink":"https://blog.mariojd.cn/druid-spring-boot-example.html","excerpt":"","keywords":"","text":"简介Spring Boot 1.x 版本中,默认使用的数据库连接池为:Tomcat JDBC;到了 Spring Boot 2.x,也切换到了更高性能的 HikariCP 连接池。 不过上面这两个都不是今天的重点,下面介绍的是国内较为流行的 Druid ,一款为监控而生的数据库连接池,由阿里巴巴数据库事业部出品。Druid 连接池内置了强大的监控功能,该特性不影响性能。功能强大,能防止 SQL 注入,内置 Logging 能诊断 hack 应用行为。 早期使用 Druid 时候还得配合着 Spring 来使用,一堆的 XML 配置文件,那可真叫是非常的不便。好在,Spring Boot 的全面推广,使得 Web 开发显得越来越高效简单,各种自带的、第三方的 Starter 也随之而生,约定大于配置,这不仅仅是简化了开发人员的上手复杂度,更是让整个体系走向越来越自动化、智能化。 使用Druid 官方同样提供了相应的 Spring Boot Starter ,旨在帮助开发者在 Spring Boot 项目中轻松集成 Druid 数据库连接池和监控。 加入依赖 <dependencies> <!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter --><!-- 引入 Druid , 排除 HikariCP --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.16</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <exclusions> <exclusion> <artifactId>HikariCP</artifactId> <groupId>com.zaxxer</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> 添加配置 spring: jpa: open-in-view: false datasource: druid: ##### JDBC 配置(内嵌数据库可省略) # # 或spring.datasource.url= # url: # # 或spring.datasource.username= # username: # # 或spring.datasource.password= # password: # # 或spring.datasource.driver-class-name= # driver-class-name: ##### 连接池配置 min-idle: 5 max-active: 5 max-wait: 5000 initial-size: 5 validation-query: \"select 'x'\" test-while-idle: true filters: conn,config,stat,wall,slf4j ##### 监控配置 web-stat-filter: enabled: true stat-view-servlet: enabled: true # http://localhost:8080/druid/login.html login-username: admin login-password: admin 启动 Application 访问 http://localhost:8080/druid/login.html ,输入登录用户名和密码 admin,就可以查看 Druid 提供的所有监控功能。 更多资料可参考下方链接 Druid wikiDruid Spring Boot Starter 示例源码欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"spring boot","slug":"spring-boot","permalink":"https://blog.mariojd.cn/tags/spring-boot/"},{"name":"druid","slug":"druid","permalink":"https://blog.mariojd.cn/tags/druid/"}]},{"title":"还将密码明文写在配置文件?试试 Jasypt Spring Boot","slug":"还将密码明文写在配置文件?试试 Jasypt Spring Boot","date":"2019-04-13","updated":"2019-04-13","comments":true,"path":"jasypt-spring-boot-example.html","link":"","permalink":"https://blog.mariojd.cn/jasypt-spring-boot-example.html","excerpt":"","keywords":"","text":"你是否还是这样,简单粗暴的把数据库用户名、密码等敏感信息写在配置文件中?那你又是否曾经考虑过其中的安全性问题? spring: datasource: url: jdbc:mysql://localhost:3306/test?useUnicode=true&useSSL=false username: root password: 123456 如果有的话,那下面来看看,如何通过使用 Jasypt Spring Boot ,以更加优雅的方式来规避这种操作。 相关依赖 <dependencies> <!-- https://mvnrepository.com/artifact/com.github.ulisesbocchio/jasypt-spring-boot-starter --> <dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> 完善配置 spring: datasource: url: jdbc:mysql://localhost:3306/test?useUnicode=true&zeroDateTimeBehavior=convertToNull&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8&tinyInt1isBit=false # 对应用户名 root ,密码 123456 username: ENC[KHRM9dKY8KykzzYbt8rRZQ==] password: ENC[RWmQMxlcukotJAb36PrKSA==]jasypt: encryptor: # 任意的随机字符串均可 password: SBPstLlrFzXW01Okb62R95qvpj4J83Dn property: # 自定义属性规则,默认前缀是“ENC(”,后缀为“)” prefix: \"ENC[\" suffix: \"]\" 留意到上面这段配置的用户名和密码是 ENC[xxx] 这种格式的,其中 ENC[] 是自定义配置的,这也是 Jasypt 能正常识别待解密数据的规则,那其中的加密串又是从哪来的呢? 当然是运算出来的。最简单的配置,开发者只需要再补充完 jasypt.encryptor.password=xxx 属性即可(同上,还支持使用 DER、PEM 这种证书的 private/public keys 加解密方式),具体的生成代码在下方: @Slf4j@SpringBootApplication@EnableEncryptablePropertiespublic class JasyptSpringBootApplication { public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(JasyptSpringBootApplication.class, args); JasyptSpringBootApplication application = context.getBean(JasyptSpringBootApplication.class); // 这里可以将明文(用户名、密码)转换成相应密文 application.jasypt(\"root\"); application.jasypt(\"123456\"); // 不过程序最后还是通过明文信息进行数据库连接 HikariDataSource hikariDataSource = (HikariDataSource) context.getBean(DataSource.class); log.info(\"DB username: {} , password: {}\", hikariDataSource.getUsername(), hikariDataSource.getUsername()); } @Resource private StringEncryptor stringEncryptor; public void jasypt(String text) { // 即使是相同明文,但这里每次生成的都是不同的密文 String encryptedText = stringEncryptor.encrypt(text.trim()); String decryptedText = stringEncryptor.decrypt(encryptedText); log.info(\"ORIGINAL: {} ; ENCRYPTED: {} ; DECRYPTED: {}\", text, encryptedText, decryptedText); }} 相关链接jasypt-spring-bootjasypt-spring-boot-samples 示例源码欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"spring boot","slug":"spring-boot","permalink":"https://blog.mariojd.cn/tags/spring-boot/"},{"name":"jasypt spring boot","slug":"jasypt-spring-boot","permalink":"https://blog.mariojd.cn/tags/jasypt-spring-boot/"}]},{"title":"Spring Boot Application 监控管理利器: Spring Boot Admin","slug":"Spring Boot Application 监控管理利器: Spring Boot Admin","date":"2019-04-12","updated":"2019-04-12","comments":true,"path":"spring-boot-admin-example.html","link":"","permalink":"https://blog.mariojd.cn/spring-boot-admin-example.html","excerpt":"","keywords":"","text":"文章前言上篇文章了解了 Spring Boot Actuator,引入后即可通过访问不同的端点,来获得相应的监控信息。 对应 HTTP 方式请求,返回的数据都是 JSON 格式,这对于运维或是其他人员来说当然不是很方便直观,特别是当需要监控的应用越来越多时,如果还依旧通过地址栏来逐个访问,就显得过于繁琐和低效了。下面,我们再来认识下 Spring Boot Admin 这个 Spring Boot Application UI 监控管理工具。 快速上手Spring Boot Admin 由 Server 和 Client 两个端组成,其中 Client 端通常是需要被监控的应用。先来配置下 Server 端的依赖,考虑到安全性方面的问题,这里还额外加入了 Security: <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-server</artifactId> </dependency></dependencies> 接着在 application.yml 文件中配置登录用户及密码: spring: security: user: name: admin password: admin 必要的 Security 配置: @Configurationpublic class SecuritySecureConfig extends WebSecurityConfigurerAdapter { private final String adminContextPath; public SecuritySecureConfig(AdminServerProperties adminServerProperties) { this.adminContextPath = adminServerProperties.getContextPath(); } @Override protected void configure(HttpSecurity http) throws Exception { SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); // 设置redirectTo参数和登录成功重定向地址 successHandler.setTargetUrlParameter(\"redirectTo\"); successHandler.setDefaultTargetUrl(adminContextPath + \"/\"); http.authorizeRequests() // 授予对静态资源和登录页面的公共访问权 .antMatchers(adminContextPath + \"/assets/**\").permitAll() .antMatchers(adminContextPath + \"/login\").permitAll() // 除上面配置的其他请求都必须经过身份验证 .anyRequest().authenticated() .and() // 配置登录和退出请求路径 .formLogin().loginPage(adminContextPath + \"/login\").successHandler(successHandler).and() .logout().logoutUrl(adminContextPath + \"/logout\").and() // 启用HTTP-Basic,这是Spring Boot Admin Client注册所必需的 .httpBasic().and() .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .ignoringAntMatchers( adminContextPath + \"/instances\", adminContextPath + \"/actuator/**\" ); } } 最后,在启动类上加入 @EnableAdminServer ,运行服务后访问:http://localhost:8080 ,输入用户名密码 admin 进入系统: @EnableAdminServer@SpringBootApplicationpublic class SpringBootAdminServerApplication { public static void main(String[] args) { SpringApplication.run(SpringBootAdminServerApplication.class, args); }} Server 端到此就搭建好了,下面再来处理 Client 端服务,同样的先加入相关依赖: <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency></dependencies> 由于引入了 Security 模块,为了可以正常访问到 Actuator 的 Endpoints,这里还需要做相应的配置处理: @Configurationpublic class SecurityPermitAllConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().permitAll() .and().csrf().ignoringAntMatchers(\"/actuator/**\").disable(); }} 在配置文件中加入必要信息: server: port: 8081spring: application: name: spring-boot-admin-client boot: admin: client: # Spring Boot Admin Server 服务地址,可配置多个 url: http://localhost:8080 instance: name: ${spring.application.name} prefer-ip: true # Spring Boot Admin Server 认证信息 username: admin password: admin # 设置为true,客户端将只注册1个 Admin Server 服务(按定义的顺序) # 当该 Admin Server 服务宕机,将自动注册下个 Admin Server 服务器; # 如果为false,将针对所有管理服务器进行注册 register-once: true# Actuator 配置management: endpoints: web: exposure: include: \"*\" endpoint: health: show-details: alwaysinfo: version: @project.version@ name: @project.artifactId@ author: happyJared blog: https://blog.mariojd.cn/ 最后,启动 Client 服务,我们来查看 Admin Server 提供的 UI 监控管理系统: 点击 Instances 进入,这时就可以方便的查看该应用的所有监控状态信息。 此外,Spring Boot Admin 还支持动态更改 Logger Level、提供异常监控告警等功能,这些姿势可以自行解锁。 参考阅读Spring Boot AdminSpring Boot Admin Reference Guide监控管理之Spring Boot Admin使用 示例源码欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"spring boot","slug":"spring-boot","permalink":"https://blog.mariojd.cn/tags/spring-boot/"},{"name":"spring boot admin","slug":"spring-boot-admin","permalink":"https://blog.mariojd.cn/tags/spring-boot-admin/"}]},{"title":"Spring Boot Application 监控利器: Spring Boot Actuator","slug":"Spring Boot Application 监控利器: Spring Boot Actuator","date":"2019-04-11","updated":"2019-04-12","comments":true,"path":"spring-boot-actuator-example.html","link":"","permalink":"https://blog.mariojd.cn/spring-boot-actuator-example.html","excerpt":"","keywords":"","text":"前言接触和使用 Spring Boot,当然要知道 Spring Boot 的四大核心,包括: Auto Configuration(自动配置) Starter Dependency(启动依赖) Spring Boot CLI (Command-Line Interface,命令行界面) Spring Boot Actuator(监控利器) 其中,Actuator 的出现,更是帮助了开发者可以更轻松的监控并管理应用程序。 具体到 Spring Boot Application 中,只需要简单的引入 spring-boot-starter-actuator 依赖,配置开启相应的 Endpoint ,我们就可以通过 HTTP 或 JMX 的方式来访问这些暴露端点,以获取具体的监控信息: 相关依赖 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency></dependencies> Endpoints从 Spring Boot 2.0 起,默认 Actuator 的基本访问路径带上了 /actuator ,且 HTTP 监控仅开放了 health 和 info 两个 Endpoint ,在 application.yml 配置文件中可以更改这些配置: 配置信息 spring: application: name: spring-boot-actuator# Actuator 配置management: server: address: localhost port: 8080 endpoint: health: show-details: always shutdown: enabled: true endpoints: enabled-by-default: true web: exposure: include: \"*\" exclude: - auditevents - caches jmx: exposure: include: - info - health exclude: configprops 在应用启动之后,通过访问 http://localhost:8080/actuator 来查看 Actuator 提供的这些监控功能: 下图列出了一些常用的 Endpoint ,HTTP 访问的方式为: http://localhost:8080/actuator/${ID}: Health Indicator Health Indicator 是 Spring Boot Actuator 中最常用的功能之一,通过访问 /actuator/health 端点,可以获取应用、磁盘、中间件等服务的状态和信息。 在 Spring Boot 中,是通过 HealthIndicatorRegistry 来收集信息的,而 HealthIndicator 用于实现具体的健康检查逻辑,目前已支持内置的 HealthIndicator 清单已包含如下: 举个例子,想通过 Actuator 来查看 Redis 的状态,只需要补充相应的依赖及配置即可: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency> spring: redis: url: redis://localhost:6379 Custom幸运的是,Spring Boot Actuator 还允许开发者们自定义内容,包括 Endpoint、HealthIndicator和InfoContributor,有兴趣的可以自行看看文档资料。 MetricsMetrics 也是 Actuator 提供支持的特色功能之一。简单来说,就是通过 Micrometer 门面把应用的一些状态指标等分发到其它监测系统中: 如上图所示,这里以 Elastic 为例,看看具体如何使用这项功能,先补充相关依赖: <!-- https://mvnrepository.com/artifact/io.micrometer/micrometer-registry-elastic --><dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-elastic</artifactId> <version>1.1.4</version></dependency> 接着完善相关配置: management: metrics: export: elastic: enabled: true # Elasticsearch 服务端口 host: http://localhost:9200 启动服务,留意到控制台输出日志中有这么行信息: 等待1分钟后,借助 Head 插件可以看到相应的采集信息: 参考阅读Spring Boot ActuatorSpring Boot Actuator: IntroductionSpring Boot Actuator: EndpointsSpring Boot Actuator: MetricsMicrometer Elastic 示例源码欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"spring boot","slug":"spring-boot","permalink":"https://blog.mariojd.cn/tags/spring-boot/"},{"name":"spring boot actuator","slug":"spring-boot-actuator","permalink":"https://blog.mariojd.cn/tags/spring-boot-actuator/"}]},{"title":"Alibaba 开源通用缓存访问框架:JetCache","slug":"Alibaba 开源通用缓存访问框架:JetCache","date":"2019-04-09","updated":"2019-04-10","comments":true,"path":"spring-boot-jetcache-example.html","link":"","permalink":"https://blog.mariojd.cn/spring-boot-jetcache-example.html","excerpt":"","keywords":"","text":"IntroductionJetCache 是由阿里巴巴开源的一款通用缓存访问框架。上篇文章介绍过了 Spring Cache 的基本使用,下面我们再来了解下这款更好用的 JetCache。 引用下官方文档说明,JetCache 提供的核心能力包括: 提供统一的,类似jsr-107风格的API访问Cache,并可通过注解创建并配置Cache实例 通过注解实现声明式的方法缓存,支持TTL和两级缓存 分布式缓存自动刷新,分布式锁 (2.2+) 支持异步Cache API Spring Boot支持 Key的生成策略和Value的序列化策略是可以定制的 针对所有Cache实例和方法缓存的自动统计 目前支持的缓存系统包括以下4个: Caffeine(基于本地内存) LinkedHashMap(基于本地内存,JetCache自己实现的简易LRU缓存) Alibaba Tair(相关实现未在Github开源,在阿里内部Gitlab上可以找到) Redis 来看个简单的使用示例: public interface UserService { @Cached(name=\"user\", key=\"#userId\", expire = 3600, cacheType = CacheType.REMOTE) @CacheRefresh(refresh = 1800, stopRefreshAfterLastAccess = 3600, timeUnit = TimeUnit.SECONDS) @CachePenetrationProtect User getById(long userId); } 是不是和 Spring Cache 很像,不过这里原生支持了细粒度的 TTL(超时时间,而 Spring Cache Redis 默认只支持配置全局的),CacheType 还有 LOCAL/REMOTE/BOTH 三种选择, 分别代表本地内存/远程 Cache Server(如Redis)/两级缓存。下面,结合 Redis 的使用,来看看更复杂的一些使用场景: Example首先,使用 JetCache 的环境需求包括如下: JDK:必须 Java 8+ Spring Framework:v4.0.8 以上,如果不使用注解就不需要 Spring Boot:v1.1.9 以上(可选) 依赖部分,示例中使用的是基于 Lettuce Redis 客户端的 Spring Boot 方式的 Starter <dependencies> <!-- https://mvnrepository.com/artifact/com.alicp.jetcache/jetcache-redis --> <dependency> <groupId>com.alicp.jetcache</groupId> <artifactId>jetcache-starter-redis-lettuce</artifactId> <version>2.6.0.M1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency></dependencies> 插件这部分的配置是必需的,参考下方引用: <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.7.0</version> <configuration> <source>1.8</source> <target>1.8</target> <compilerArgument>-parameters</compilerArgument> </configuration> </plugin> </plugins></build> @Cached的key、condition等表达式中使用参数名以后缓存没有生效? 全局配置 spring: jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: create # 开启 SQL 输出,方便查看结果是否走了缓存 show-sql: true# @see com.alicp.jetcache.autoconfigure.JetCachePropertiesjetcache: # 统计间隔,默认0:表示不统计 statIntervalMinutes: 1 # areaName是否作为缓存key前缀,默认True areaInCacheName: false local: default: # 已支持可选:linkedhashmap、caffeine type: linkedhashmap # key转换器的全局配置,当前只有:fastjson, @see com.alicp.jetcache.support.FastjsonKeyConvertor keyConvertor: fastjson # 每个缓存实例的最大元素的全局配置,仅local类型的缓存需要指定 limit: 100 # jetcache2.2以上,以毫秒为单位,指定多长时间没有访问,就让缓存失效,当前只有本地缓存支持。0表示不使用这个功能 expireAfterAccessInMillis: 30000 remote: default: # 已支持可选:redis、tair type: redis.lettuce # 连接格式@see:https://github.com/lettuce-io/lettuce-core/wiki/Redis-URI-and-connection-details uri: redis://localhost:6379/1?timeout=5s keyConvertor: fastjson # 序列化器的全局配置。仅remote类型的缓存需要指定,可选java和kryo valueEncoder: java valueDecoder: java # 以毫秒为单位指定超时时间的全局配置 expireAfterWriteInMillis: 5000 编写实体 @Data@Builder@NoArgsConstructor@AllArgsConstructor@Entitypublic class Coffee implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String name; private Float price;} public interface CoffeeRepository extends JpaRepository<Coffee, Integer> {} JetCache 提供了两种实现缓存的方式,可以通过 @CreateCache 注解创建 Cache 实例,这种灵活性比较高;其次是通过纯注解的方式来实现缓存。 4.1 通过 @CreateCache 注解创建 Cache 实例 @Slf4j@Servicepublic class CoffeeCreateCacheService { private static final String CACHE_NAME = \"CoffeeCreateCache:\"; @Resource private CoffeeRepository coffeeRepository; /** * 使用 @CreateCache 注解创建Cache实例; * 未定义默认值的参数,将使用yml中指定的全局配置; * 缓存在 Local,也可以配置成 both 开启两级缓存 */ @CreateCache(name = CACHE_NAME, expire = 1, localLimit = 10, timeUnit = TimeUnit.MINUTES, cacheType = CacheType.LOCAL) private Cache<Integer, Coffee> coffeeCache; @Transactional public void add(Coffee coffee) { coffeeRepository.save(coffee); coffeeCache.put(coffee.getId(), coffee, 3, TimeUnit.SECONDS); } public Optional<Coffee> get(int id) { Coffee coffee = coffeeCache.get(id); log.info(\"CoffeeCreateCache get {} res {}\", id, coffee); if (Objects.isNull(coffee)) { Optional<Coffee> coffeeOptional = coffeeRepository.findById(id); if (coffeeOptional.isPresent()) { coffee = coffeeOptional.get(); boolean res = coffeeCache.putIfAbsent(id, coffee); log.info(\"CoffeeCreateCache putIfAbsent {} res {}\", id, res); } } return Optional.ofNullable(coffee); } @Transactional public Coffee update(Coffee coffee) { Coffee c = coffeeRepository.save(coffee); coffeeCache.put(c.getId(), c, 60, TimeUnit.SECONDS); return c; } @Transactional public void delete(int id) { coffeeRepository.deleteById(id); boolean res = coffeeCache.remove(id); log.info(\"CoffeeCreateCache delete {} res {}\", id, res); }} 4.2 通过注解实现方法缓存,主要是:@Cached(缓存新增)、@CacheUpdate(缓存更新)、@CacheInvalidate(缓存删除),还有用于配置自动刷新和加载保护的“大杀器”:@CacheRefresh、@CachePenetrationProtect @Slf4j@Servicepublic class CoffeeMethodCacheService { private static final String CACHE_NAME = \"CoffeeMethodCache:\"; @Resource private CoffeeRepository coffeeRepository; @Transactional public Coffee add(Coffee coffee) { return coffeeRepository.save(coffee); } /** * 缓存在 Remote 的 Redis,也可以配置成 both 开启两级缓存 */ @Cached(name = CACHE_NAME, key = \"#id\", cacheType = CacheType.REMOTE, serialPolicy = SerialPolicy.KRYO, condition = \"#id>0\", postCondition = \"result!=null\") public Coffee get(int id) { return coffeeRepository.findById(id).orElse(null); } @CacheUpdate(name = CACHE_NAME, key = \"#coffee.id\", value = \"result\", condition = \"#coffee.id!=null\") @Transactional public Coffee update(Coffee coffee) { return coffeeRepository.save(coffee); } @CacheInvalidate(name = CACHE_NAME, key = \"#id\") @Transactional public void delete(int id) { coffeeRepository.deleteById(id); }} 简单测试 /** * 这里 @EnableMethodCache,@EnableCreateCacheAnnotation 分别用于激活 @Cached 和 @CreateCache 注解 */@Slf4j@EnableMethodCache(basePackages = \"cn.mariojd.jetcache\")@EnableCreateCacheAnnotation@SpringBootApplicationpublic class SpirngBootJetcacheApplication implements ApplicationRunner { public static void main(String[] args) { new SpringApplicationBuilder() .sources(SpirngBootJetcacheApplication.class) .bannerMode(Banner.Mode.OFF) .web(WebApplicationType.NONE) .run(args); } @Resource private CoffeeCreateCacheService coffeeCreateCacheService; @Resource private CoffeeMethodCacheService coffeeMethodCacheService; @Override public void run(ApplicationArguments args) throws InterruptedException { // Test Coffee create cache Coffee latte = Coffee.builder().name(\"Latte\").price(20.0f).build(); coffeeCreateCacheService.add(latte); log.info(\"Reading from cache... {}\", coffeeCreateCacheService.get(latte.getId())); TimeUnit.SECONDS.sleep(3); log.info(\"Cache expire... \"); coffeeCreateCacheService.get(latte.getId()); latte.setPrice(25.0f); latte = coffeeCreateCacheService.update(latte); coffeeCreateCacheService.delete(latte.getId()); // Test Coffee method cache Coffee cappuccino = Coffee.builder().name(\"Cappuccino\").price(30.0f).build(); coffeeMethodCacheService.add(cappuccino); coffeeMethodCacheService.get(cappuccino.getId()); log.info(\"Reading from cache... {}\", coffeeMethodCacheService.get(cappuccino.getId())); cappuccino.setPrice(25.0f); cappuccino = coffeeMethodCacheService.update(cappuccino); coffeeMethodCacheService.delete(cappuccino.getId()); } } 具体的可以自行运行测试,最后来看看 Redis 的 Monitor,图中红框部分说明该查询走了缓存: 参考阅读最后,如果在使用中遇到问题,在下方的参考阅读中可以寻求答案。 JetCache wikiJetCache introduce 示例源码欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"redis","slug":"redis","permalink":"https://blog.mariojd.cn/tags/redis/"},{"name":"spring boot","slug":"spring-boot","permalink":"https://blog.mariojd.cn/tags/spring-boot/"},{"name":"jetcache","slug":"jetcache","permalink":"https://blog.mariojd.cn/tags/jetcache/"}]},{"title":"为 Spring Boot 应用添加 Redis Caching","slug":"为 Spring Boot 应用添加 Redis Caching","date":"2019-04-08","updated":"2019-04-10","comments":true,"path":"spring-boot-caching-example.html","link":"","permalink":"https://blog.mariojd.cn/spring-boot-caching-example.html","excerpt":"","keywords":"","text":"中大型应用开发中,缓存的重要性不言而喻,早期常用的进程式类的缓存,像 EhCache 或者是 ConcurrentHashMap 这样的容器,发展到如今,更流行的是那些分布式的独立缓存服务,如:Redis、Memcached。 对于 Java 应用开发者来说,Spring 提供了完善的缓存抽象机制,结合 Spring Boot 的使用,可以做到非常轻松的完成缓存实现和切换。下面通过简单的示例,展示下如何快速为你的 Spring Boot 应用添加 Redis Caching。 相关依赖 <dependencies> <!-- 添加该依赖后,将自动使用 Redis 作为 Cache Provider --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> 补充配置 spring: datasource: url: jdbc:mysql://localhost:3306/test?useUnicode=true&zeroDateTimeBehavior=convertToNull&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8&tinyInt1isBit=false username: root password: root jpa: database-platform: org.hibernate.dialect.MySQL5InnoDBDialect hibernate: ddl-auto: create # 开启 SQL 输出,方便查看结果是否走了缓存 show-sql: true open-in-view: false redis: host: localhost cache: # 非必须,但如果配置了需补充相应的依赖,否则会出错 #type: redis redis: # 过期时间5秒,默认单位:毫秒,等同于设置成 5s、5S time-to-live: 5000 key-prefix: cn.mariojd.cache. cache-null-values: false 添加实体,实现 Serializable 接口 @Data@Entity@Builder@NoArgsConstructor@AllArgsConstructorpublic class User implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String name;} 定义 Repository 接口: public interface UserRepository extends JpaRepository<User, Integer> {} 编写 Service,进行缓存规则配置,核心注解有:@CacheConfig、@Cacheable(缓存新增)、@CachePut(缓存更新)、@CacheEvict(缓存删除) @Slf4j@Service@CacheConfig(cacheNames = \"user\")public class UserService { @Resource private UserRepository userRepository; /** * Key name: key-prefix.classSimpleName.methodName?pn=xxx&ps=xxx&sort=xxx */ @Cacheable(key = \"#root.targetClass.simpleName+'.'+#root.methodName+'?pn='+#pageable.pageNumber+'&ps='+#pageable.pageSize+'&sort='+#pageable.sort.toString()\") public Page<User> page(Pageable pageable) { return userRepository.findAll(pageable); } @Cacheable(key = \"'user.'+#userId\", unless = \"#result == null\") public Optional<User> get(int userId) { return userRepository.findById(userId); } @Transactional public User add(String name) { User user = User.builder().name(name).build(); return userRepository.save(user); } @CachePut(key = \"'user.'+#userId\", unless = \"#result == null\") @Transactional public Optional<User> update(int userId, String name) { Optional<User> userOptional = userRepository.findById(userId); userOptional.ifPresent(user -> { user.setName(name); userRepository.save(user); }); return userOptional; } @CacheEvict(key = \"'user.'+#userId\") @Transactional public void delete(int userId) { userRepository.findById(userId).ifPresent(user -> userRepository.delete(user)); }} 缓存测试,为启动类添加:@EnableCaching @Slf4j@EnableCaching@SpringBootApplicationpublic class SpringBootCacheApplication implements ApplicationRunner { public static void main(String[] args) { new SpringApplicationBuilder() .sources(SpringBootCacheApplication.class) .bannerMode(Banner.Mode.OFF) .web(WebApplicationType.NONE) .run(args); } @Resource private UserRepository userRepository; @PostConstruct public void init() { // 初始化数据 for (int i = 0; i < 10; i++) { User user = User.builder().name(\"ZS\" + i).build(); userRepository.save(user); } } @Resource private UserService userService; @Resource private Environment environment; @Override public void run(ApplicationArguments args) throws InterruptedException { // 测试缓存,观察是否有SQL输出 PageRequest pageable = PageRequest.of(0, 5); userService.page(pageable); for (int i = 0; i < 5; i++) { userService.page(pageable); log.info(\"Reading page cache...\"); } // 由于配置是5秒中后缓存失效,这里休眠后重新读取 TimeUnit.MILLISECONDS.sleep(Integer.parseInt(environment.getProperty(\"spring.cache.redis.time-to-live\", \"5000\"))); log.warn(\"Page Cache expired : \" + userService.page(pageable).getTotalElements()); log.info(\"\\n\"); // Test CRUD Cache User user = userService.add(\"李四\"); int userId = user.getId(); userService.get(userId); log.info(\"Reading user cache...\" + userService.get(userId)); userService.update(userId, \"王五\"); log.info(\"Reading new user cache...\" + userService.get(userId)); userService.delete(userId); log.warn(\"User Cache delete : \" + userService.get(userId)); } } 从图中的红框部分输出可以看到,这些查询走了缓存,如果需要在 redis 中查看缓存内容,可以将配置中的 TTL 时间调大: 扩展操作Spring 允许开发者们通过自定义 KeyGenerator 来覆盖繁琐的 Key 定义(非必须),同时也允许我们配置自定义的 CacheManager,下面来看看如何编写 KeyGenerator: @Slf4jpublic class CustomKeyGenerator implements KeyGenerator { @Override public Object generate(Object target, Method method, Object... params) { // 类名.方法名.参数值 String keySuffix = target.getClass().getSimpleName() + \".\" + method.getName() + \".\" + Arrays.toString(params); log.info(\"Cache key suffix : {}\", keySuffix); return keySuffix; }} 接着配置注册为 Bean: @Configurationpublic class CustomConfig { @Bean public CustomKeyGenerator customKeyGenerator() { return new CustomKeyGenerator(); }} 编写 Service 用于测试,具体的测试代码这里就不再贴出来了,有兴趣的可以自行尝试。 @Slf4j@Service@CacheConfig(cacheNames = \"user\")public class UserSupportService { @Resource private UserRepository userRepository; /** * 使用了自定义的KeyGenerator * 缓存生效需满足:存在不为空的入参i、且返回值非空 */ @Cacheable(keyGenerator = \"customKeyGenerator\", condition = \"#i!=null\", unless = \"#result.isEmpty()\") public List<User> list(Integer i) { return userRepository.findAll(); }} 参考阅读Spring-Boot-Caching 示例源码欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"spring boot","slug":"spring-boot","permalink":"https://blog.mariojd.cn/tags/spring-boot/"},{"name":"spring cache","slug":"spring-cache","permalink":"https://blog.mariojd.cn/tags/spring-cache/"}]},{"title":"让 Restful API 更加 Simpler:Spring Data Rest","slug":"让 Restful API 更加 Simpler:Spring Data Rest","date":"2019-04-02","updated":"2019-04-09","comments":true,"path":"spring-data-rest-simple-reference-example.html","link":"","permalink":"https://blog.mariojd.cn/spring-data-rest-simple-reference-example.html","excerpt":"","keywords":"","text":"背景说明Spring Data REST 作为 Spring Data 项目的子集,开发者只需使用注解 @RepositoryRestResource 标记,就可以把整个 Repository 转换为 HAL 风格的 REST 资源,目前已支持 Spring Data JPA、Spring Data MongoDB、Spring Data Neo4j等等。 上手示例下面的示例,可以帮助大家快速了解下使用 Spring Data REST 所带来的便利,当然了,大部分业务场景都不会有这么的简单,因此在实际项目中并不推荐使用: 添加依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-rest</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> 核心代码 配置 application.yml spring: data: rest: # Restful API 路径前缀 base-path: api max-page-size: 10 default-page-size: 5 datasource: url: jdbc:mysql://localhost:3306/test?useUnicode=true&zeroDateTimeBehavior=convertToNull&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8&tinyInt1isBit=false username: root password: root mvc: servlet: load-on-startup: 1 throw-exception-if-no-handler-found: true jpa: database-platform: org.hibernate.dialect.MySQL5InnoDBDialect hibernate: ddl-auto: update show-sql: true open-in-view: false jackson: time-zone: GMT+8logging: level: web: debug 定义实体和性别枚举类 @Data@MappedSuperclass@NoArgsConstructor@AllArgsConstructorpublic class BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @UpdateTimestamp @Column(nullable = false) private Date updateTime; @CreationTimestamp @Column(nullable = false, updatable = false) private Date createTime; @Version private Long version; @NotNull private Boolean deleted = false;} @Data@Entity@Builder@NoArgsConstructor@AllArgsConstructor@EqualsAndHashCode(callSuper = false)public class User extends BaseEntity { @NotBlank private String name; @NotNull @Enumerated private Gender gender;} public enum Gender { /** * 男 */ MAN, /** * 女 */ WOMAN, /** * 未知 */ UNKNOWN;} 添加 Repository @RepositoryRestResource(path = \"user\")public interface UserRepository extends JpaRepository<User, Integer> { /** * /api/user/search/findByName */ List<User> findByName(@Param(\"name\") String name); /** * /api/user/1 */ @Override @Modifying @Query(\"UPDATE User u SET u.deleted = true WHERE u.id = ?1\") void deleteById(Integer id);} 初始化测试数据 @SpringBootApplicationpublic class SpringBootDataRestApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDataRestApplication.class, args); } @Resource private UserRepository userRepository; /** * 初始化数据 */ @PostConstruct public void init() { EnumSet<Gender> genders = EnumSet.allOf(Gender.class); List<User> users = new LinkedList<>(); for (int i = 0; i < 10; i++) { User user = User.builder().name(\"test\" + i) .gender(genders.stream().findAny().get()).build(); users.add(user); } userRepository.saveAll(users); }} 测试说明启动 Application,此时已暴露出来的 Restful API 接口包含几个: 请求方式 请求路径 接口说明 GET http://ip:port/api/user{?page,size,sort} 分页查询 GET http://ip:port/api/user/1 查询id为1的用户 GET http://ip:port/api/user/search/findByName?name=xxx 查询name为xxx的用户 POST http://ip:port/api/user 新增用户 PUT http://ip:port/api/user/1 更新id为1的用户 DELETE http://ip:port/api/user/1 删除id为1的用户 以上这些动作都有相应的触发事件,我们可以参考文档说明并根据实际需求做补充监听。 参考文档Spring Data REST Reference Guide 示例源码欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"spring boot","slug":"spring-boot","permalink":"https://blog.mariojd.cn/tags/spring-boot/"},{"name":"spring data rest","slug":"spring-data-rest","permalink":"https://blog.mariojd.cn/tags/spring-data-rest/"},{"name":"restful api","slug":"restful-api","permalink":"https://blog.mariojd.cn/tags/restful-api/"}]},{"title":"那些容易被遗忘的 Redis 功能","slug":"那些容易被遗忘的 Redis 功能","date":"2019-03-15","updated":"2019-03-18","comments":true,"path":"several-practical-and-easily-overlooked-redis-features.html","link":"","permalink":"https://blog.mariojd.cn/several-practical-and-easily-overlooked-redis-features.html","excerpt":"","keywords":"","text":"1. 强大的排序Redis 的 SORT 命令可以对列表(List)、集合(Set)和有序集合(Sorted Set)的元素值进行排序(快排算法)。 对列表(List)键进行排序: 127.0.0.1:6379> lpush numbers 3 4 1 5 2(integer) 5# 按插入顺序返回列表元素127.0.0.1:6379> lrange numbers 0 -11) "2"2) "5"3) "1"4) "4"5) "3"127.0.0.1:6379> sort numbers [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination]# 默认是按数值类型的正排序输出127.0.0.1:6379> sort numbers1) "1"2) "2"3) "3"4) "4"5) "5"# 还可以倒序进行输出127.0.0.1:6379> sort numbers desc1) "5"2) "4"3) "3"4) "2"5) "1" 使用 ALPHA 选项对包含字符串值的集合键(Set)进行排序: 127.0.0.1:6379> sadd characters b d a e c(integer) 5# 集合元素的返回是无序的127.0.0.1:6379> smembers characters1) "e"2) "a"3) "d"4) "b"5) "c"127.0.0.1:6379> sort characters [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination]# 有序输出集合中的元素127.0.0.1:6379> sort characters alpha1) "a"2) "b"3) "c"4) "d"5) "e"127.0.0.1:6379> sort characters desc alpha1) "e"2) "d"3) "c"4) "b"5) "a" 添加权重 weight 并以外部 key 进行排序,以有序集合(Sorted Set)键为例: 127.0.0.1:6379> zadd friends 8 Jared 7 Nancy 9 Lucy(integer) 3# 按元素的分值正序输出127.0.0.1:6379> zrange friends 0 -1 withscores1) "Nancy"2) "7"3) "Jared"4) "8"5) "Lucy"6) "9"# 添加外部 key 以作为权重127.0.0.1:6379> mset Jared_weight 3 Nancy_weight 2 Lucy_weight 1OK# 以 上述 key 为权重,对有序集合(Sorted Set)元素进行排序127.0.0.1:6379> sort friends by *_weight1) "Lucy"2) "Nancy"3) "Jared"# 添加外部 key 用于获取127.0.0.1:6379> mset Jared-age 20 Nancy-age 19 Lucy-age 18OK# 追加 GET 命令获取外部匹配 Key 的元素值127.0.0.1:6379> sort friends by *_weight get *-age1) "18"2) "19"3) "20"# 可多次使用 GET 命令127.0.0.1:6379> sort friends by *_weight get *-age get *_weight1) "18"2) "1"3) "19"4) "2"5) "20"6) "3"# 还可以通过使用特殊 # 模式获取 GET 元素本身127.0.0.1:6379> sort friends by *_weight get *-age get *_weight get #1) "18"2) "1"3) "Lucy"4) "19"5) "2"6) "Nancy"7) "20"8) "3"9) "Jared" 实现分页排序功能: 分页排序功能通过 LIMIT 修饰符限制:此修饰符带有 offset 参数,指定要跳过的元素数量;还带有 count 参数,指定了从 offset 开始要返回的元素数量: 127.0.0.1:6379> sort numbers1) "1"2) "2"3) "3"4) "4"5) "5"127.0.0.1:6379> sort numbers limit 2 21) "3"2) "4"127.0.0.1:6379> sort numbers limit 2 2 desc1) "3"2) "2" 保存排序操作的结果 默认的,SORT 命令将返回排序后的元素给客户端。而 STORE 选项,可以将结果存储于到列表中,以代替返回到客户端。 127.0.0.1:6379> sort friends by *_weight store myfriends(integer) 3127.0.0.1:6379> debug object myfriendsValue at:0x7fa02a5121e0 refcount:1 encoding:quicklist serializedlength:33 lru:9111483 lru_seconds_idle:138 ql_nodes:1 ql_avg_node:3.00 ql_ziplist_max:-2 ql_compressed:0 ql_uncompressed_size:31127.0.0.1:6379> lrange myfriends 0 -11) "Lucy"2) "Nancy"3) "Jared" 2. 关键的慢日志开启 Redis 的慢查询日志记录功能,可以帮助开发者更好的监视和优化查询功能,这主要与两个配置选项有关: slowlog-log-slower-than 选项:指定执行时间超过多少微秒(1秒 = 1 000 000微秒)的命令将记录在日志上,默认是 10000微秒 = 0.01秒 slowlog-max-len 选项:指定最多保留多少条慢查询日志(先进先出),默认是 128 下面,我们通过 CONFIG SET 命令来更改上述两个选项的配置(也可以在redis.conf中更改),来看看慢查询日志功能的应用: 127.0.0.1:6379> config set slowlog-log-slower-than 0OK127.0.0.1:6379> config set slowlog-max-len 5OK# 新增几条数据用于测试127.0.0.1:6379> set test01 01OK127.0.0.1:6379> set test02 02OK127.0.0.1:6379> set test03 03OK127.0.0.1:6379> set test04 04OK127.0.0.1:6379> set test05 05OK127.0.0.1:6379> set test06 06OK# slowlog get 查看慢日志记录127.0.0.1:6379> slowlog get 31) 1) (integer) 182 # 标志Id 2) (integer) 1552617567 # 时间戳 3) (integer) 2 # 执行时长,单位:微秒 4) 1) "SET" # 执行命令 2) "test01" 3) "01"2) 1) (integer) 181 2) (integer) 1552617566 3) (integer) 2 4) 1) "SET" 2) "test02" 3) "02"3) 1) (integer) 180 2) (integer) 1552617565 3) (integer) 1 4) 1) "SET" 2) "test03" 3) "03"# slowlog len 查看慢日志记录条数127.0.0.1:6379> slowlog len(integer) 5# slowlog reset 清空慢日志记录127.0.0.1:6379> slowlog resetOK127.0.0.1:6379> slowlog get(empty list or set) 3. 数据备份恢复Redis 实现持久化有两种方式: RDB (redis data base):默认开启,对数据执行周期性的持久化,可以手动执行(SAVE和BGSAVE),RDB 文件 dump.rdb 是个经过压缩的二进制文件,在服务启动时自动载入 (如果未开启 AOF) save,阻塞进程,直到 RDB 文件创建完毕 bgsave,派生出子进程来负责创建 RDB 文件,lastsave可以检查该操作结果 AOF (append only file):以每条写入命令作为日志(可读),以 append-only 模式写进日志文件中,在 redis 重启的时候,可以通过回放 AOF 日志中的写入指令来重构整个数据集 bgrewriteaof,手动触发异步执行 AOF 文件的重写操作,用于优化压缩体积 扩展:Redis 的持久化有哪几种方式?不同的持久化机制都有什么优缺点?持久化机制具体底层是如何实现的? 来看看如何远程备份 rdb 文件: # 远程服务器会执行bgsave操作,然后将 rdb 文件传输到当前客户端$ redis-cli -host 192.168.x.x -port 6379 -a password --rdb ./dump.rdb.bakSYNC sent to master, writing 1995841 bytes to './dump.rdb.bak'Transfer finished with success. 如果需要恢复数据,只需将相应的备份文件移动到 redis 安装目录,然后重启服务即可。获取 redis 目录可以使用 CONFIG 命令: 127.0.0.1:6379> config get dir1) "dir"2) "/usr/local/etc/redis" 4. 压力性能测试Redis 压力性能测试通过同时执行多个命令实现:redis-benchmark [option] [option value] redis-benchmark 部分可选参数如下所示,更多使用 redis-benchmark --help 查看: 选项 描述 默认值 -h 指定服务器主机名 127.0.0.1 -p 指定服务器端口 6379 -c 指定并发连接数 50 -n 指定请求数 10000 -l 循环执行测试 -q 执行完一遍测试后退出 5. 命令行小工具 实时查看当前 Redis 服务接收及处理的命令请求:MONITOR 实时监控服务器的状态: redis-cli --stat 查看连接的客户端情况:info client 127.0.0.1:6379> info clients# Clientsconnected_clients:23client_longest_output_list:0client_biggest_input_buf:0blocked_clients:0 查看具体的客户端连接信息:client list 扫描大 KEY:redis-cli --bigkeys -i 0.1 查看执行命令统计的快照:info commandstats 输出包含命令执行了多少次,执行所耗费的毫秒数(含总时间及平均时间),重置统计结果使用 CONFIG RESETSTAT 欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Redis","slug":"Redis","permalink":"https://blog.mariojd.cn/categories/Redis/"}],"tags":[{"name":"redis","slug":"redis","permalink":"https://blog.mariojd.cn/tags/redis/"}]},{"title":"如何优雅的设计 Spring Boot API 接口版本号","slug":"如何优雅的设计 Spring Boot API 接口版本号","date":"2019-03-14","updated":"2019-03-15","comments":true,"path":"how-to-design-spring-boot-api-version-number-elegantly.html","link":"","permalink":"https://blog.mariojd.cn/how-to-design-spring-boot-api-version-number-elegantly.html","excerpt":"","keywords":"","text":"一般来说,系统上线以后,需求仍会发生变动,功能也会迭代更新。可能是接口参数发生变更,也有可能是业务逻辑需要调整,如果直接在原来的接口上进行修改,必然会影响原有服务的正常运行。 常见的解决方案,是在接口路径中加入版本号用于区分,此外还可以在参数甚至 header 里带上版本号。这里以在请求路径中带上版本号为例,如:http://IP:PORT/api/v1/test ,v1 即代表的是版本号。当然了,可以像这样,直接写死在 @RequestMapping("api/v1/test") 属性中,不过下面提供了更为优雅的解决方案。 自定义版本号标记注解 @Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface ApiVersion { /** * 标识版本号,从1开始 */ int value() default 1;} 重写相应的 RequestCondition @Data@Slf4jpublic class ApiVersionCondition implements RequestCondition<ApiVersionCondition> { /** * 接口路径中的版本号前缀,如: api/v[1-n]/test */ private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile(\"/v(\\\\d+)/\"); private int apiVersion; ApiVersionCondition(int apiVersion) { this.apiVersion = apiVersion; } @Override public ApiVersionCondition combine(ApiVersionCondition other) { // 最近优先原则,方法定义的 @ApiVersion > 类定义的 @ApiVersion return new ApiVersionCondition(other.getApiVersion()); } @Override public ApiVersionCondition getMatchingCondition(HttpServletRequest request) { Matcher m = VERSION_PREFIX_PATTERN.matcher(request.getRequestURI()); if (m.find()) { // 获得符合匹配条件的ApiVersionCondition int version = Integer.valueOf(m.group(1)); if (version >= getApiVersion()) { return this; } } return null; } @Override public int compareTo(ApiVersionCondition other, HttpServletRequest request) { // 当出现多个符合匹配条件的ApiVersionCondition,优先匹配版本号较大的 return other.getApiVersion() - getApiVersion(); }} 重写部分 RequestMappingHandlerMapping 的方法 @Slf4jpublic class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping { @Override protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) { // 扫描类上的 @ApiVersion ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class); return createRequestCondition(apiVersion); } @Override protected RequestCondition<?> getCustomMethodCondition(Method method) { // 扫描方法上的 @ApiVersion ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class); return createRequestCondition(apiVersion); } private RequestCondition<ApiVersionCondition> createRequestCondition(ApiVersion apiVersion) { if (Objects.isNull(apiVersion)) { return null; } int value = apiVersion.value(); Assert.isTrue(value >= 1, \"Api Version Must be greater than or equal to 1\"); return new ApiVersionCondition(value); }} 配置注册自定义的 CustomRequestMappingHandlerMapping @Slf4j@Configurationpublic class WebMvcConfig extends WebMvcConfigurationSupport { @Override public RequestMappingHandlerMapping requestMappingHandlerMapping() { return new CustomRequestMappingHandlerMapping(); }} 编写接口,标记上相应的 @ApiVersion @Slf4j@ApiVersion@RestController@RequestMapping(\"api/{version}/test\")public class TestController { @GetMapping public String test01(@PathVariable String version) { return \"test01 : \" + version; } @GetMapping @ApiVersion(2) public String test02(@PathVariable String version) { return \"test02 : \" + version; }} 启动 Application,测试及查看结果 示例源码欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"spring boot","slug":"spring-boot","permalink":"https://blog.mariojd.cn/tags/spring-boot/"}]},{"title":"携程 Apollo 配置中心:Example","slug":"携程 Apollo 配置中心:Example","date":"2019-03-11","updated":"2019-03-14","comments":true,"path":"apollo-config-center-of-ctripcorp-example.html","link":"","permalink":"https://blog.mariojd.cn/apollo-config-center-of-ctripcorp-example.html","excerpt":"","keywords":"","text":"本文介绍如何基于 Spring Boot 来搭建 Apollo 客户端,并展示如何动态更改运行时服务的输出日志等级。参考阅读 Apollo · Java 客户端使用指南 以及 Apollo · 使用示例 搭建客户端 加入依赖 <dependency> <groupId>com.ctrip.framework.apollo</groupId> <artifactId>apollo-client</artifactId> <version>1.3.0</version></dependency> 在application.yml中添加配置: # appid 是应用的身份信息,需后台设定app: id: 2019# 一般指向 config service 服务地址apollo: meta: http://localhost:8080 在启动类上添加@EnableApolloConfig注解用于开启 Apollo 客户端: @EnableApolloConfig@SpringBootApplicationpublic class ApolloApplication { public static void main(String[] args) { SpringApplication.run(ApolloApplication.class, args); System.out.println(\"ApolloApplication started...\"); // 监听配置变化事件 Config config = ConfigService.getAppConfig(); config.addChangeListener(changeEvent -> { log.debug(\"Changes for namespace {}\", changeEvent.getNamespace()); for (String key : changeEvent.changedKeys()) { ConfigChange change = changeEvent.getChange(key); System.out.println(String.format(\"Found change - key: %s, oldValue: %s, newValue: %s, changeType: %s\", change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType())); } }); }} 动态调整日志级别 在application.yml中设定日志等级 logging: level: cn.mariojd.config.apollo: info 配置监听事件,实现动态变更日志等级 @Slf4j@Configurationpublic class DynamicLogLevelConfig { @Resource private LoggingSystem loggingSystem; private static final String LOG_KEY = \"logging.level\"; @ApolloConfigChangeListener private void configChangeListener(ConfigChangeEvent e) { e.changedKeys().stream().filter(key -> StringUtils.startsWithIgnoreCase(LOG_KEY, key)).forEach(key -> { ConfigChange change = e.getChange(key); String oleLevel = change.getOldValue(); String newLevel = change.getNewValue(); // 只有合法的日志等级配置才会生效,常见的如 DEBUG, INFO, WARN, ERROR Arrays.stream(LogLevel.values()).filter(logLevel -> StringUtils.startsWithIgnoreCase(newLevel, logLevel.toString())).findFirst() .ifPresent(logLevel -> loggingSystem.setLogLevel(\"cn.mariojd.config.apollo\", logLevel)); System.out.println(String.format(\"动态调整日志级别:Key -> %s ; OldLevel -> %s ; NewLevel -> %s\", key, oleLevel, newLevel)); }); }} 进行简单的测试,项目中引入了lombok @Slf4j@EnableApolloConfig@SpringBootApplicationpublic class ApolloApplication implements InitializingBean { private ExecutorService executorService = Executors.newFixedThreadPool(1); public static void main(String[] args) { SpringApplication.run(ApolloApplication.class, args); log.info(\"ApolloApplication started...\"); } @Override public void afterPropertiesSet() { executorService.submit(this::testDynamicLogLevelConfig); } public void testDynamicLogLevelConfig() { // 默认 INFO 级别下程序不输出 DEBUG 日志 for (; ; ) { log.debug(\"Debug log...\"); log.info(\"Info log...\"); log.warn(\"Warn log...\"); log.error(\"Error log...\"); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } } 在 Apollo Protal 管理后台新增项目2019,添加配置并发布 查看应用日志输出,符合预期,测试结束 Docker 部署 Apollo 服务导致 Apollo 客户端 无法访问?参考这里提供的几种解决方案。当然了,最简单的方式还是在启动时指定-Dapollo.configService=http://IP:PORT,来跳过meta service的服务发现: 示例源码欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Micro Service","slug":"Micro-Service","permalink":"https://blog.mariojd.cn/categories/Micro-Service/"}],"tags":[{"name":"Micro Service","slug":"Micro-Service","permalink":"https://blog.mariojd.cn/tags/Micro-Service/"},{"name":"Ctrip Apollo","slug":"Ctrip-Apollo","permalink":"https://blog.mariojd.cn/tags/Ctrip-Apollo/"},{"name":"Config Center","slug":"Config-Center","permalink":"https://blog.mariojd.cn/tags/Config-Center/"}]},{"title":"如何在 Jar 包外管理 Spring Boot 应用的配置文件","slug":"如何在 Jar 包外管理 Spring Boot 应用的配置文件","date":"2019-03-05","updated":"2019-03-11","comments":true,"path":"how-to-manage-the-config-file-of-the-spring-boot-application-outside-the-jar-package.html","link":"","permalink":"https://blog.mariojd.cn/how-to-manage-the-config-file-of-the-spring-boot-application-outside-the-jar-package.html","excerpt":"","keywords":"","text":"常见的 spring boot 应用多是打包成 jar 包运行在服务器,这包含了一系列的配置文件以及第三方的依赖,不过这也引发了常见的思考:除application.properties之外的其它配置文件变动,是否需要重新打包再重新部署?如日志配置文件、mybatis 的 xml 文件。 先来看看 Spring Boot 是如何加载核心配置文件的,在org.springframework.boot.context.config.ConfigFileApplicationListener的内部类Loader的load()可以查看具体实现,以下优先级从高到低依次为: 通过启动命令指定:java -jar -Dspring.config.location=xxx/application.properties demo.jar Jar 包同级目录下的 config 目录 Jar 包同级目录 classpath (resources) 同级目录下的 config 目录 classpath (resources) 目录 留意到这段代码定义: // 默认的搜索路径,对应了上面描述的优先级private static final String DEFAULT_SEARCH_LOCATIONS = \"classpath:/,classpath:/config/,file:./,file:./config/\"; 而 classpath 路径是可以指定的,在Application启动类添加如下代码,来看看默认的 jar 应用程序对应的 resources 位置: @SpringBootApplicationpublic class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); System.out.println(DemoApplication.class.getResource(\"/\")); }} 相应的输出结果为:jar:file:/C:/xxx/demo.jar!/BOOT-INF/classes!/ 因为java -jar所指定及对应的优先级是最高的,所以启动时设定 classpath 就可以达到想要的效果:将配置文件从 Jar 包独立出来进行管理。 以下是该对应扩展方式的几种说明: -Xbootclasspath: 完全替换基本的 Java class 搜索路径(不常用) -Xbootclasspath/p: 将 classpath 添加在核心class搜索路径前面(不常用,避免引起冲突) -Xbootclasspath/a: 将classpath添加在核心class搜索路径后面 (常用) 最后,这里通过指定当前目录下的resources文件夹进行了简单测试:java -jar -Xbootclasspath/a:./resources/ demo.jar,观察启动日志是否可以读取 logback 配置: 相应的输出及日志为:file:/C:/xxx/demo-project/target/resources/,结果符合预期。 参考链接 springboot项目实现jar包外配置文件管理-jar参数运行应用时,设置classpath的方法 欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"spring boot","slug":"spring-boot","permalink":"https://blog.mariojd.cn/tags/spring-boot/"}]},{"title":"携程 Apollo 配置中心:Quick Start","slug":"携程 Apollo 配置中心:Quick Start","date":"2019-03-04","updated":"2019-03-05","comments":true,"path":"apollo-config-center-of-ctripcorp-quick-start.html","link":"","permalink":"https://blog.mariojd.cn/apollo-config-center-of-ctripcorp-quick-start.html","excerpt":"","keywords":"","text":"Apollo(阿波罗)是携程开源的分布式配置中心,能够集中化管理不同环境、不同集群的应用配置,配置修改后能够实时推送到客户端,具备规范的权限、流程治理等特性,适用于绝大多数的微服务配置管理场景。详细的介绍可点击 Apollo Wiki 进行了解,本文基于简单的本地部署和 Docker 部署示例进行演示。 本地部署 下载 Quick Start 从 github checkout ,项目地址:https://github.com/nobodyiam/apollo-build-scripts 从百度网盘下载,本地解压:https://pan.baidu.com/s/1iftkG14dVtOq-JIyz3AqLA 导入数据库 Apollo 服务需要两个数据库:ApolloPortalDB 和 ApolloConfigDB,相关脚本在 sql 文件夹下,自行导入 MySQL 即可: 更改数据库连接信息 编辑根目录下的 demo.sh 脚本,修改数据库连接信息: 执行 demo.sh 脚本 ( windows下可借助 Git 客户端来启动 ) Quick Start 服务会在本地启动3个服务,分别使用8070, 8080, 8090端口,请确保这3个端口当前没有被使用。 启动命令:./demo.sh [commands],其中 commands 可使用如下3个指令: 查看启动状态 在 http://localhost:8070 可进入配置管理中心: 在 http://localhost:8080 可查看注册中心 Eureka: 测试示例 启动测试客户端: 在后台新增配置: 发布后查看客户端输出: Docker 部署 下载 需要 clone Apollo 的代码,以确保 docker-quick-start 文件夹已经在本地存在 启动 docker-compose up -d 测试,运行Demo客户端: docker exec -i apollo-quick-start /apollo-quick-start/demo.sh client 全文完,后面还会结合具体的使用案例再进行演示。最后,建议多翻翻 Apollo Wiki ,对 Apollo 源码有兴趣的童鞋强烈推荐看这里 欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Micro Service","slug":"Micro-Service","permalink":"https://blog.mariojd.cn/categories/Micro-Service/"}],"tags":[{"name":"Micro Service","slug":"Micro-Service","permalink":"https://blog.mariojd.cn/tags/Micro-Service/"},{"name":"Ctrip Apollo","slug":"Ctrip-Apollo","permalink":"https://blog.mariojd.cn/tags/Ctrip-Apollo/"},{"name":"Config Center","slug":"Config-Center","permalink":"https://blog.mariojd.cn/tags/Config-Center/"}]},{"title":"如何自定义 JPA 的数据库命名策略","slug":"如何自定义 JPA 的数据库命名策略","date":"2019-02-19","updated":"2019-02-19","comments":true,"path":"custom-database-naming-strategy-for-jpa.html","link":"","permalink":"https://blog.mariojd.cn/custom-database-naming-strategy-for-jpa.html","excerpt":"","keywords":"","text":"本次示例的项目中,定义了如下两个实体: @Data@Entitypublic class Teacher { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @Column(name = \"Name\") private String name;} @Data@Entity@Table(name = \"teacher_class\")public class TeacherClass { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private Integer teacherId; @Column(name = \"ClassName\") private String className;} 先来看看 JPA 默认的命名策略,我们可以显示配置如下: spring: datasource: username: root password: root url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false jpa: database-platform: org.hibernate.dialect.MySQL5InnoDBDialect show-sql: true hibernate: ddl-auto: create naming: # 1. 表名及字段全小写下划线分隔命名策略(默认) physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy Hibernate: create table teacher (id integer not null auto_increment, name varchar(255), primary key (id)) engine=InnoDBHibernate: create table teacher_class (id integer not null auto_increment, class_name varchar(255), teacher_id integer, primary key (id)) engine=InnoDB 运行项目,通过数据库或输出的SQL语句,默认策略的表现为:表名及字段全小写,并以下划线分隔 此外,引入的 Hibernate 还提供了另外一种物理命名策略,先进行如下配置,再来观察结果: spring: datasource: username: root password: root url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false jpa: database-platform: org.hibernate.dialect.MySQL5InnoDBDialect show-sql: true hibernate: ddl-auto: create naming: # 2. 物理命名策略,未定义 @Table 和 @Column 将以实体名和属性名作为表名及字段名 physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl Hibernate: create table Teacher (id integer not null auto_increment, Name varchar(255), primary key (id)) engine=InnoDBHibernate: create table teacher_class (id integer not null auto_increment, ClassName varchar(255), teacherId integer, primary key (id)) engine=InnoDB 以上表明,Hibernate 提供的命名策略是以实体名和属性名分别作为表名及字段名,但如果有定义 @Table 和 @Column ,则以该属性值进行映射命名 当然,某些场景下,可以通过自定义命名策略来简化操作,或实现自身特定的业务,例如:假设需要为未定义 @Table 实体加上表前缀 tb_,或是以大写字母下划线来分隔定义字段(简单起见,以下配置并不直接实现 PhysicalNamingStrategy): public class CustomNamingStrategyConfig extends SpringPhysicalNamingStrategy { /** * 配置映射的数据表名 * * @param name * @param jdbcEnvironment * @return */ @Override public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment jdbcEnvironment) { if (name == null) { return null; } // 实体名 or 自定义的@Tabel name属性值 String text = name.getText(); // 首字母大写(类名),实体未定义@Table, 为表名加上tb_前缀 if (Character.isUpperCase(text.charAt(0))) { final String tb = \"tb_\"; if (!text.contains(tb)) { text = \"tb\" + text; } StringBuilder builder = new StringBuilder(text.replace('.', '_')); for (int i = 1, maxLength = builder.length() - 1; i < maxLength; i++) { if (this.isUnderscoreRequired(builder.charAt(i - 1), builder.charAt(i), builder.charAt(i + 1))) { builder.insert(i++, '_'); } } return super.getIdentifier(builder.toString(), name.isQuoted(), jdbcEnvironment); } else { // 实体定义了@Table(name=\"xxx\"),以name属性值为表名 return super.getIdentifier(text, name.isQuoted(), jdbcEnvironment); } } /** * 判断是否前一个字符为小写字母,当前字符为大写字母,下一个字符为小写字母 * * @param before * @param current * @param after * @return */ private boolean isUnderscoreRequired(char before, char current, char after) { return Character.isLowerCase(before) && Character.isUpperCase(current) && Character.isLowerCase(after); } /** * 配置映射的字段名 * * @param name * @param jdbcEnvironment * @return */ @Override public Identifier toPhysicalColumnName(Identifier name, JdbcEnvironment jdbcEnvironment) { // 实体的属性名 or 自定义的@Column name属性值 String text = name.getText(); if (Character.isUpperCase(text.charAt(0))) { // 大写字母下划线分隔命名策略,有在实体字段上自定义@Column(name=\"Xx_Xxx\") return new Identifier(text, name.isQuoted()); } else { // 常见的小写驼峰式命名策略 return super.toPhysicalColumnName(name, jdbcEnvironment); } }} 在 application.yml 中修改如下配置: spring: datasource: username: root password: root url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false jpa: database-platform: org.hibernate.dialect.MySQL5InnoDBDialect show-sql: true hibernate: ddl-auto: create naming: # 3. 自定义的命名策略 physical-strategy: cn.mariojd.jpa.naming.config.CustomNamingStrategyConfig``` 启动项目,查看结果,符合以上预设的实现要求``` sqlHibernate: create table tb_teacher (id integer not null auto_increment, Name varchar(255), primary key (id)) engine=InnoDBHibernate: create table teacher_class (id integer not null auto_increment, ClassName varchar(255), teacher_id integer, primary key (id)) engine=InnoDB 示例源码欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"spring data jpa","slug":"spring-data-jpa","permalink":"https://blog.mariojd.cn/tags/spring-data-jpa/"}]},{"title":"基于 Spring Data JPA 聊聊悲观锁和乐观锁","slug":"基于 Spring Data JPA 聊聊悲观锁和乐观锁","date":"2019-02-16","updated":"2019-03-01","comments":true,"path":"practice-pessimistic-and-optimistic-lock-with-spring-data-jpa.html","link":"","permalink":"https://blog.mariojd.cn/practice-pessimistic-and-optimistic-lock-with-spring-data-jpa.html","excerpt":"","keywords":"","text":"举个场景:多线程、多进程应用在对数据库的同一数据进行非幂等操作时,如果没有添加相应的锁机制进行校验、判断,通常会导致数据的脏写。抛开分布式锁这种解决思路,简单的来讲,可以优先考虑从数据库层面去解决这个问题。 数据库锁分为乐观锁和悲观锁,前者适合读多写少的场景,后者适合读少写多的场景。乐观锁的实现通常是采用加版本号的形式,即如果更新时版本号未发生改变,则本次操作是成功的,且当前版本号的信息也相应会发生改变;再来看看悲观锁,悲观锁的实现方式是在待执行的SQL语句后加上for update,利用了数据库的行锁或是表锁特性来进行实现,但如果使用不当,会严重拖累整个操作的执行速度。 下面的实际案例展示了具体的操作,该项目基于 Spring Data JPA 实现: 新建两个实体类,Teacher对应悲观锁的示例,User对应乐观锁示例: /** * 悲观锁示例 */@Data@Entity@NoArgsConstructorpublic class Teacher { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String name; public Teacher(String name) { this.name = name; }} /** * 乐观锁示例 */@Data@Entity@NoArgsConstructorpublic class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String name; /** * 乐观锁实现方式①:直接添加@Version */ @Version private Integer version = 0; public User(String name) { this.name = name; }} 乐观锁的实现有两种: 2.1 实体类添加 version 字段并用相应的@Version进行标记,这种方式较为简单方便(如上User); 2.2 在进行更新操作的时候进行 version 判断,需要自己写 SQL 实现(如下) public interface UserRepository extends JpaRepository<User, Integer> { /** * 乐观锁实现方式②:在SQL中添加版本号校验 */ @Modifying @Query(value = \"update user u set u.name = ?2, u.version = u.version + 1 where u.id = ?1 and u.version = ?3\", nativeQuery = true) int updateWithVersion(int id, String name, int version);} 悲观锁的实现也有两种: 3.1 添加@Lock注解,并设置值为LockModeType.PESSIMISTIC_WRITE即可代表行级锁,同样这种方式也容易操作 public interface TeacherRepository extends JpaRepository<Teacher, Integer> { /** * 悲观锁实现①:使用@Lock注解,并且设置值为LockModeType.PESSIMISTIC_WRITE */ @Override @Lock(value = LockModeType.PESSIMISTIC_WRITE) Optional<Teacher> findById(Integer id);} 3.2 自行写 SQL 语句,然后结尾加上for update(如下) public interface TeacherRepository extends JpaRepository<Teacher, Integer> { /** * 悲观锁实现②:手写SQL,结尾加上for update */ @Query(value = \"select t.* from teacher t where t.id = ?1 for update\", nativeQuery = true) Optional<Teacher> findTeacherForUpdate(int id);} 编写测试用例: @RunWith(SpringRunner.class)@SpringBootTestpublic class LockApplicationTests { @Resource private UserService userService; @Resource private TeacherService teacherService; @Test public void testUser() { // 新增数据 User user = new User(\"Jared\"); User dbUser = userService.add(user); // @throws org.springframework.orm.ObjectOptimisticLockingFailureException // 乐观锁①:更新User new Thread(() -> userService.optimisticLock(dbUser, 500L)).start(); userService.optimisticLock(dbUser, 1000L); // 乐观锁②:更新User new Thread(() -> userService.optimisticLock(dbUser, 1000L)).start(); userService.optimisticLock2(dbUser, 500L); } @Test public void testTeacher() { // 新增数据 Teacher teacher = new Teacher(\"Nancy\"); Teacher dbTeacher = teacherService.add(teacher); // 悲观锁①:更新Teacher new Thread(() -> { try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } teacherService.pessimisticLock(dbTeacher, 200L); }).start(); // Success teacherService.pessimisticLock(dbTeacher, 2000L); // 悲观锁②:更新Teacher new Thread(() -> { try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } teacherService.pessimisticLock2(dbTeacher, 300L); }).start(); // Success teacherService.pessimisticLock2(dbTeacher, 3000L); }} 最终结论 采用乐观锁,操作失败后会抛出ObjectOptimisticLockingFailureException;使用悲观锁,上述两条操作里面,最终只有先获取到锁的那条操作可以成功执行。 参考链接 JPA之@Version进行乐观锁并发更新JPA 各种实体锁模式的区别聊聊数据库乐观锁和悲观锁,乐观锁失败后重试 示例源码欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"spring data jpa","slug":"spring-data-jpa","permalink":"https://blog.mariojd.cn/tags/spring-data-jpa/"},{"name":"java","slug":"java","permalink":"https://blog.mariojd.cn/tags/java/"},{"name":"乐观锁","slug":"乐观锁","permalink":"https://blog.mariojd.cn/tags/乐观锁/"},{"name":"悲观锁","slug":"悲观锁","permalink":"https://blog.mariojd.cn/tags/悲观锁/"}]},{"title":"Java 获取视频时长及截取帧截图","slug":"Java 获取视频时长及截取帧截图","date":"2019-02-15","updated":"2019-02-15","comments":true,"path":"captures-video-length-and-intercepts-screenshots-with-java.html","link":"","permalink":"https://blog.mariojd.cn/captures-video-length-and-intercepts-screenshots-with-java.html","excerpt":"","keywords":"","text":"前言只是最近碰到有这方面的项目需求,所以简单 Mark 下本文。下面的示例是参考过他人分享的文章,之后本人再自行实践、调整和测试过的,希望对有这方面需求的人有所帮助。 示例 添加依赖 <dependency> <groupId>org.bytedeco</groupId> <artifactId>javacv-platform</artifactId> <version>1.4.4</version></dependency> 上述这段 maven 依赖包含了完整的 javacv 功能 (非常多,依赖Jar就占大概有500MB),由于这里只使用到了其中 ffmpeg 这块的特性,因此也可以像下面这样排除掉无关的部分 <dependency> <groupId>org.bytedeco</groupId> <artifactId>javacv</artifactId> <version>1.4.4</version> <exclusions> <exclusion> <groupId>org.bytedeco</groupId> <artifactId>javacpp</artifactId> </exclusion> <exclusion> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>flycapture</artifactId> </exclusion> <exclusion> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>libdc1394</artifactId> </exclusion> <exclusion> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>libfreenect</artifactId> </exclusion> <exclusion> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>libfreenect2</artifactId> </exclusion> <exclusion> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>librealsense</artifactId> </exclusion> <exclusion> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>videoinput</artifactId> </exclusion> <exclusion> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>opencv</artifactId> </exclusion> <exclusion> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>tesseract</artifactId> </exclusion> <exclusion> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>leptonica</artifactId> </exclusion> <exclusion> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>flandmark</artifactId> </exclusion> <exclusion> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>artoolkitplus</artifactId> </exclusion> </exclusions></dependency><dependency> <groupId>org.bytedeco</groupId> <artifactId>javacv-platform</artifactId> <version>1.4.4</version> <exclusions> <exclusion> <groupId>org.bytedeco</groupId> <artifactId>javacv</artifactId> </exclusion> <exclusion> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>flycapture-platform</artifactId> </exclusion> <exclusion> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>libdc1394-platform</artifactId> </exclusion> <exclusion> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>libfreenect-platform</artifactId> </exclusion> <exclusion> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>libfreenect2-platform</artifactId> </exclusion> <exclusion> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>librealsense-platform</artifactId> </exclusion> <exclusion> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>videoinput-platform</artifactId> </exclusion> <exclusion> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>opencv-platform</artifactId> </exclusion> <exclusion> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>tesseract-platform</artifactId> </exclusion> <exclusion> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>leptonica-platform</artifactId> </exclusion> <exclusion> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>flandmark-platform</artifactId> </exclusion> <exclusion> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>artoolkitplus-platform</artifactId> </exclusion> </exclusions></dependency> 核心代码 获取视频时长 /** * 获取视频时长,单位为秒 * * @param video 源视频文件 * @return 时长(s) */public static long getVideoDuration(File video) { long duration = 0L; FFmpegFrameGrabber ff = new FFmpegFrameGrabber(video); try { ff.start(); duration = ff.getLengthInTime() / (1000 * 1000); ff.stop(); } catch (FrameGrabber.Exception e) { e.printStackTrace(); } return duration;} 截取视频指定帧为图片 /** * 截取视频获得指定帧的图片 * * @param video 源视频文件 * @param picPath 截图存放路径 */public static void getVideoPic(File video, String picPath) { FFmpegFrameGrabber ff = new FFmpegFrameGrabber(video); try { ff.start(); // 截取中间帧图片(具体依实际情况而定) int i = 0; int length = ff.getLengthInFrames(); int middleFrame = length / 2; Frame frame = null; while (i < length) { frame = ff.grabFrame(); if ((i > middleFrame) && (frame.image != null)) { break; } i++; } // 截取的帧图片 Java2DFrameConverter converter = new Java2DFrameConverter(); BufferedImage srcImage = converter.getBufferedImage(frame); int srcImageWidth = srcImage.getWidth(); int srcImageHeight = srcImage.getHeight(); // 对截图进行等比例缩放(缩略图) int width = 480; int height = (int) (((double) width / srcImageWidth) * srcImageHeight); BufferedImage thumbnailImage = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR); thumbnailImage.getGraphics().drawImage(srcImage.getScaledInstance(width, height, Image.SCALE_SMOOTH), 0, 0, null); File picFile = new File(picPath); ImageIO.write(thumbnailImage, \"jpg\", picFile); ff.stop(); } catch (IOException e) { e.printStackTrace(); }} 测试用例 public static void main(String[] args) { String videoPath = ResourceUtils.CLASSPATH_URL_PREFIX + \"video.mp4\"; File video = null; try { video = ResourceUtils.getFile(videoPath); } catch (FileNotFoundException e) { e.printStackTrace(); } String picPath = \"video.jpg\"; getVideoPic(video, picPath); long duration = getVideoDuration(video); System.out.println(\"videoDuration = \" + duration);} 示例源码欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"java","slug":"java","permalink":"https://blog.mariojd.cn/tags/java/"},{"name":"javacv","slug":"javacv","permalink":"https://blog.mariojd.cn/tags/javacv/"},{"name":"ffmpeg","slug":"ffmpeg","permalink":"https://blog.mariojd.cn/tags/ffmpeg/"}]},{"title":"Python 程序打包工具:py2exe 和 PyInstaller","slug":"Python 程序打包工具:py2exe 和 PyInstaller","date":"2019-02-14","updated":"2019-02-15","comments":true,"path":"build-python-program-with-py2exe-or-pyinstaller.html","link":"","permalink":"https://blog.mariojd.cn/build-python-program-with-py2exe-or-pyinstaller.html","excerpt":"","keywords":"","text":"通常执行 python 程序要有相应的 Python 环境,但某些特定场景下,我们可能并不愿意这么麻烦的去配置这些环境(比如将写好的脚本发给客户进行操作),如果可以提前将程序打包成 Windows平台的 .exe 文件或者是Linux下的 .sh 脚本,那么使用起来就会方便很多,py2exe 和 PyInstaller 这两款工具都是干这么个事的,下面以 hello.py 脚本(代码内容如下)为例进行介绍。 age = input(\"How old are you?\\n\")print(\"A: \" + age) 提示:PyInstaller 可以在 Windows 和 Linux 下使用,更推荐使用,而 py2exe 暂不支持 Linux 平台 PyInstaller 安装 pip install pyinstaller 使用 常见的用法有: 生成单个可执行文件:pyinstaller -F hello.py 生成指定icon的可执行文件:pyinstaller -i xxx.ico hello.py 在当前目录下的 dist 文件夹内可以找到生成后的可执行文件(脚本),更多用法请参考说明 py2exe 安装 pip install py2exe 使用 如上图,打包失败了,留意到这里说不支持 python3.6,果断放弃,有兴趣的可以自行降低到 python3.4 或 python3.5 进行尝试。 欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.mariojd.cn/categories/Python/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.mariojd.cn/tags/python/"},{"name":"py2exe","slug":"py2exe","permalink":"https://blog.mariojd.cn/tags/py2exe/"},{"name":"pyinstaller","slug":"pyinstaller","permalink":"https://blog.mariojd.cn/tags/pyinstaller/"}]},{"title":"Spring Boot 项目参数校验的常见使用场景","slug":"Java 服务端参数校验 - JSR 303介绍及实践","date":"2019-01-30","updated":"2019-06-02","comments":true,"path":"java-parameter-and-bean-validation.html","link":"","permalink":"https://blog.mariojd.cn/java-parameter-and-bean-validation.html","excerpt":"","keywords":"","text":"可以说几乎所有的应用场景中,参数验证都在编写业务逻辑前完成,严格确保进来的数据是合法且符合要求的。 Java Web 开发领域,也早有较为完善的 Bean Validation 为 Java Bean 验证定义了相应的元数据模型和 API。首先,在项目中引入 web 模块的依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency> Hibernate Validator 是 Bean Validation 的一种实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,以及一些附加的 constraint。如果想了解更多请查看 http://www.hibernate.org/subprojects/validator.html 具体以及常用的 constraint 包含如下: @Datapublic class Validate { // 空和非空检查: @Null、@NotNull、@NotBlank、@NotEmpty @Null(message = \"验证是否为 null\") private Integer isNull; @NotNull(message = \"验证是否不为 null, 但无法查检长度为0的空字符串\") private Integer id; @NotBlank(message = \"检查字符串是不是为 null,以及去除空格后长度是否大于0\") private String name; @NotEmpty(message = \"检查是否为 NULL 或者是 EMPTY\") private List<String> stringList; // Boolean值检查: @AssertTrue、@AssertFalse @AssertTrue(message = \" 验证 Boolean参数是否为 true\") private Boolean isTrue; @AssertFalse(message = \"验证 Boolean 参数是否为 false \") private Boolean isFalse; // 长度检查: @Size、@Length @Size(min = 1, max = 2, message = \"验证(Array,Collection,Map,String)长度是否在给定范围内\") private List<Integer> integerList; @Length(min = 8, max = 30, message = \"验证字符串长度是否在给定范围内\") private String address; // 日期检查: @Future、@FutureOrPresent、@Past、@PastOrPresent @Future(message = \"验证日期是否在当前时间之后\") private Date futureDate; @FutureOrPresent(message = \"验证日期是否为当前时间或之后\") private Date futureOrPresentDate; @Past(message = \"验证日期是否在当前时间之前\") private Date pastDate; @PastOrPresent(message = \"验证日期是否为当前时间或之前\") private Date pastOrPresentDate; // 其它检查: @Email、@CreditCardNumber、@URL、@Pattern、@ScriptAssert、@UniqueElements @Email(message = \"校验是否为正确的邮箱格式\") private String email; @CreditCardNumber(message = \"校验是否为正确的信用卡号\") private String creditCardNumber; @URL(protocol = \"http\", host = \"127.0.0.1\", port = 8080, message = \"校验是否为正确的URL地址\") private String url; @Pattern(regexp = \"^1[3|4|5|7|8][0-9]{9}$\", message = \"正则校验是否为正确的手机号\") private String phone; // 对关联对象元素进行递归校验检查 @Valid @UniqueElements(message = \"校验集合中的元素是否唯一\") private List<CalendarEvent> calendarEvent; @Data @ScriptAssert(lang = \"javascript\", script = \"_this.startDate.before(_this.endDate)\", message = \"通过脚本表达式校验参数\") private class CalendarEvent { private Date startDate; private Date endDate; } // 数值检查: @Min、@Max、@Range、@DecimalMin、@DecimalMax、@Digits @Min(value = 0, message = \"验证数值是否大于等于指定值\") @Max(value = 100, message = \"验证数值是否小于等于指定值\") @Range(min = 0, max = 100, message = \"验证数值是否在指定值区间范围内\") private Integer score; @DecimalMin(value = \"10.01\", inclusive = false, message = \"验证数值是否大于等于指定值\") @DecimalMax(value = \"199.99\", message = \"验证数值是否小于等于指定值\") @Digits(integer = 3, fraction = 2, message = \"限制整数位最多为3,小数位最多为2\") private BigDecimal money;} 常见的前后端分离开发模式,数据通信通常以 JSON 为主。针对 POST 和 PUT 请求,一般通过新建域(对象)模型来进行数据绑定和校验,constraint 通常附加在这些域模型的字段上(如上): /** * Valid注解标明要对参数对象进行数据校验 */ @PutMapping @PostMapping public Map<String, Object> test01(@RequestBody @Valid Validate validate, BindingResult bindingResult) { Map<String, Object> map = new HashMap<>(4); if (bindingResult.hasErrors()) { String errorMsg = bindingResult.getFieldErrors().stream().map(FieldError::getDefaultMessage) .collect(Collectors.joining(\",\")); map.put(\"errorMsg\", errorMsg); } map.put(\"params\", validate.toString()); return map; } 此外,对于 GET 和 DELETE 请求,参数通常为 key1=value1&key2=value2 这种形式。默认情况下,Hibernate Validator 只能对 Object 属性进行校验,并不能对单个参数进行校验,Spring 在此基础上进行了扩展,通过配置 MethodValidationPostProcessor 处理器,可以实现对方法参数的拦截校验。 @Configurationpublic class ValidateConfig { /** * 配置MethodValidationPostProcessor拦截器,以实现对方法参数的校验 */ @Bean public MethodValidationPostProcessor methodValidationPostProcessor() { return new MethodValidationPostProcessor(); }} 注意,要在 Controller 类上明确标明 @Validated: @Validated@RestController@RequestMapping(\"validate\")public class ValidateController { @GetMapping @DeleteMapping public Map<String, Object> test02(@NotNull(message = \"id不能为空\") @Range(min = 1, max = 100, message = \"id最小为1最大为100\") Integer id, @NotBlank(message = \"email不能为空\") @Email(message = \"邮箱格式错误\") String email, @ModelAttribute @Valid Validate validate) { Map<String, Object> map = new HashMap<>(4); map.put(\"id\", id); map.put(\"email\", email); map.put(\"params\", validate.toString()); return map; }} 上述这种形式的参数要是校验失败,错误提示明显并不友好,通过捕获此类异常就可以解决,这里就不再介绍了。 参考链接 JSR 303 - Bean Validation 介绍及最佳实践SpringBoot-服务端参数验证-JSR-303验证框架 示例源码欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"https://blog.mariojd.cn/tags/Spring-Boot/"},{"name":"JSR 303","slug":"JSR-303","permalink":"https://blog.mariojd.cn/tags/JSR-303/"}]},{"title":"Docker 下的 MongoDB + Mongo-Express 环境搭建","slug":"Docker 下的 MongoDB + Mongo-Express 环境搭建","date":"2019-01-11","updated":"2019-01-15","comments":true,"path":"build-mongo-and-mongo-express-environment-under-docker.html","link":"","permalink":"https://blog.mariojd.cn/build-mongo-and-mongo-express-environment-under-docker.html","excerpt":"","keywords":"","text":"MongoDB 是一种面向文档的、介于关系型数据库和非关系型数据库的系统,Mongo-Express 则是一款图形化的 MongoDB web 客户端管理工具,使用 Node.js、Express 和 Bootstrap3 编写。去年,MongoDB 4.0 正式发布,新特性中包含支持 ACID 事务,这也使得 MongoDB 在今后的作用和优势将会越来越明显。 当前最新的 MongoDB 版本是 4.1.6,下面使用 Docker 简单示例下环境搭建: 启动 MongoDB,设置 root用户及密码 docker run -d -p 27017:27017 --name mongodb -e MONGO_INITDB_ROOT_USERNAME=mongoadmin -e MONGO_INITDB_ROOT_PASSWORD=mongoadmin mongo:4.1.6 启动 Mongo-Express,设置登录用户及密码 docker run -it --restart=always --name mongo-express --link mongodb:mongo-db -d -p 8081:8081 -e ME_CONFIG_OPTIONS_EDITORTHEME=\"3024-night\" -e ME_CONFIG_BASICAUTH_USERNAME=\"mongoexpress\" -e ME_CONFIG_BASICAUTH_PASSWORD=\"mongoexpress\" -e ME_CONFIG_MONGODB_ADMINUSERNAME=\"mongoadmin\" -e ME_CONFIG_MONGODB_ADMINPASSWORD=\"mongoadmin\" mongo-express 补充:docker network ls可以查看当前所有的 Docker NETWORK 也可以用 Docker Compose 来合并上两步操作: 创建 stack.yml version: '3.1'services: mongo: image: mongo:4.1.6 ports: - 27017:27017 restart: always environment: MONGO_INITDB_ROOT_USERNAME: mongoadmin MONGO_INITDB_ROOT_PASSWORD: mongoadmin mongo-express: links: - mongo image: mongo-express restart: always ports: - 8081:8081 environment: ME_CONFIG_OPTIONS_EDITORTHEME: 3024-night ME_CONFIG_BASICAUTH_USERNAME: mongoexpress ME_CONFIG_BASICAUTH_PASSWORD: mongoexpress ME_CONFIG_MONGODB_ADMINUSERNAME: mongoadmin ME_CONFIG_MONGODB_ADMINPASSWORD: mongoadmin 启动 docker-compose -f stack.yml up 参考链接 Install Docker ComposeDocker Hub - MongoDBDocker Hub - Mongo-ExpressCannot link to a running container started by docker-compose 欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Docker","slug":"Docker","permalink":"https://blog.mariojd.cn/categories/Docker/"}],"tags":[{"name":"docker","slug":"docker","permalink":"https://blog.mariojd.cn/tags/docker/"},{"name":"mongodb","slug":"mongodb","permalink":"https://blog.mariojd.cn/tags/mongodb/"},{"name":"mongo-express","slug":"mongo-express","permalink":"https://blog.mariojd.cn/tags/mongo-express/"},{"name":"docker compose","slug":"docker-compose","permalink":"https://blog.mariojd.cn/tags/docker-compose/"}]},{"title":"Chrome 开发者工具的小技巧","slug":"Chrome 开发者工具的小技巧","date":"2018-12-10","updated":"2018-12-14","comments":true,"path":"tips-for-chrome-developer-tools.html","link":"","permalink":"https://blog.mariojd.cn/tips-for-chrome-developer-tools.html","excerpt":"","keywords":"","text":"来源:陈皓 - 酷壳 CoolShell ;链接:https://coolshell.cn/articles/17634.html Chrome的开发者工具是个很强大的东西,相信程序员们都不会陌生,不过有些小功能可能并不为大众所知,所以,写下这篇文章罗列一下可能你所不知道的功能,有的功能可能会比较实用,有的则不一定,也欢迎大家补充交流。 代码格式化 有很多css/js的代码都会被 minify 掉,你可以点击代码窗口左下角的那个 { } 标签,chrome会帮你给格式化掉。 强制DOM状态 有些HTML的DOM是有状态的,比如<a> 标签,其会有 active,hover, focus,visited这些状态,有时候,我们的CSS会来定关不同状态的样式,在分析网页查看网页上DOM的CSS样式时,我们可以点击CSS样式上的 :hov 这个小按钮来强制这个DOM的状态。 动画 现在的网页上都会有一些动画效果。在Chrome的开发者工具中,通过右上角的菜单中的 More Tools => Animations 呼出相关的选项卡。于是你就可以慢动作播放动画了(可以点选 25% 或 10%),然后,Chrome还可以帮你把动画录下来,你可以拉动动再画的过程,甚至可以做一些简单的修改。 直接编辑网页 在你的 console 里 输入下面的命令: document.designMode = \"on\"; 于是你就可以直接修改网页上的内容了。 P.S. 下面这个抓屏中还演示了一个如何清空console的示例。你可以输入 clear() 或是 按 Ctrl+L(Windows下),CMD + K (Mac下) 网络限速 你可以设置你的网络的访问速度来模拟一个网络很慢的情况。 复制HTTP请求 这个是我很喜欢 的一个功能,你可以在 network选项卡里,点击 XHR 过滤相关的Ajax请求,然后在相关的请求上点鼠标右键,在菜单中选择: Copy => Copy as cURL,然后就可以到你的命令行下去 执行 curl 的命令了。这个可以很容易做一些自动化的测试。 友情提示:这个操作有可能会把你的个人隐私信息复制出去,比如你个人登录后的cookie。 抓个带手机的图 这个可能有点无聊了,不过我觉得挺有意思的。 在device显示中,先选择一个手机,然后在右上角选 Show Device Frame,然后你就看到手机的样子了,然后再到那个菜中中选 Capture snapshot,就可以抓下一个有手机样子的截图了。 我抓的图如下(当然,不是所有的手机都有frame的) 设置断点 除了给Javascript的源代码上设置断点调试,你还可以: 给DOM设置断点选中一个DOM,然后在右键菜单中选 Break on … 你可以看到如下三个选项: 给XHR和Event Lisener设置断点在 Sources 面页中,你可以看到右边的那堆break points中,除了上面我们说的给DOM设置断点,你还可以给XHR和Event Listener设置断点,载图如下: 关于Console中的技巧 DOM操作 chrome会帮你buffer 5个你查看过的DOM对象,你可以直接在Console中用 $0, $1, $2, $3, $4来访问。 你还可以使用像jQuery那样的语法来获得DOM对象,如:$("#mydiv") 你还可使用 $$(".class") 来选择所有满足条件的DOM对象。 你可以使用 getEventListeners($("selector")) 来查看某个DOM对象上的事件(如下图所示)。 你还可以使用 monitorEvents($("selector")) 来监控相关的事件。比如: monitorEvents(document.body, \"click\"); Console中的一些函数1)monitor函数 使用 monitor函数来监控一函数,如下面的示例 2)copy函数 copy函数可以把一个变量的值copy到剪贴板上。 3)inspect函数 inspect函数可以让你控制台跳到你需要查看的对象上。如: 更多的函数请参数官方文档:Using the Console / Command Line Reference Console的输出我们知道,除了console.log之外,还有console.debug,console.info,console.warn,console.error这些不同级别的输出。另外一个鲜为人知的功能是,console.log中,你还可以对输出的文本加上css的样式,如下所示: console.log(\"%c左耳朵\", \"font-size:90px;color:#888\"); 于是,你可以定义一些相关的log函数,如: console.todo = function( msg){ console.log( '%c%s %s %s', 'font-size:20px; color:yellow; background-color: blue;', '--', msg, '--');}console.important = function( msg){ console.log( '%c%s %s %s', 'font-size:20px; color:brown; font-weight: bold; text-decoration: underline;', '--', msg, '--');} 关于console.log中的格式化,你可以参看如下表格: 指示符 输出 %s 格式化输出一个字符串变量 %i or %d 格式化输出一个整型变量的值 %f 格式化输出一个浮点数变量的值 %o 格式化输出一个DOM对象 %O 格式化输出一个Javascript对象 %c 为后面的字符串加上CSS样式 除了console.log打印js的数组,你还可以使用console.table来打印,如下所示: var pets = [ { animal: 'Horse', name: 'Pony', age: 23 }, { animal: 'Dog', name: 'Snoopy', age: 13 }, { animal: 'Cat', name: 'Tom', age: 18 }, { animal: 'Mouse', name: 'Jerry', age: 12}];console.table(pets) 关于console对象 console对象除了上面的打日志的功能,其还有很多功能,比如: console.trace() 可以打出js的函数调用栈 console.time() 和 console.timeEnd() 可以帮你计算一段代码间消耗的时间。 console.profile() 和 console.profileEnd() 可以让你查看CPU的消耗。 console.count() 可以让你看到相同的日志当前被打印的次数。 console.assert(expression, object) 可以让你assert一个表达式 这些东西都可以看看Google的Console API的文档。 其实,还有很多东西,你可以参看Google的官方文档 - Chrome DevTools 关于快捷键 点击在 DevTools的右上角的那三个坚排的小点,你会看到一个菜单,点选 Shortcuts,你就可以看到所有的快捷键了 如果你知道更多,也欢迎补充!(全文完) 欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Web","slug":"Web","permalink":"https://blog.mariojd.cn/categories/Web/"}],"tags":[{"name":"chrome","slug":"chrome","permalink":"https://blog.mariojd.cn/tags/chrome/"}]},{"title":"Java 异常知识点思考与总结","slug":"Java 异常知识点思考与总结","date":"2018-12-05","updated":"2019-06-02","comments":true,"path":"java-exception.html","link":"","permalink":"https://blog.mariojd.cn/java-exception.html","excerpt":"","keywords":"","text":"Java 中的异常可以是方法执行过程中引发的,也可以是通过 throw 语句手动抛出的。一旦程序运行过程中发生了异常,JRE 就会试图寻找异常处理程序来处理异常,用具体的异常对象来包装该异常。 Throwable 类是 Java 异常类的顶层父类,一个对象只有是 Throwable 类的(直接或者间接)实例,它才是一个异常对象,才可以被抛出(throw)或者捕获(catch),才能被异常处理机制识别和处理。除了 JDK 中内建的常用异常类,还允许我们自定义异常。 异常划分 可以看到,Throwable 派生出 Error 和 Exception ,这体现了 Java 平台设计者针对不同异常情况的合理分类。其中,Exception 是指应用正常运行中,可以被预料的意外情况,程序捕获后可以进行相应的处理。Error 是指在正常情况下,不大可能出现的情况,而绝大多数的 Error 都会导致程序进入非正常的、不可恢复的状态,Error 类错误通常不可以被捕获,如 OutOfMemoryError、NoClassDefFoundError。 对于程序来说,异常又可以划分为应检查(checked)异常和不检查(unchecked)异常,应检查异常要求必须在代码里进行显式捕获和处理,javac 会在编译期间就进行检查。 不检查异常(unchecked exception): 不检查异常就是所谓的运行时异常,通常是可以通过编码来避免的一些逻辑错误,包括 Error 和 RuntimeException 以及他们的子类。javac在编译时,不会提示和发现此类异常,即不要求通过代码显示处理这些异常。对于这些异常,我们应该修正代码,而不是通过异常处理器来解决,如除0错误:ArithmeticException,强制类型转换错误:ClassCastException,数组越界异常:ArrayIndexOutOfBoundsException,使用了空对象出现的 NullPointerException 等。 应检查异常(checked exception): 除了 Error 和 RuntimeException 的其它异常。javac 强制要求处理的异常,可以用 try-catch-finally 或 try-with-resources 语句捕获并处理,也可以使用 throws 往上抛出,否则编译不会通过。应检查异常通常是由程序的运行环境所导致的,而这些在程序运行过程中是无法提前预知的,于是代码中就应该为这样的异常提前准备,如SQLException , IOException和ClassNotFoundException 等。 函数通常是层级调用的,进而形成调用栈,而异常则是执行某个函数时所引发的。因此,只要在方法调用的过程中发生了异常,那么他的所有 caller 都会被异常影响,当这些被影响的函数以异常信息输出时,就形成的了异常追踪栈(如上图所示)。所以,异常最先发生的地方,也叫做异常抛出点。 try…catch…finally 语句块try (ServletOutputStream outputStream = response.getOutputStream()) { // Try-with-resources // 1. try块中放可能发生异常的代码 // 1.1 如果执行完try且不发生异常,则接着去执行finally块和finally后面的代码(如果有的话) // 1.2 如果发生了异常,则会先尝试去匹配catch块,最后再执行finally块(如果有的话)} catch (ClassCastException | IndexOutOfBoundsException e) { // Multiple catch // 1. 每一个catch块用于捕获并处理一个特定的异常,或者这个异常类型的子类。Java7提供的multiple catch新特性,可以将多个异常声明在一个catch中 // 2. catch后面的括号定义了异常类型和异常参数。如果异常与之匹配且是最先匹配到的,则虚拟机将使用这个catch块来处理异常 // 3. 在catch块中可以使用这个块的异常参数来获取异常的相关信息。异常参数是这个catch块中的局部变量,其它块不能访问 // 4. 如果当前try块中发生的异常在后续的所有catch中都没捕获到,则先去执行finally,然后到这个函数的外部caller中去匹配异常处理器 // 5. 如果try中没有发生异常,则所有的catch块将被忽略} catch (Exception e) { // 1. 异常匹配是按照catch块的顺序从上往下寻找的,只有第一个匹配的catch会得到执行。匹配时,不仅运行精确匹配,也支持父类匹配 // 2. 如果同一个try块下的多个catch异常类型有父子关系,应该将子类异常放在前面,父类异常放在后面,确保每个catch块都有其存在的意义 // 3. 异常处理就是将执行控制流从异常发生的地方转移到能够处理这种异常的地方。当一个函数的某条语句发生异常时,这条语句的后面的语句就不会再执行了,它失去了焦点} finally { // 1. finally块是可选的 // 2. 无论异常是否发生,异常是否匹配被处理,finally都最终会执行 // 3. 一个try至少要有一个catch块,否则至少要有1个finally块。但是finally不是用来处理异常的,finally不会捕获异常 // 4. finally主要做一些资源的清理工作,比如流的关闭,数据库连接的关闭等;Java7及以后的版本中,更是推荐使用try-with-resources这种新特性来简化这些操作 } throws 异常声明throws 是另一种处理异常的方式,它不同于 try…catch…finally,throws仅仅是将函数中可能出现的异常向调用者声明,其本身并不进行处理。 采取这种异常处理的原因大多是:方法编写者本身不知道如何处理这样的异常,或者说让调用方来处理会更好,从而让调用方来为可能发生的异常负责。 public void example() throws IOException { } finally 块try块中的代码执行完后,finally块是一定执行的。但也有一种比较特殊的情况,就是在这之前执行了System.exit()。 finally块通常用来做资源的释放、关闭文件等操作。良好的编程习惯是:在try块中打开资源,在finally块中清理并释放这些资源,Java7之后更是推荐直接使用try-with-resources。 下面简单总结一下: finally块没有处理异常的能力,处理异常的只能是catch块; 在同一try…catch…finally块中 ,如果try中抛出异常,且有匹配的catch块,则先执行catch块,再执行finally块。如果没有catch块匹配,则先执行finally,然后去外面的调用者中寻找合适的catch块; 在同一try…catch…finally块中 ,try发生异常,且匹配的catch块中处理异常时也抛出异常,后面的finally还是会先执行,最后才去外围调用者中寻找合适的catch块。 补充几点开发建议: 不要在fianlly中使用return 不要在finally中向外抛出异常 不要在finally中做除了释放资源的其它的事情 用try-with-resources避免finally 自定义异常扩展自Exception类的自定义异常,属于应检查异常(checked exception)。如果要自定义非检查异常(unchecked exception),继承RuntimeException即可。 通常情况下,自定义的异常应该总是包含如下的构造器,具体可以参考jdk中自带的异常是如何定义的: 一个无参构造函数 一个带有 String 参数的构造函数,并传递给父类的构造函数。 一个带有 String 参数和 Throwable 参数,并都传递给父类构造函数 一个带有 Throwable 参数的构造函数,并传递给父类的构造函数。 异常案例异常处理可谓也是一门艺术活。下面列举了一些错误的、常见的异常处理方式,你可以通过阅读代码来提前思考,判断这些异常处理中,具体有哪些不当之处: 示例一try { Thread.sleep(1000L);} catch (Exception e) { // do nothing...} 尽量不要捕获类似 Exception 这样的通用异常,而是应该捕获特定异常,在这里 Thread.sleep() 抛出的 InterruptedException 不要生吞(swallow)异常,这是异常处理中特别要注意的事情,因为很可能会导致非常难以诊断的诡异情况。如果我们没有把异常抛出来,或者也没有输出到日志(Logger)之类,程序可能在后续代码以不可控的方式结束。没人能够轻易判断究竟是哪里抛出了异常,以及是什么原因产生了异常 示例二try { // …} catch (Exception e) { e.printStackTrace();} 开发或测试环境中,上面这段代码是没有问题的,但在产品代码中,是绝对不允许这样处理的。来看看printStackTrace()的文档,开头就是“Prints this throwable and its backtrace to the standard error stream”。问题就在这里,在稍微复杂一点的应用中,标准错误流(STERR)并不是个合适的输出选项,因为你很难判断异常到底输出到哪里了。 示例三public void test(String fileName) { // 提前校验参数是否合法 Assert.isTrue(!\"\".equals(fileName) && Objects.nonNull(fileName), \"file name is not empty\"); File file = new File(fileName); // ...} 遵循 Throw early, catch later 原则。如果 fileName 是 null 或者 空字符串,那么后面程序获取文件时肯定会抛出异常。提前校验并且抛出异常,可以更加清晰地反映问题。 示例四@Servicepublic class UserService { @Resource private UserMapper userMapper; @Transactional public void insert(User user) { // 插入用户信息 userMapper.insertUser(user); // 手动抛出异常 throw new SQLException(\"数据库异常\"); }} 上述这段代码中,异常并没有被捕获到,所以事务并不会回滚。 Spring Boot 默认的事务规则是遇到运行异常(RuntimeException)和程序错误(Error)才会回滚。而 SQLException 是非运行异常,继承自 Exception。解决上述问题,需要在 @Transactional 注解中使用 rollbackFor 属性来指定异常:@Transactional(rollbackFor = Exception.class) 示例五@Servicepublic class UserService { @Resource private UserMapper userMapper; @Transactional(rollbackFor = Exception.class) public void insert(User user) { try { // 插入用户信息 userMapper.insertUser(user); // 手动抛出异常 throw new SQLException(\"数据库异常\"); } catch (Exception e) { // 异常处理逻辑 } }} 同样的,事务并没有因为抛出异常而回滚,这是因为 try…catch 把异常生吞了,这个细节往往比上面那个坑更加难发现。 示例六@Servicepublic class UserService { @Resource private UserMapper userMapper; @Transactional(rollbackFor = Exception.class) public synchronized void insert(User user) { // 插入用户信息 userMapper.insertUser(user); } } 上述代码中,synchronized 并不会生效,原因是因为事务的范围比锁的范围大。加锁的那部分代码执行完之后,锁释放掉了但事务还没结束,此时假设另外一个线程进来了,事务没结束的话,插入动作就会产生脏数据。解决办法有两种,第一,去掉事务(不推荐);第二,在调用该方法的地方加锁,保证锁的范围比事务的范围大即可。 性能分析从性能的角度来审视一下 Java 的异常处理机制,这里有两个相对昂贵的地方: try-catch 代码段会产生额外的性能开销,换个角度说,它往往会影响 JVM 对代码进行优化,因此建议仅捕获有必要的代码段,尽量不要用一个大的 try 块包住整段的代码;与此同时,利用异常控制代码流程,也不是一个好主意,这远比通常意义上的条件语句(if/else、switch)要低效 每实例化一个 Exception,都会对当前的栈进行快照,这是一个相对比较重的操作。如果发生的非常频繁,这个性能开销就不能被忽略了 推荐阅读 Java 常用异常整理Java 中的异常和处理详解如何优雅的设计 Java 异常Java 中 9 个处理 Exception 的最佳实践改进异常处理的6条建议程序员如何处理被 “吃” 掉的异常?Spring MVC 异常处理详解谈谈异常 欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"java exception","slug":"java-exception","permalink":"https://blog.mariojd.cn/tags/java-exception/"},{"name":"java error","slug":"java-error","permalink":"https://blog.mariojd.cn/tags/java-error/"}]},{"title":"Spring Data JPA 的时间注解:@CreatedDate 和 @LastModifiedDate","slug":"Spring Data JPA 时间注解:@CreatedDate 和 @LastModifiedDate","date":"2018-12-04","updated":"2019-04-02","comments":true,"path":"time-annotation-in-spring-data-jpa.html","link":"","permalink":"https://blog.mariojd.cn/time-annotation-in-spring-data-jpa.html","excerpt":"","keywords":"","text":"选择 Spring Data JPA 框架开发时,常用在实体和字段上的注解有@Entity、@Id、@Column等。在表设计规范中,通常建议保留的有两个字段,一个是更新时间,一个是创建时间。Spring Data JPA 提供了相应的时间注解,只需要两步配置,就可以帮助开发者快速实现这方面的功能。 在实体类上加上注解 @EntityListeners(AuditingEntityListener.class),在相应的字段上添加对应的时间注解 @LastModifiedDate 和 @CreatedDate 注意:日期类型可以用 Date 也可以是 Long @Entity@EntityListeners(AuditingEntityListener.class)public class User { /** * 自增主键 */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; /** * 更新时间 */ @LastModifiedDate @Column(nullable = false) private Long updateTime; /** * 创建时间 */ @CreatedDate @Column(updatable = false, nullable = false) private Date createTime; // 省略getter和setter 在Application启动类中添加注解 @EnableJpaAuditing @EnableJpaAuditing@SpringBootApplicationpublic class TestApplication { public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); } } 此外,Spring Data JPA 还提供 @CreatedBy 和 @LastModifiedBy 注解,用于保存和更新当前操作用户的信息(如id、name)。如果有这方面的需求,可以参考下面的配置实现: @Entity@EntityListeners(AuditingEntityListener.class)public class User { /** * 自增主键 */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; /** * 更新时间 */ @LastModifiedDate @Column(nullable = false) private Long updateTime; /** * 创建时间 */ @CreatedDate @Column(updatable = false, nullable = false) private Date createTime; /** * 创建人 */ @CreatedBy private Integer createBy; /** * 最后修改人 */ @LastModifiedBy private Integer lastModifiedBy; // 省略getter和setter 配置实现AuditorAware接口,以获取字段需要插入的信息: @Configurationpublic class AuditorConfig implements AuditorAware<Integer> { /** * 返回操作员标志信息 * * @return */ @Override public Optional<Integer> getCurrentAuditor() { // 这里应根据实际业务情况获取具体信息 return Optional.of(new Random().nextInt(1000)); }} 2019.04 补充Hibernate 也提供了类似上述时间注解的功能实现,这种方法只需要一步配置,更改为注解 @UpdateTimestamp 和 @CreationTimestamp 即可(参考如下): @Data@MappedSuperclass@NoArgsConstructor@AllArgsConstructorpublic class BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @UpdateTimestamp @Column(nullable = false) private Date updateTime; @CreationTimestamp @Column(nullable = false, updatable = false) private Date createTime; @NotNull private Boolean deleted = false;} 欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"spring boot","slug":"spring-boot","permalink":"https://blog.mariojd.cn/tags/spring-boot/"},{"name":"spring data jpa","slug":"spring-data-jpa","permalink":"https://blog.mariojd.cn/tags/spring-data-jpa/"}]},{"title":"Spring Boot 1.0 && 2.0 + Mybatis 多数据源配置与使用","slug":"Spring Boot 1.0 && 2.0 + Mybatis 多数据源配置与使用","date":"2018-11-22","updated":"2018-11-24","comments":true,"path":"spring-boot-mybatis-multiple-datasource.html","link":"","permalink":"https://blog.mariojd.cn/spring-boot-mybatis-multiple-datasource.html","excerpt":"","keywords":"","text":"环境说明 Spring Boot 1.5.17.RELEASE 或 Spring Boot 2.1.0.RELEASE MySQL v5.6.19 PostgreSQL v10.4 无特殊说明,以下所说的环境均指 Spring Boot 2.1.0.RELEASE,如果使用的是 Spring Boot 1.5.17.RELEASE 这个版本,只需要调整下面有做说明的几处地方 连接配置在application.yml中定义如下信息: spring: jpa: hibernate: # 多数据源下,该属性不生效,需要在配置中额外指定,这里仅表示普通定义 ddl-auto: create-drop properties: hibernate: show_sql: true format_sql: true jdbc: lob: non_contextual_creation: true open-in-view: false # 定义不同数据源的连接信息 datasource: hikari: mysql: # Spring Boot 1.0+ 版本:使用spring.datasource.url # Spring Boot 2.0+ 版本:使用spring.datasource.hikari.jdbc-url jdbc-url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8 username: root password: root # Spring Boot 1.0+ 版本:使用com.mysql.jdbc.Driver # Spring Boot 2.0+ 版本:使用com.mysql.cj.jdbc.Driver driver-class-name: com.mysql.cj.jdbc.Driver postgres: jdbc-url: jdbc:postgresql://localhost:5432/postgres username: postgres password: postgres driver-class-name: org.postgresql.Driver 配置数据源根据上面定义的配置信息,配置这两个数据源: // Spring Boot 1.0+ ,DataSourceBuilder所在包位置为:org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder// Spring Boot 2.0+ ,DataSourceBuilder所在包位置为:org.springframework.boot.jdbc.DataSourceBuilder@Configurationpublic class DataSourceConfig { @Primary @Bean @ConfigurationProperties(\"spring.datasource.hikari.mysql\") public DataSource mysqlDataSource() { return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties(\"spring.datasource.hikari.postgres\") public DataSource postgresDataSource() { return DataSourceBuilder.create().build(); }} Mybatis 支持添加 mysql 对应数据源的 Mybatis 支持: @Configuration@MapperScan( // 数据层所在包位置 basePackages = \"cn.mariojd.springboot.multiple.datasource.mybatis.mysql.mapper\", sqlSessionTemplateRef = \"mybatisMysqlSqlSessionTemplate\")public class MybatisMysqlDataSourceConfig { @Resource @Qualifier(\"mysqlDataSource\") private DataSource dataSource; @Bean @Primary public SqlSessionFactory mybatisMysqlSqlSessionFactory() throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); // 如果是xml形式,需要在此处指定mapper位置 // factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(\"classpath:mybatis/mysql/mapper/*.xml\")); return factoryBean.getObject(); } @Bean @Primary public DataSourceTransactionManager mybatisMysqlTransactionManager() { return new DataSourceTransactionManager(dataSource); } @Bean @Primary public SqlSessionTemplate mybatisMysqlSqlSessionTemplate(@Qualifier(\"mybatisMysqlSqlSessionFactory\") SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); }} 添加 postgres 对应数据源的 Mybatis 支持: @Configuration@MapperScan( // 数据层所在包位置 basePackages = \"cn.mariojd.springboot.multiple.datasource.mybatis.postgres.mapper\", sqlSessionTemplateRef = \"mybatisPostgresSqlSessionTemplate\")public class MybatisPostgresDataSourceConfig { @Resource @Qualifier(\"postgresDataSource\") private DataSource dataSource; @Bean public SqlSessionFactory mybatisPostgresSqlSessionFactory() throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); // 如果是xml形式,需要在此处指定mapper位置 // factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(\"classpath:mybatis/postgres/mapper/*.xml\")); return factoryBean.getObject(); } @Bean public DataSourceTransactionManager mybatisPostgresTransactionManager() { return new DataSourceTransactionManager(dataSource); } @Bean public SqlSessionTemplate mybatisPostgresSqlSessionTemplate(@Qualifier(\"mybatisPostgresSqlSessionFactory\") SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); }} 相关定义mysql 对应的数据源配置中,定义了实体 Boy 和对应的数据层接口 BoyMapper: @Data@AllArgsConstructor@NoArgsConstructorpublic class Boy { private Integer id; private String name;} public interface BoyMapper { @Insert(\"INSERT INTO boy(id,name) VALUES(#{boy.id},#{boy.name})\") @Options(useGeneratedKeys = true, keyProperty = \"boy.id\") int insert(@Param(\"boy\") Boy boy); @Select(\"SELECT id,name FROM boy\") @Results({ @Result(property = \"id\", column = \"id\", javaType = Integer.class, jdbcType = JdbcType.INTEGER), @Result(property = \"name\", column = \"name\", javaType = String.class, jdbcType = JdbcType.VARCHAR), }) List<Boy> findAll();} postgres 对应的数据源配置中,定义了实体 Girl 和对应的数据层接口 GirlMapper: @Data@AllArgsConstructor@NoArgsConstructorpublic class Girl { private Integer id; private String name;} public interface GirlMapper { @Insert(\"INSERT INTO girl(id,name) VALUES(#{girl.id},#{girl.name})\") @Options(useGeneratedKeys = true, keyProperty = \"girl.id\") int insert(@Param(\"girl\") Girl girl); @Select(\"SELECT id,name FROM girl\") @Results({ @Result(property = \"id\", column = \"id\", javaType = Integer.class, jdbcType = JdbcType.INTEGER), @Result(property = \"name\", column = \"name\", javaType = String.class, jdbcType = JdbcType.VARCHAR), }) List<Girl> findAll();} 单元测试@RunWith(SpringRunner.class)@SpringBootTestpublic class SpringBootMybatisMultipleDataSourceTest { @Resource private BoyMapper boyMapper; @Resource private GirlMapper girlMapper; @Test public void test() { boyMapper.insert(new Boy(1, \"大肖\")); boyMapper.insert(new Boy(2, \"大熊\")); Assert.assertEquals(2, boyMapper.findAll().size()); girlMapper.insert(new Girl(1, \"小红\")); girlMapper.insert(new Girl(2, \"小花\")); Assert.assertEquals(2, girlMapper.findAll().size()); }} 示例源码欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"spring boot","slug":"spring-boot","permalink":"https://blog.mariojd.cn/tags/spring-boot/"},{"name":"mybatis","slug":"mybatis","permalink":"https://blog.mariojd.cn/tags/mybatis/"},{"name":"multiple datasource","slug":"multiple-datasource","permalink":"https://blog.mariojd.cn/tags/multiple-datasource/"}]},{"title":"Spring Boot 1.0 && 2.0 + JPA 多数据源配置与使用","slug":"Spring Boot 1.0 && 2.0 + JPA 多数据源配置与使用","date":"2018-11-21","updated":"2018-11-21","comments":true,"path":"spring-boot-jpa-multiple-datasource.html","link":"","permalink":"https://blog.mariojd.cn/spring-boot-jpa-multiple-datasource.html","excerpt":"","keywords":"","text":"环境说明 Spring Boot 1.5.17.RELEASE 或 Spring Boot 2.1.0.RELEASE MySQL v5.6.19 PostgreSQL v10.4 无特殊说明,以下所说的环境均指 Spring Boot 2.1.0.RELEASE,如果使用的是 Spring Boot 1.5.17.RELEASE 这个版本,只需要调整下面有做说明的几处地方 连接配置在application.yml中定义如下信息: spring: jpa: hibernate: # 多数据源下,该属性不生效,需要在配置中额外指定,这里仅表示普通定义 ddl-auto: create-drop properties: hibernate: show_sql: true format_sql: true jdbc: lob: non_contextual_creation: true open-in-view: false # 定义不同数据源的连接信息 datasource: hikari: mysql: # Spring Boot 1.0+ 版本:使用spring.datasource.url # Spring Boot 2.0+ 版本:使用spring.datasource.hikari.jdbc-url jdbc-url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8 username: root password: root # Spring Boot 1.0+ 版本:使用com.mysql.jdbc.Driver # Spring Boot 2.0+ 版本:使用com.mysql.cj.jdbc.Driver driver-class-name: com.mysql.cj.jdbc.Driver postgres: jdbc-url: jdbc:postgresql://localhost:5432/postgres username: postgres password: postgres driver-class-name: org.postgresql.Driver 配置数据源根据上面定义的配置信息,配置这两个数据源: // Spring Boot 1.0+ ,DataSourceBuilder所在包位置为:org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder// Spring Boot 2.0+ ,DataSourceBuilder所在包位置为:org.springframework.boot.jdbc.DataSourceBuilder@Configurationpublic class DataSourceConfig { @Primary @Bean @ConfigurationProperties(\"spring.datasource.hikari.mysql\") public DataSource mysqlDataSource() { return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties(\"spring.datasource.hikari.postgres\") public DataSource postgresDataSource() { return DataSourceBuilder.create().build(); }} JPA 支持添加 mysql 对应数据源的 JPA 支持: @Configuration@EnableTransactionManagement@EnableJpaRepositories( entityManagerFactoryRef = \"mysqlEntityManagerFactory\", transactionManagerRef = \"mysqlTransactionManager\", // 数据层所在的包位置 basePackages = \"cn.mariojd.springboot.multiple.datasource.jpa.mysql.repository\")public class MysqlDataSourceConfig { @Resource private Environment environment; @Resource @Qualifier(\"mysqlDataSource\") private DataSource dataSource; @Bean @Primary public LocalContainerEntityManagerFactoryBean mysqlEntityManagerFactory(EntityManagerFactoryBuilder builder) { Map<String, Object> properties = new HashMap<>(4); // Spring Boot 1.0+ ,使用MySQLDialect // Spring Boot 2.0+ ,指定MySQLDialect会默认使用MyISAM引擎,改成MySQL55Dialect即可 properties.put(\"hibernate.dialect\", \"org.hibernate.dialect.MySQL55Dialect\"); properties.put(\"hibernate.hbm2ddl.auto\", environment.getProperty(\"spring.jpa.hibernate.ddl-auto\")); return builder.dataSource(dataSource) .properties(properties) // 实体所在的包位置 .packages(\"cn.mariojd.springboot.multiple.datasource.jpa.mysql.entity\") .persistenceUnit(\"jpa-mysql\") .build(); } @Bean @Primary public PlatformTransactionManager mysqlTransactionManager(@Qualifier(\"mysqlEntityManagerFactory\") EntityManagerFactory entityManagerFactory) { return new JpaTransactionManager(entityManagerFactory); }} 添加 postgres 对应数据源的 JPA 支持: @Configuration@EnableTransactionManagement@EnableJpaRepositories( entityManagerFactoryRef = \"postgresEntityManagerFactory\", transactionManagerRef = \"postgresTransactionManager\", // 数据层所在的包位置 basePackages = \"cn.mariojd.springboot.multiple.datasource.jpa.postgres.repository\")public class PostgresDataSourceConfig { @Resource private Environment environment; @Resource @Qualifier(\"postgresDataSource\") private DataSource dataSource; @Bean public LocalContainerEntityManagerFactoryBean postgresEntityManagerFactory(EntityManagerFactoryBuilder builder) { Map<String, Object> properties = new HashMap<>(4); properties.put(\"hibernate.dialect\", \"org.hibernate.dialect.PostgreSQLDialect\"); properties.put(\"hibernate.hbm2ddl.auto\", environment.getProperty(\"spring.jpa.hibernate.ddl-auto\")); return builder.dataSource(dataSource) // 实体所在的包位置 .properties(properties) .packages(\"cn.mariojd.springboot.multiple.datasource.jpa.postgres.entity\") .persistenceUnit(\"jpa-postgres\") .build(); } @Bean public PlatformTransactionManager postgresTransactionManager(@Qualifier(\"postgresEntityManagerFactory\") EntityManagerFactory entityManagerFactory) { return new JpaTransactionManager(entityManagerFactory); }} 相关定义mysql 对应的数据源配置中,定义了实体 Student 和对应的数据层接口 StudentRepository: @Data@Entity@NoArgsConstructor@AllArgsConstructorpublic class Student { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String name; public Student(String name) { this.name = name; }} public interface StudentRepository extends JpaRepository<Student, Integer> {} postgres 对应的数据源配置中,定义了实体 Teacher 和对应的数据层接口 TeacherRepository: @Data@Entity@NoArgsConstructor@AllArgsConstructorpublic class Teacher { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String name; public Teacher(String name) { this.name = name; }} public interface TeacherRepository extends JpaRepository<Teacher, Integer> {} 单元测试@RunWith(SpringRunner.class)@SpringBootTestpublic class SpringBootJpaMultipleDataSourceTest { @Resource private StudentRepository studentRepository; @Resource private TeacherRepository teacherRepository; @Test public void test() { studentRepository.save(new Student(\"张三\")); studentRepository.save(new Student(\"李四\")); studentRepository.save(new Student(\"王五\")); Assert.assertEquals(3, studentRepository.findAll().size()); teacherRepository.save(new Teacher(\"张老师\")); teacherRepository.save(new Teacher(\"李老师\")); teacherRepository.save(new Teacher(\"王老师\")); Assert.assertEquals(3, teacherRepository.findAll().size()); }} 参考链接 Using multiple datasources with Spring Boot and Spring DataSpring JPA – Multiple DatabasesSpring Boot多数据源配置与使用How to connect to Multiple databases with Spring Data JPASpringboot2.0中Hibernate默认创建的mysql表为myisam引擎问题关于springboot2.0.0配置多数据源出现jdbcUrl is required with driverClassName的错误解决mysql java.sql.SQLException: The server time zone value … 示例源码欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"spring boot","slug":"spring-boot","permalink":"https://blog.mariojd.cn/tags/spring-boot/"},{"name":"multiple datasource","slug":"multiple-datasource","permalink":"https://blog.mariojd.cn/tags/multiple-datasource/"},{"name":"spring data jpa","slug":"spring-data-jpa","permalink":"https://blog.mariojd.cn/tags/spring-data-jpa/"}]},{"title":"Ajax 跨域问题及其解决方案","slug":"Ajax 跨域问题及其解决方案","date":"2018-11-17","updated":"2019-06-02","comments":true,"path":"ajax-cross-domain-problem-and-solution.html","link":"","permalink":"https://blog.mariojd.cn/ajax-cross-domain-problem-and-solution.html","excerpt":"","keywords":"","text":"什么是 ajax 跨域主流的前后端分离模式下,当前端调用后台接口时,由于是在非同一个域下的请求,从而会引发浏览器的自我安全保护机制,最终结果是接口成功请求并响应,但前端不能正常处理该返回数据。 因此,当同时满足以下三个条件的情况下,就会出现跨域问题: 浏览器限制 非同源请求(跨域) 发送的是 XHR ( XMLHttpRequest ) 请求 解决方案想要彻底解决跨域问题,只需要破坏以上三个条件的任一即可: 1. 修改浏览器(不推荐)添加浏览器启动参数:chrome --disable-web-security,但是极不推荐这种解决方式。 2. JSONP请求(不常用)Jsonp,全称 JSON with Padding,一种非官方的协议,而是一种约定;前端通过向后台发送 script 类型请求解决跨域,此时接口响应的 application/javascript 类型的数据会作为 callback 函数的参数进行处理。 所以,后台也需要做相应的处理。以 Java 为例,添加如下配置即可: @ControllerAdvicepublic class JsonpAdvice extends AbstractJsonpResponseBodyAdvice { public JsonpAdvice() { // 前后端约定的jsonp参数名,默认值是callback super(\"callback\"); } } 注意,Spring 4.1 版本之后,官方已不再推荐使用上述允许 jsonp 请求的配置,建议使用 CROS 配置来解决跨域问题,详情可查看这里 综上,jsonp 请求存在以下几个弊端: 服务端需要改动代码进行支持; 只支持发送 Get 请求,请求头中更改其它类型的请求方式是无效的; 发送的不是 XHR 请求,而是 script 类型,无法享受到相关的特性。 3. 调用方隐藏跨域用 Nginx 或 Apache 来代理调用方的请求(客户端变更为相对路径请求,而非绝对路径),此时对于浏览器来说,由于请求是同源的,因此就不存在跨域问题。 4. 被调用方允许跨域(最常用) 服务端配置 以 Java 应用为例,添加如下全局配置: @Configurationpublic class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(\"/**\") // 允许跨域的接口 .allowedOrigins(\"*\") // 允许跨域的请求源 .allowedMethods(\"*\") // 允许跨域的请求方式 .allowedHeaders(\"*\") // 允许跨域的请求头 .allowCredentials(true) // 带cookie请求的时候需要开启,且allowedOrigins需要指定为具体的请求源(最好是动态配置) .maxAge(60 * 60 * 24); // 设定options请求预检命令的缓存时长 }} 如果只想针对某个类下的接口,或者是某个具体的接口配置允许跨域,只需要在相应的地方添加注解 @CrossOrigin 即可。 Nginx 配置 如果配置了 nginx 作为代理服务器,那么只需要为 nginx 添加支持跨域请求即可: server { listen 80; server_name xxx.com; location / { proxy_pass http://localhost:8080/; # 配置允许跨域 add_header Access-Control-Allow-Origin $http_origin; add_header Access-Control-Allow-Methods *; add_header Access-Control-Allow-Headers $http_access_control_request_headers; add_header Access-Control-Max-Age 3600; add_header Access-Control-Allow-Credentials true; # 对于options预检请求,直接响应200 if ($request_method = OPTIONS) { return 200; } }} 扩展思考Q1:浏览器在执行跨域请求时,是先执行后判断,还是先判断后执行?A1:都有可能,这需要根据所发送的请求是简单请求还是非简单请求来判断;如果是非简单请求,浏览器每次在执行真正的请求之前,还会先发送一个 options 请求方式的预检命令【 可设定缓存时长,取消每次请求都要预检,提高效率,参考上面的服务端配置 】。关于两种请求的区分及定义,参考下图说明: Q2:如果是允许带(被调用方) cookie 的跨域请求,此时服务端同样配置为 Access-Control-Allow-Origin 等于 *,前端是否还可以请求成功?A2:不可以,此时要将 Access-Control-Allow-Origin 指定为调用方具体的域【 可以先取得调用方的域再动态配置,这样就不存在多个域请求的限制问题 】,并且添加配置 Access-Control-Allow-Credentials 为 true。","categories":[{"name":"前端","slug":"前端","permalink":"https://blog.mariojd.cn/categories/前端/"}],"tags":[{"name":"ajax","slug":"ajax","permalink":"https://blog.mariojd.cn/tags/ajax/"},{"name":"jsonp","slug":"jsonp","permalink":"https://blog.mariojd.cn/tags/jsonp/"}]},{"title":"小巧实用的 HTTP 代理抓包工具:mitmproxy","slug":"小巧实用的 HTTP 代理抓包工具:mitmproxy","date":"2018-10-31","updated":"2019-06-02","comments":true,"path":"get-started-with-mitmproxy.html","link":"","permalink":"https://blog.mariojd.cn/get-started-with-mitmproxy.html","excerpt":"","keywords":"","text":"常见的http代理有:Fiddler、Charles以及下来要介绍的Mitmproxy,几款抓包软件本人都使用过,可以说是各有各的特点。Mitmproxy小巧强大,最吸引我的是它支持加入Python脚本,方便开发人员直接处理监听到的数据。 安装这步比较简单,直接pip install mitmproxy,或者自行下载安装包。更多更详细的安装说明可以查看这篇文章:MitmProxy的安装 使用window下不支持使用mitmproxy,但可以使用另外两个附带的组件:mitmdump和mitmweb,二选一在控制台输入,代理开启后默认的监听端口为8080。mitmdump是纯控制台输出的监听形式,mitmweb则对应有相关的web监听界面。 更改监听端口?添加-p参数,如mitmdump -p 10000;想指定py脚本?带上-s参数即可,如mitmweb -s out.py;完整的参数信息及使用应该查看官网说明,下来的示例中会介绍Python脚本的使用。 示例mitmproxy中定义了一系列完整的监听流程事件,通常这并不需要我们关注太多,常见的Events莫过于request和response,完整的mitmproxy事件请点击这里查看,下面的简单示例或许对你有帮助: import mitmproxy.httpdef request(flow: mitmproxy.http.HTTPFlow): \"\"\" The full HTTP request has been read. \"\"\" flow.request.headers[\"User-Agent\"] = \"Chrome/66.0.3497.100\" pretty_url = flow.request.pretty_url print(pretty_url)def response(flow: mitmproxy.http.HTTPFlow): \"\"\" The full HTTP response has been read. \"\"\" content = flow.response.content print(content) 小结在功能上多数抓包软件大同小异,重要的还是得根据场景来选择最合适的;我当初接触到mitmProxy,是因为在爬取某APP数据的时候,尝试了一段时间都没有还原出客户端接口加密的请求流程,后来结合使用了Appium和mitmProxy,算是部分实现了该需求。综上,全文只是简单浅显的介绍了mitmproxy,而往往看官方文档才是最完整和高效的。 示例源码欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"开源和中间件","slug":"开源和中间件","permalink":"https://blog.mariojd.cn/categories/开源和中间件/"}],"tags":[{"name":"http proxy","slug":"http-proxy","permalink":"https://blog.mariojd.cn/tags/http-proxy/"},{"name":"mitmproxy","slug":"mitmproxy","permalink":"https://blog.mariojd.cn/tags/mitmproxy/"}]},{"title":"APP 自动化:Appium 极简上手","slug":"APP 自动化:Appium 极简上手","date":"2018-10-30","updated":"2019-06-02","comments":true,"path":"get-started-with-appium.html","link":"","permalink":"https://blog.mariojd.cn/get-started-with-appium.html","excerpt":"","keywords":"","text":"像Selenium可以操控Web浏览器,手机APP平台也有类似的自动化测试工具:Appium;全文分基础介绍、环境搭建和案例演示三部分介绍Appium,以帮助Learner快速的上手。 基础介绍Appium是一个开源的自动化测试框架,用于原生,混合和移动Web应用程序。 它使用WebDriver协议驱动iOS,Android和Windows应用程序。关于它的运作流程,用图来介绍会更加生动形象一些: 在上图中,左边这部分是Appium-Client,通俗点来说,是用于间接驱动最右边的设备执行预定的自动化测试流程,支持使用多种主流的编程语言进行编写,这也是测试开发人员需要关注的核心部分;中间的Appium-Server是衔接左边客户端以及右边APP设备端的重要桥梁,一般仅需要配置好环境及启动运行;右边这块,当然就是实际执行自动化测试的终端,如IOS真机、Android真机,或者是模拟器。 环境搭建 NodeJS Appium是使用nodejs实现的,因此Node是解释器,首先要确认安装好 Appium-Server nodejs appium-desktop 上述的两种方式都可以搭建Appium-Server环境,后面演示会基于Appium-Desktop。(PS:下载太慢了?分享个百度网盘) Andrioid SDK android sdk android studio 上述方式可以直接和间接搭建安装Android环境,因为后面要用到adb这个工具,所以需要配置好ANDROID_HOME这个环境变量。(PS:下载太慢了?分享个百度网盘) Appium-Python-Client 后面会用到Python来编写Appium客户端:pip install Appium-Python-Client okay,准备好以上几个环境后,启动Appium测试一下: 案例演示下面演示在安卓真机上的自动登录Keep(APP)。 获取设备名称。操作流程:开启手机的开发和调试模式,连接电脑授权认证,Window + R输入并运行cmd,用adb devices -l查看: 启动Appium Server进行调试: 从上图可以看到,启动App Session需要有以下几个参数(点击了解更详细的Appium Desired Capabilities): platformName,如Android、iOS等 deviceName,参考前面是如何获取的 appPackage和appActivity,获取参考这里 综上所述,这里对应Keep的信息如下: { "platformName": "Android", "deviceName": "WAS_AL00", "appPackage": "com.gotokeep.keep", "appActivity": "com.gotokeep.keep.splash.SplashActivity"} 点击Start Session,之后可以看到手机端启动了Keep,并且在Appium Server端中同步展示: 上述的操作通常只是用来方便获取控件id及定位的,下面基于Python编写完整的Appium-Client以实现自动登录操作: from appium import webdriverfrom selenium.webdriver.common.by import Byfrom selenium.webdriver.support.ui import WebDriverWaitfrom selenium.webdriver.support import expected_conditions as ECserver = 'http://localhost:4723/wd/hub' # Appium Server, 端口默认为4723desired_capabilities = { 'platformName': 'Android', 'deviceName': 'WAS_AL00', # 需替换成你的deviceName 'appPackage': 'com.gotokeep.keep', 'appActivity': 'com.gotokeep.keep.splash.SplashActivity'}driver = webdriver.Remote(server, desired_capabilities)wait = WebDriverWait(driver, 10) # 最大查找等待超时时间:10sdef get_permission(): \"\"\"允许APP获取的某些权限\"\"\" try: ask = wait.until(EC.presence_of_element_located((By.ID, 'com.android.packageinstaller:id/do_not_ask_checkbox'))) ask.click() allow = wait.until( EC.presence_of_element_located((By.ID, 'com.android.packageinstaller:id/permission_allow_button'))) allow.click() except: pass# 允许两项授权get_permission()get_permission()# 点击“立即使用”welcome = wait.until(EC.presence_of_element_located((By.ID, 'com.gotokeep.keep:id/btn_bottom_in_video_welcome')))welcome.click()# 切换“密码登录”(同样可以使用第三方进行授权登录)driver.tap([(900, 110)])# 输入“手机号”phone = driver.find_element_by_accessibility_id('Phone Number In Login')phone.send_keys('13988888888') # 替换成实际的账号# 输入“密码”password = driver.find_element_by_accessibility_id('Password In Login')password.send_keys('123456') # 替换成实际的密码# 点击“登录”login = driver.find_element_by_id('com.gotokeep.keep:id/btn_action')login.click() 最后,附上运行效果图: 示例源码欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"开源和中间件","slug":"开源和中间件","permalink":"https://blog.mariojd.cn/categories/开源和中间件/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.mariojd.cn/tags/python/"},{"name":"appium","slug":"appium","permalink":"https://blog.mariojd.cn/tags/appium/"}]},{"title":"IntelliJ IDEA 2018.3 Beta 重大更新:支持 CPU 火焰图,新增酷炫主题","slug":"IntelliJ IDEA 2018.3 Beta 重大更新:支持 CPU 火焰图,新增酷炫主题","date":"2018-10-29","updated":"2019-06-02","comments":true,"path":"idea-2018.3-beat.html","link":"","permalink":"https://blog.mariojd.cn/idea-2018.3-beat.html","excerpt":"","keywords":"","text":"本文转载自公众号:闪电侠的博客JetBrain 是一家伟大的公司,一直致力于为开发者开发世界上最好用的集成开发环境 就在上周,JetBrain 公司发布了 Java 集成开发环境 IntelliJ IDEA 最新版本 2018.3 Beta,本篇文章,我将根据官方博客以及自己的理解来为大家解读一下这次更新有哪些重磅的功能。 1. 重构类、文件、符号,Action 搜索IntelliJ IDEA(以下简称 IDEA) 中的搜索可以分为以下几类 类搜索,比如 Java,Groovy,Scala 等类文件 文件搜索,类文件之外的所有文件 符号搜索,包括接口名,类名,函数名,成员变量等 Action 搜索,找到你的操作 字符串搜索及替换 在 IDEA 的世界里,搜索无处不在,你几乎可以瞬间找到你想要找到的任何一行代码甚至任何一个字。新版中,IDEA 更是将类、文件、符号、Action 搜索与双 Shift 键调出来的 SearchEverywhere 无缝地结合在一起。 在老的版本中,类、文件、符号、Action 搜索是独立的快捷键,在新版中,任意一种类型的搜索行为被触发,将弹出来以下窗口 从以上演示可以看到,我们调出搜索类的窗口,该窗口将首先会展示基于类名搜索的结果,如果你想复用当前输入的字符基于其他的语义(比如文件或者符号)进行搜索,只需要按 Tab 键,结果瞬间就出来了。 2. 重新设计的结构搜索/替换对话框其实,IDEA 里面除了以上五种类型的搜索,还有一种非常强大的搜索叫做 结构化搜索,你可以基于一定的代码结构搜到你所需要的结果。 举个栗子:如果我们想搜索所有的 try catch 语句块,在调出结构化搜索框之后,可以输入以下文本 try { $TryStatement$;} catch ($ExceptionType$ $Exception$) { $CatchStatement$;} 然后,IDEA 就会把所有的 try catch 语句块搜索出来,而新版更是强化了这个功能,下面我用两张动图演示一下这次更新的两个功能 结构化搜索由于输入的文本比较长,所以一般我们会自己预置一些模板,然后给模板命名,然后结构化搜索的时候呢,我们就可以直接基于这个模板名来搜索,新版更新的第一个功能就是,在文本输入框里,按下智能补全键,可以迅速调出模板,按照最近的搜索历史排序,然后再按下回车,文本就自动给你填充上了,你还可以点击左上角的搜索 icon,也会展示你最近的搜索记录,这些记录是以文本的方式展示的 上面的文本就是系统内置的结构化模板 try's,点击完 Find 按钮之后,所有的 try catch 都会展示出来,我们还可以进一步过滤,比如,我们想要找出 catch 到的 exception 的名字为 flash,给对应的模板变量加上一个 Text 类型的 filter 即可迅速定位 3. 运行一切你可以双击 ctrl 键,调出 RunAnything 窗口,你可以输入点什么来运行任意可以运行的东西,比如起 tomcat 容器,单元测试,甚至可以运行终端指令,gradle、maven 构建命令 另外,你还可以按住 shift 键,那么所有支持 debug 的运行将秒变 debug 模式 4. 重构插件中心IDEA 中很多强大的功能都是通过插件来实现的,随便举个栗子,装个语言插件,IDEA 摇身一变为 nodejs IDE、php IDE、python IDE、scala IDE、go IDE,我自己就安装了 30+ 非常好用的插件。 而在新版的 IDEA 中,JetBrain 更是对插件中心进行全面改版,如下图 调出插件配置之后,页面分为三大部分 Marketplace: 插件市场,你可以搜索到你想要的插件 Installed: 当前安装的所有的插件,你还可以点击左上角搜索小 icon,按类别查看当前已安装的插件,其中的 custom 选项便是自己下载安装的插件 Updates:当前安装过的插件如果有更新,都会在这里显示出来 最后一个是配置项,你可以自定义你的插件仓库,你可以给配置插件下载的 http 代理(尤其是国外网络访问差的时候),你还可以从本地硬盘中安装插件 5. 不断改进的版本控制系统我个人对于版本控制,是不太喜欢用图形界面的,但是 IDEA 对于版本控制的设计真是太好用了,只能沦陷了,嘿嘿~ 5.1 GitHub Pull Requests新版中,加入了对 GitHub Pull Requests 的支持,现在你可以直接在电脑上创建或者查看某个项目的 Pull Request 了 你还可以基于某个 Pull Requests 直接创建一个分支,或者直接在 Github 上查看当前的 Pull Request,这个功能对于开源工作者来说是一件非常幸福的事。 5.2 Git 子模块支持此外,新本 IDEA 对于 Git 子模块的支持也更加友好了。如果你的 Git 项目中包含 Git 子模块,在 clone 代码的时候,也会一并 clone 到本地,另外,项目中任何文件有变更,提交 commit,IDEA 也会智能匹配到外层模块或者子模块,一并提交 commit,进而同时 push 到多个仓库。 5.3 Improved Annotate support我们有时候会不经意地格式化自己或者别人写过的代码,这就导致了每次提交代码的时候,即使只更新了一两处代码,最后 diff 出来也会显得很乱,然而其中大部分乱的地方是因为空格导致的。 在新版 IDEA 中,我们在对比文件的时候,可以选择忽略空格 注意:这个选项默认是打开的 另外,在合并代码的时候,你也可以选择忽略空格 这样在解决冲突的时候,你也不会看到空格相关的改动,省下的很多宝贵的注意力。 6. 全新主题IDEA 终于在这一版新增了一款默认主题,该主题为一款高对比度主题,应该会有很多人会喜欢吧 预计在不久的将来,IDEA 会在主题这方面下功夫,毕竟笔者觉得 VS Code 的主题还是蛮好看的,IDEA 可以吸收过来。 7. 编辑器改进7.1 多行 TODO 注释在 IDEA 中,只要你在注释中添加了 todo 关键词,在边条栏中的 todo 选项卡中就可以看到当前所有待未完成的功能,如下图 老版本中,是不支持 多行 todo 注释的显示的,而在新版本中,如果 todo 注释有多行,你只需要在下面几行前面再添加一个空格即可 7.2 缩进状态栏IDEA 现在可以在状态栏中显示当前文件的缩进是几个空格,你可以点击这个状态栏,控制当前文件的缩进风格。 比如,你的项目缩进风格是4个空格,然后某个新人写了个 tab 风格的源文件提交了,你可以直接点击弹出菜单的 ConfigureIndentsForJava...,然后做一些修改即可 7.3 TAB 快速切换源文件 你现在可以使用 Tab+数字,迅速切换到你想要的文件,这比鼠标点击要快一些 7.4 多行字符串搜索在新版 IDEA 中,不仅仅能够搜索字符串,而且能够搜索整个段落 8. JVM 调试器8.1 attach 到任意 Java 进程IDEA 的 debug 功能无论是对于调试找错还是阅读源码,都发挥了非常重要的作用,新版 IDEA 对 debug 功能进一步加强,现在不仅仅能 debug 当前的应用,而且能够 attach 到任意的 Java 进程,attach 之后,你就可以看到该进程的线程状态,并且使用强大的 Memory View 功能可以看到当前内存的状态。 8.2 远程调试支持异步栈追踪IDEA 支持远程 debug 几乎和本地 debug 一样,只需要远程端口开启即可。 IDEA 也支持异步线程的调试,断点打在某一行,你不仅可以看到这行对应线程的调用栈,还能看到启动对应线程的外部线程的调用栈。 新版中,对远程调试也加入了异步栈的支持,采用以下两个步骤即可 拷贝 /lib/rt/debugger-agent.jar 到远程机器 添加启动参数 -javaagent:debugger-agent.jar 到远程机器 9. 运行配置9.1 配置宏我们在运行应用程序的时候,有的时候需要设定不同的启动参数来查看不同的效果,在以前,这些参数都需要你手动敲进去,并且经常会忘记当前启动参数的测试目的,非常麻烦。 现在,你可以提前将参数通过宏的方式输入,调试的时候,通过调整宏,你不用反复修改启动参数文本,通过宏文本还可以一目了然看到当前的启动参数的测试目的是什么。 9.2 使用文本作为控制台输入有的时候需要在控制台输入一些文本,然后再运行程序,这个对于调试来说非常不便,新版 IDEA 支持指定一个文本文件作为控制台输入,这样,你就可以预先定义好控制台输入,重复利用,提高效率 10. JVM Profiler最后一个重磅功能,应该可以说是本次更新最大的亮点,IDEA 现在可以分析 Java 程序的性能分析了,包括如下几个方面 火焰图分析 CPU 性能消耗,你可以分析 Java 进程的所有线程的 CPU 消耗火焰图,也可以只选择一个线程来分析 方法调用图,可以找到在某个线程中,消耗 cpu 最多的方法 方法列表,可以看到每个方法的调用次数,点进去还可以看到详细的调用栈 下面用一章动图来展示一下,具体的细节读者可自行探索 有了这个神器之后,你不需要额外的 profiler 工具,就可以直接在 IDEA 里面完成应用程序的性能分析。预计不久的将来,Eclipse MAT 相关的功能可能也会移植到 IDEA 中,届时,Java 应用程序性能分析,堆分析,gc 分析将统统可以在 IDEA 里面运行,真正的 All In One 时代即将到来! 11. More……除此之外,本次更新还有大量的小功能的更新,在你使用新版 IDEA 的时候就会体验到,这里就不一一赘述了,赶紧下载体验吧,下载地址:https://www.jetbrains.com/idea/nextversion/ 这篇文章更多的是解析本次更新,其实上个版本的更新也有很多重磅的功能,如果你想了解这些,可以参考一下这篇文章 IntelliJ IDEA 2018.1正式发布,希望能够帮助你 原文地址:IntelliJ IDEA 重大更新:支持CPU火焰图,新增酷炫主题欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/categories/IDEA/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"}]},{"title":"Linux 私房菜:走进 bash","slug":"Linux 私房菜:走进bash","date":"2018-10-18","updated":"2019-06-02","comments":true,"path":"linux-bash.html","link":"","permalink":"https://blog.mariojd.cn/linux-bash.html","excerpt":"","keywords":"","text":"本文内容精简、整理、摘抄、有感于《鸟哥的Linux私房菜 - 基础篇第四版》第十章 • 认识与学习BASH。 Bash功能 命令编辑修复能力:history。用户bash指令使用记录文件:~/.bash_history 命令与文件补全:tab 命令别名设定:alias 工作控制、前景和背景控制 程序化脚本:shell scripts 通配符:wildcard 指令操作 快捷键 说明 backspace 向前删除指令 ctrl + u 向前删除全部指令 del 向后删除指令 ctrl + k 向后删除全部指令 ctrl + a 或 home 移动到指令串最前面 ctrl + e 或 end 移动到指令串最末尾 变量使用 在终端输入,如设定a=b(不能以数字开头,等号两边不能有空格),那么可以使用echo指令取出值b:echo $a,这种设定仅对本次登录有效; 常见的环境变量有$HOME、$PATH等,可用env、set或export命令查找已定义的变量; 打印本shell的PID:echo $$ 打印上一个指令的回传值:echo $?(正常回传为0) 取消变量设定:unset a; 双引号字符串中,仍可引用变量,而单引号字符串仅代表是字符串本身。 相关命令 命令 说明 alias 设定别名 unalias 取消设定别名 history 查看历史命令。!number,执行第n个指令;!command,往前模糊搜索匹配的指令并执行;!!,执行上一个指令,相当于↑ + Enter ctrl + u 提示符命令输入下可快速删除整行 ctrl + s 暂停屏幕的输出 ctrl + q 恢复屏幕的输出 特殊符号 bash特殊符号 说明 # 批注符号:注释 \\ 跳脱符号:将特殊字符或通配符还原成一般字符 | 管线:连接两个管线命令 ; 分隔多个连续性命令 ~ 当前用户的家目录 $ 取用变量前导符 & 工作控制:将指令设为后台工作 / 目录间的分隔符 ! 逻辑运算符:非,not >, >> 数据流输出重导向,前者是覆盖追加,后者是累记追加 <, << 数据流输入重导向 ‘xxx’ 单引号确保了当前引用的肯定为字符串 “xxx” 双引号仍具有变量置换功能(如$可保留相关变量引用) `xxx` 可执行的指令,亦可使用$() () 中间为子shell的起始和结束 {} 中间为命令区块的结合 cmd1 && cmd2 cmd2仅在cmd1正确执行的情况下执行 cmd1 || cmd2 cmd2仅在cmd1执行错误的情况下执行 数据流重导向 标准输入(stdin,standard input): 代码为0,使用 < 或 << 标准输出(stdout,standard output):指令正确执行所回传的讯息。代码为1,使用 > 或 >> 标准错误输出(stderr,standard error output):指令执行失败后,所回传的讯息。代码为2,使用 2> 或 2>> 垃圾桶黑洞:/dev/null 提取命令 cut [-dfc],将输入讯息的某一段切出来 (不适合处理多空格或无规则的数据) d后接指定分隔符,与-f一起使用 f分段后,这个代表第几段(以1开始,同时取出多段以,分隔),echo $PATH | cut -d ":" -f 4,5,6 c以字符为单位取出行的固定字符区间,后接区间范围,如10-表示取出第10个字符到最后,export | cut -c 12- grep [-acinvAB] [--color=auto] '搜寻字符串' filename a将binary文件转成text文件后再搜索 c计算出现次数 i忽略大小写 n输出行号 v反向选择未搜索匹配到的 An列出该行之后的n行 Bn列出该行之前的n行 排序命令 sort [-fbMnrtuk] [file or stdin] f忽略大小写 b忽略前面的空格 M按月份排序 n按数字排序 r反向排序 u去重,相同数据仅出现一行 t指定分隔符 k按指定区间排序,cat /etc/passwd | sort -t ':' -k 3 wc [-lwm],数据统计 l统计行 w统计英文单字 m统计字符 uniq [-c],去重,c代表统计次数 双向重导向 tee [-a] file,用于同时将数据流分送到文件和屏幕,而类似>和>>只能输送到文件,-a参数代表追加 字符转换 tr [-ds],用于将删除或替换某一讯息中的文字 d后接某一字符用于删除 s后接被替换的字符和待取代的字符 col [-xb],将tab键转换成对等的空格键 join -[til2] file1 file2,处理两个文字的数据 paste [-d] file1 file2,以tab键分隔将两文件的对应每一行合并成一行,-d参数可以指定分隔符 expand [-t] file,将空格键转换成对等的tab键,-t参数可以指定一个tab键代表多少个字符 分区命令 split -[bl] file [PREFIX],-b参数后接分区文件大小,可指定b,k,m,g等;-l参数为以行数进行分区;PREFIX指定分区文件名前缀 参数替换 xargs -[0epn] command,产生某个指令的参数 减号用途减号-可用于连接一些特殊的stdin和stdout,像这个文件压缩和解压缩的示例:tar -cvf - /home | tar -xvf - -C /tmp/home。前面的-用于将stdout传到后面,而后面那个-则用于接收stdout,这样就可以免去要生成file的流程 重点回顾 欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Linux","slug":"Linux","permalink":"https://blog.mariojd.cn/categories/Linux/"}],"tags":[{"name":"bash","slug":"bash","permalink":"https://blog.mariojd.cn/tags/bash/"}]},{"title":"Linux 私房菜:vi 与 vim 编辑器","slug":"Linux 私房菜:vi 与 vim 编辑器","date":"2018-10-17","updated":"2019-06-02","comments":true,"path":"linux-vi-and-vim.html","link":"","permalink":"https://blog.mariojd.cn/linux-vi-and-vim.html","excerpt":"","keywords":"","text":"本文内容精简、整理、摘抄、有感于《鸟哥的Linux私房菜 - 基础篇第四版》第九章 • vim程序编辑器。 vi/vim模式 一般指令模式 ( command mode ) vi/vim File,打开文件后即进入当前模式 编辑模式 ( edit mode ) 一般指令模式下,按i,I,a,A,o,O,r,R任意键进入 指令列命令模式 ( command-line mode ) 一般指令模式下,按:,/,?任意键进入 快捷键一般指令模式下,对应的常用操作键: 光标移动 说明 k 或 ↑ 向上移动 j 或 ↓ 向下移动 h 或 ← 向左移动 l 或 → 向右移动 ctrl + f 或 page up 向下翻页 ctrk + b 或 page down 向上翻页 ctrk + d 向下翻半页 ctrk + u 向上翻半页 n + 空格键 右移n个字符 0 或 home 移动到当前列最前面 $ 或 end 移动到当前列最后面 H 屏幕第一行最前面 M 屏幕中间行最前面 L 屏幕最后一行最前面 G 跳到文件最后一列 n + G 跳到文件第n列 gg 跳到文件第一列,相当于1G n + enter键 往下移动n列 搜索、替换 说明 /word 向下搜索 ?word 向上搜索 n 搜索匹配的下一处 N 搜索匹配的上一处 ?word 向上搜索 :n1,n2s/word1/word2/g 将n1到n2行的所有word1替换成word2 :1,$s/word1/word2/g[c] 全局搜索,将所有word1替换成word2,最后那个可选的c用于在全局替换前进行确认 删除、复制及替换 说明 x 或 del 向后删除一个字符 X 或 backspace 向前删除一个字符 n + x 向后删除n个字符 dd 整行删除 ndd 向下n行删除 dnG 向上删除到第n行 dG 向下删除全部 d$ 删除光标处到最后面 d0 删除光标处到最前面 yy 复制整行 nyy 向下复制n行 ynG 向上复制到第n行 yG 向下复制全部 y$ 复制光标处到最后面 y0 复制光标处到最前面 p 复制内容从下一行开始粘贴 P 复制内容从上一行开始粘贴 J 当前行与下一行合并成一行 u 还原前一个操作 ctrl + r 重做前一个操作 . 重复做上一个操作 从一般指令模式进入编辑模式,对应的常用操作键: 操作 说明 i 光标所在处前开始插入 I 所在列最前面开始插入 a 光标所在处后开始插入 A 所在列最后面开始插入 o 光标所在处上一行开始插入 O 光标所在处下一行开始插入 r 替换模式,只取代光标处字符一次 R 替换模式,一直取代光标处字符直到按ESC退出 ESC 退出编辑模式,回到一般指令模式 从一般指令模式进入指令列模式下对应的常用操作键: 操作 说明 :w 保存,但不退出 :w! 强制保存(跟用户的操作权限有关) :q 退出vi/vim(没有操作的情况下可成功退出) :q! 强制退出vi/vim,也不保存修改 :wq 保存修改并退出vi/vim ZZ 保存修改后退出vi/vim :w [file] 另存为file :n1,n2 w [file] 将n1到n2列另存为file :r [file] 将file的内容追加到光标下一列 :! [command] 暂离vi/vim编辑执行后面的command,按Enter后返回 :set nu 或 :set number 显示行号 :set nonu 或 :set nonumber 取消显示行号 区块选择 操作 说明 v 字符选择,将光标移动过的位置全部选择 V 列选择,将光标移动过的位置全部选择 ctrl + v 区块选择,将光标移动过的位置全部选择 y 复制上述所选 d 删除上述所选 p 粘贴上述所选 多文件编辑vim还支持同时打开多个文件,用于跨文件操作:vim file1 file2 file* 操作 说明 n 编辑下一个文件 N 编辑上一个文件 files 列出当前vim编辑的所有文件 多窗口功能 操作 说明 sp [filename] 开启多窗口,如果不输入默认为当前文件 ctrl + w + j 或 ctrl + w + ↓ 将光标移动到下一个窗口 ctrl + w + k 或 ctrl + w + ↑ 将光标移动到上一个窗口 ctrl + w + q 或 :q 退出当前窗口 其它功能 代码提示补全 环境设定。set all命令用于查看所有的设定值,vim的设定文件在一般放置在/etc/vim/vimrc,自定义下建议新建保存在~/.vimrc中 vim常用指令示意图 重点回顾 欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Linux","slug":"Linux","permalink":"https://blog.mariojd.cn/categories/Linux/"}],"tags":[{"name":"vi","slug":"vi","permalink":"https://blog.mariojd.cn/tags/vi/"},{"name":"vim","slug":"vim","permalink":"https://blog.mariojd.cn/tags/vim/"}]},{"title":"Linux 私房菜:打包、压缩和备份","slug":"Linux 私房菜:打包、压缩和备份","date":"2018-10-16","updated":"2019-06-02","comments":true,"path":"linux-packaging-compression-and-backup.html","link":"","permalink":"https://blog.mariojd.cn/linux-packaging-compression-and-backup.html","excerpt":"","keywords":"","text":"本文内容精简、整理、摘抄、有感于《鸟哥的Linux私房菜》第八章 • 文件与文件系统的压缩、打包和备份。 压缩目前常见的Linux压缩命令包括有gzip、bzip2和最新的xz,下表是一些常见的压缩文件扩展名及对应指令关系: 扩展名 指令 说明 *.Z compress 已经不流行,gzip可以代替 *.zip zip window下常见的压缩格式 *.gz gzip 较快较为常见的一种压缩格式 *.bz2 bzip2 压缩率比gzip要好的压缩格式 *.xz xz 压缩率比gzip/bzip2都要高的压缩方式 如果是经过打包(tar)的压缩文件,那常见的对应关系大多如下: 扩展名 说明 *.tar tar指令打包,未压缩过 *.tar.gz tar指令打包,用gzip压缩过 *.tar.bz2 tar指令打包,用bzip2压缩过 *.tar.xz tar指令打包,用xz压缩过 以下是上述几种压缩命令的常见用法: gzip [-cdv#] 档名,压缩或解压缩,支持操作旧式的compress指令处理的*.Z压缩文档 c将输出流重定向,可自定义压缩文档名称 d解压缩 v显示压缩比 #取值范围介于1-9,1最快压缩率最低,9最慢但压缩比最高,默认是6 zcat/zmore/zless/zgrep 压缩档,这几个命令都是用于查看或过滤出gzip格式的压缩档内容 bzip2 [-cdkv#] 档名,提供了比gzip更好的压缩比 c将输出流重定向,可自定义压缩文档名称 d解压缩 k保留源文件而不是删除 v显示压缩比 #同gzip,取值范围介于1-9,1最快压缩率最低,9最慢但压缩比最高,默认是6 bzcat/bzmore/bzless/bzgrep 压缩档,用于查看或过滤出bz2格式的压缩档内容 xz [-cdlk#] 档名,提供了比gzip/bzip2更好的压缩比 c将输出流重定向,可自定义压缩文档名称 d解压缩 1显示压缩文件相关信息 k保留源文件而不是删除 #同gzip,取值范围介于1-9,1最快压缩率最低,9最慢但压缩比最高,默认是6 xzcat/xzmore/xzless/xzgrep 压缩档,用于查看或过滤出xz格式的压缩档内容 打包以上压缩指令仅支持操作单一文件,多个文件则需要配合使用tar进行打包: tar [-{z|j|J}{c|t|x}vpP] -f 压缩档 待压缩或待解压缩文件 [-C 解压缩目录] zjJ对应gzip、bzip2和xz三种压缩格式。( PS: 不加该参数时,档名最好取为:*.tar;如果是-z,对应为*.tar.gz;如果是-j,对应为*.tar.bz2;如果是-J,对应为*.tar.xz ) ctx分别对应打包压缩、察看压缩档内容和解压缩这三种功能,不能同时出现 v压缩或解压缩过程中显示正在处理的文件名 f后接已有tar档名或待建立档名 C解压缩时指定的目录位置 p保留备份数据的原权限和属性 P保留根目录绝对路径,解压会还原到该位置 --exclude=FILE压缩但不包含FILE 备份 XFS文件系统 备份:xfsdump 还原:xfrestroe 光盘写入工具 建立印象档:mkisofs 光盘刻录工具:cdrecord 其它压缩备份工具 dd cpio 重点回顾 欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Linux","slug":"Linux","permalink":"https://blog.mariojd.cn/categories/Linux/"}],"tags":[{"name":"linux packeage","slug":"linux-packeage","permalink":"https://blog.mariojd.cn/tags/linux-packeage/"},{"name":"linux compress","slug":"linux-compress","permalink":"https://blog.mariojd.cn/tags/linux-compress/"},{"name":"linux backup","slug":"linux-backup","permalink":"https://blog.mariojd.cn/tags/linux-backup/"}]},{"title":"12张思维导图告诉你 - Python 数据科学知识体系【 Numpy、Pandas、Matplotlib 】","slug":"12张思维导图告诉你 - Python 数据科学知识体系【 Numpy、Pandas、Matplotlib 】","date":"2018-10-15","updated":"2019-06-02","comments":true,"path":"python_data_science_knowledge_system.html","link":"","permalink":"https://blog.mariojd.cn/python_data_science_knowledge_system.html","excerpt":"","keywords":"","text":"以简单、直观、清晰的思维导图方式,帮助大家学习和回顾Python数据科学知识体系,原创作者是个萌妹纸。 原文链接压缩包下载欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.mariojd.cn/categories/Python/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.mariojd.cn/tags/python/"},{"name":"mind mapping","slug":"mind-mapping","permalink":"https://blog.mariojd.cn/tags/mind-mapping/"},{"name":"python data science","slug":"python-data-science","permalink":"https://blog.mariojd.cn/tags/python-data-science/"}]},{"title":"7张思维导图告诉你 - Python 标准库知识体系【 正则、日期、数据库、进程线程... 】","slug":"7张思维导图告诉你 - Python 标准库知识体系【 正则、日期、数据库、进程线程... 】","date":"2018-10-14","updated":"2019-06-02","comments":true,"path":"python_standard_library_knowledge_system.html","link":"","permalink":"https://blog.mariojd.cn/python_standard_library_knowledge_system.html","excerpt":"","keywords":"","text":"以简单、直观、清晰的思维导图方式,帮助大家学习和回顾Python标准库知识体系,原创作者是个萌妹子。 原文链接压缩包下载欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.mariojd.cn/categories/Python/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.mariojd.cn/tags/python/"},{"name":"mind mapping","slug":"mind-mapping","permalink":"https://blog.mariojd.cn/tags/mind-mapping/"},{"name":"python standard library","slug":"python-standard-library","permalink":"https://blog.mariojd.cn/tags/python-standard-library/"}]},{"title":"4张思维导图告诉你 - Python 爬虫知识体系","slug":"4张思维导图告诉你 - Python 爬虫知识体系","date":"2018-10-13","updated":"2019-06-02","comments":true,"path":"python_spider_knowledge_system.html","link":"","permalink":"https://blog.mariojd.cn/python_spider_knowledge_system.html","excerpt":"","keywords":"","text":"以简单、直观、清晰的思维导图方式,帮助大家学习和回顾Python爬虫知识体系,原创作者是一枚软萌妹子。 原文链接压缩包下载欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.mariojd.cn/categories/Python/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.mariojd.cn/tags/python/"},{"name":"python spider","slug":"python-spider","permalink":"https://blog.mariojd.cn/tags/python-spider/"},{"name":"mind mapping","slug":"mind-mapping","permalink":"https://blog.mariojd.cn/tags/mind-mapping/"}]},{"title":"17幅思维导图告诉你 - Python 核心知识体系","slug":"17幅思维导图告诉你 - Python 核心知识体系","date":"2018-10-12","updated":"2019-06-02","comments":true,"path":"python_core_knowledge_system.html","link":"","permalink":"https://blog.mariojd.cn/python_core_knowledge_system.html","excerpt":"","keywords":"","text":"以简单、直观、清晰的思维导图方式,帮助大家学习和回顾Python核心知识体系,原创作者还是一枚软萌妹纸。 原文链接压缩包下载欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.mariojd.cn/categories/Python/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.mariojd.cn/tags/python/"},{"name":"python core","slug":"python-core","permalink":"https://blog.mariojd.cn/tags/python-core/"}]},{"title":"Linux 私房菜:磁盘文件系统管理及常见命令","slug":"Linux 私房菜:磁盘文件系统管理及常见命令","date":"2018-10-11","updated":"2019-06-02","comments":true,"path":"linux-disk-and-file-system-management-and-command.html","link":"","permalink":"https://blog.mariojd.cn/linux-disk-and-file-system-management-and-command.html","excerpt":"","keywords":"","text":"本文内容精简、整理、摘抄、有感于《鸟哥的Linux私房菜 - 基础篇第四版》第七章 • Linux磁盘与文件系统管理。 文件系统Linux的标准文件系统是EXT2,此外常见的文件系统包含如下: 传统文件系统:ext2/minix/MS-DOS/FAT/iso9660/… 日志文件系统:ext3/ext4/ReiserFS/Window’s NTFS/IBM’s JFS/SGI’s XFS/ZFS/… 网络文件系统:NFS/SMBFS/… 查看系统已加载支持的文件系统:cat /proc/filesystems 磁盘命令 df [-ahikHTm] 文件或目录 - 查看磁盘空间使用情况 h可读格式展示(GBytes、MBytes、KBytes) du [-ahskm] 文件或目录 - 评估文件空间使用情况 h可读格式展示 s仅展示占用量 lsblk [-dfimpt] [device] - 列出系统磁盘列表 blkid - 列出装置的UUID等参数 mount - 挂载 unmount [-fn] 装置文件名或挂载点 - 取消挂载 文件档连接 ln [-sf] 来源文件 目标文件,制作连接档 s软连接,不加默认为硬连接 f目标文件存在时先删除后建立 重点回顾 欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Linux","slug":"Linux","permalink":"https://blog.mariojd.cn/categories/Linux/"}],"tags":[{"name":"linux thing","slug":"linux-thing","permalink":"https://blog.mariojd.cn/tags/linux-thing/"},{"name":"disk management","slug":"disk-management","permalink":"https://blog.mariojd.cn/tags/disk-management/"},{"name":"common commands","slug":"common-commands","permalink":"https://blog.mariojd.cn/tags/common-commands/"}]},{"title":"Linux 私房菜:小操作命令","slug":"Linux 私房菜:小操作命令","date":"2018-10-10","updated":"2019-06-02","comments":true,"path":"linux-basic-command.html","link":"","permalink":"https://blog.mariojd.cn/linux-basic-command.html","excerpt":"","keywords":"","text":"一些不定期整理、收集和更新的小操作命令 用户相关 命令 描述 补充说明 su [options] [user] 切换登录 id [user] 用户属性 useradd [options] [user] 添加用户 userdel [options] [user] 删除用户 passwd [user] 修改密码 连输两次密码后完成更改 所属组相关 命令 描述 groupadd [options] [group] 添加组 groupmod [options] [group] 修改组 groupdel [options] [group] 删除组 gpasswd [options] [group] 操作组 配置文件相关 文件 说明 /etc/hosts 主机地址设置 /etc/profile 环境变量配置 /etc/passwd 用户账号信息 /etc/shadow 账号密码信息 /etc/group 用户组信息 /etc/shells shells信息 /etc/bash.bashrc bash配置 无 理解Linux配置文件 补充扩展相关 命令 说明 alias a='b' 将命令b设置为别名a who 查看当前在线用户 last 显示登录人的信息 which 查找可执行命令的实际位置 type 同上,查找执行档的实际位置 uname 查看系统信息 env 查看环境变量 set 查看所有环境变量和自定义变量 欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Linux","slug":"Linux","permalink":"https://blog.mariojd.cn/categories/Linux/"}],"tags":[{"name":"linux thing","slug":"linux-thing","permalink":"https://blog.mariojd.cn/tags/linux-thing/"},{"name":"base command","slug":"base-command","permalink":"https://blog.mariojd.cn/tags/base-command/"}]},{"title":"Linux 私房菜:文件目录管理及常见命令","slug":"Linux 私房菜:文件目录管理及常见命令","date":"2018-10-08","updated":"2019-06-02","comments":true,"path":"linux-file-and-directory-management-and-command.html","link":"","permalink":"https://blog.mariojd.cn/linux-file-and-directory-management-and-command.html","excerpt":"","keywords":"","text":"本文内容精简、整理、摘抄、有感于《鸟哥的Linux私房菜 - 基础篇第四版》第六章 • Linux的文件权限与目录管理。 路径(PATH) 相对路径:cd /var/tmp/,由根目录/开头 绝对路径:不以/开头,与相对路径写法相对应 环境变量(ENVIRONMENT VARIABLE)。由一堆目录组成,不同目录之间用:分隔。命令相同的情况下,靠前指定的环境变量配置优先级高。 查看命令: echo $PATH 配置文件:/etc/profile 立即刷新:source /etc/profile 常用命令 文件类型 file 文件,多用于判断是ascii,data或binary文件 目录相关 cd - change directory,更换目录。可以是相对路径或者是绝对路径,常见的有: .,表示当前目录 ..,进入上一层目录 -,返回前一个工作目录 ~[user],进入当前用户(或指定user用户)的家目录,不指定~也可以返回家目录 pwd - print work directory,显示当前工作目录 pwd [-P],显示实际路径而非链接路径 mkdir - make directory,创建目录 mkdir [-mp] 目录名称。-m指定目录所属权限;-p同时创建多级目录 rmdir - remove directory,删除空目录(被删目录不能含其它子目录及文件) rmdir [-p] 目录名称。-p参数可同时删除上级空目录 文件目录管理 ls [-aAdfFhilnrRSt] [--color={never,auto,always}] [--time={mtime,atime,ctime}] [--full-time] 文件或目录 - list,列出列表。参数较多,请用man ls查看更多详细用法,大部分情况下用ll能满足日常使用(等价于ls -al) -a含全部隐藏文件 -A含隐藏文件但不含.和..这两个目录 -h以可读形式(KB,GB)展示文件大小,默认为Byte -l以每行长串输出展示数据 -r自然排序输出结果的反向 -R连同子目录文件一同输出 -S以文件大小排序 -t以时间排序 touch [-acdmt] File - 创建文件 cp [-adfilprsu] source*(一个或多个来源文件或目录) destination(目标文件或目录) - copy,复制 -a完全相同复制 -i出现覆盖询问 -p连同源文件属性(权限、用户、时间)一起复制 -r递归复制文件目录 -s快捷方式形式复制(软连接) -l硬连接 -u目标不存在才复制,或存在但mtime较旧 rm [-fir] 文件或目录 - remove,删除 -f忽略警告 -i删除询问 -r递归删除 mv [-fiu] source*(一个或多个来源文件或目录) directory(目标目录) - move,移动或重命名 -f强制覆盖 -i覆盖询问 -u目标对象存在且mtime较旧的情况下才更新 文件内容查阅 cat [-AbEnTv] File,Concatenate,从第一行开始显示文件全部内容 -b显示行号(不含空白行) -n显示行号(包含空白行) tac,相对应上面的cat命令,反向从最后一行开始显示文件全部内容 nl [-bnw] File,类同于上面两个命令 ( 暂无说明 ) more File,一页页向下翻阅查看文件 space空格键翻页 b 或 ctrl + b往回翻页 enter确认键翻行 /字符串向下搜索 :f显示文件名和当前末尾行的行数 q退出 less File,类似于more但功能更强大,同样用于翻阅查看文件 space空格键 或 page down翻页 b 或 ctrl + b 或 page up往回翻页 enter确认键翻行 /字符串向下搜索 ?字符串向上搜索 n搜索匹配的下一个 N搜索匹配的前一个 :f显示文件名、当前显示行范围、byte大小和百分占比等 g 或 home跳到文件开头 G 或 end跳到文件末尾 q退出 head [-n num] File,取出前面几行 head -n 100 info.log,取出前100行 head -n -100 info.log,取出不包括最后100行的所有行 tail [-n num] File,取出后面几行 -f侦测文件输出 tail -n 100 info.log,取出后100行 tail -n +100 info.log,取出100行以后的所有行 od [-t acdfox] File,非纯文档文本读取命令( 暂无说明 ) 文件预设权限 umask [-S] [-num] ,显示当前用户在新建文件或目录时的权限默认值 umask -num,修改当前用户在新建文件或目录时的权限默认值 文件隐藏属性 chattr [+-=][ASacdistu] 文件或目录,配置文件隐藏属性 +增设属性 -删减属性 =赋值属性 a只能追加数据不可修改删除 i文件不能删除修改添加改名等 lsattr [-adR] 文件或目录,显示文件隐藏属性 a包括隐藏文件 d仅列出目录属性 R包含子目录 脚本文件搜寻 which [-a] command,搜寻该command的执行档所在位置 whereis -[lbmsu] 文件或目录,在指定目录搜索文件或目录 l列出指定目录 b只找binary文件 m只找在manual路径下的文件 s只找来源文件 u只找不在上面三种类型中的其它文件 locate [-iclSr] keyword,根据指定数据库/var/lib/mlocate/mlocate.db搜索,可用updatedb命令更新 i忽略大小写 c输出统计数量 l后面接数量,表示输出几行 S展示数据库信息 find [PATH] [option] 文件或目录(支持正则匹配),功能比较强大和丰富,具体请用man find查看,常见的option选项有: [-{mtime,atime,ctime} {+,-}n],与时间相关的帅选 [-{user,group,nouser,nogroup}],与拥有者或群组相关的过滤 [-{name,size,type}],与文件名、大小、类型有关的参数 重点回顾 欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Linux","slug":"Linux","permalink":"https://blog.mariojd.cn/categories/Linux/"}],"tags":[{"name":"linux thing","slug":"linux-thing","permalink":"https://blog.mariojd.cn/tags/linux-thing/"},{"name":"common commands","slug":"common-commands","permalink":"https://blog.mariojd.cn/tags/common-commands/"},{"name":"file management","slug":"file-management","permalink":"https://blog.mariojd.cn/tags/file-management/"},{"name":"directory management","slug":"directory-management","permalink":"https://blog.mariojd.cn/tags/directory-management/"}]},{"title":"Linux 私房菜:文件目录权限那点事","slug":"Linux 私房菜:文件目录权限那点事","date":"2018-09-28","updated":"2019-06-02","comments":true,"path":"linux-file-and-directory-permission.html","link":"","permalink":"https://blog.mariojd.cn/linux-file-and-directory-permission.html","excerpt":"","keywords":"","text":"本文内容精简、整理、摘抄、有感于《鸟哥的Linux私房菜》第五章 • Linux的文件权限与目录配置。 文件目录身份 owner -> u -> 拥有者 group -> g -> 所属群 others -> o -> 其它人 文件目录权限 read -> r -> 可读 write -> w -> 可写(注意:于文件来说,w仅针对文件内容而言,因此是可写但不可删;而目录有w权限则可删,含目录下的文件) execute -> x -> 可执行(注意:对文件来说,这只是个状态,能否执行成功只跟文件本身有关;而目录有x权限则表示可进入该目录) Tip:常见的目录开放,大多只给r和x权限而保留具有删除风险的w权限 共有十个位,注意到第一个字符为[-],表示文件;常见的还有d,代表目录(directory);l,表示连接档(link) 相关文件位置 root相关信息 -> /etc/passwd 普通用户信息 -> /etc/shadow 群组成员信息 -> /etc/group 修改文件目录属性与权限 [change owner] –> chown: 用于更改文件所有者,不过也可以修改所属群组,-R参数用于递归修改子目录及文件,示例: chown [-R] owner file/dir chown [-R] owner:group file/dir或chown [-R] user.group file/dir 只修改所属组: chown [-R] .group file/dir Tip: cp复制会保留执行者的属性与权限 [change group] –> chgrp: 用于更改文件所属组,-R参数用于递归修改子目录及文件,示例: chgrp [-R] 组名 文件或目录 [change modify] –> chmod: 用于修改文件权限,该设定有两种方式(数字or符号),下面分别介绍: 数字类型:read/write/execute用数字来对应,分别为:r -> 4 | w -> 2 | x -> 1。举例[-rwx---r--],对应owner=rwx=4+2+1=7,group=---=0+0+0=0,others=r--=4+0+0=4,因此实际命令是chmod [-R] 704 file/dir 符号类型:owner/group/others用u/g/o来对应,符号a包含这三个表示全部,此外还用到r/w/x表示相应的权限,+/-/=用于操作和赋权。如[-rwx-w-r--],对应u=rwx,g=w,o=r,实际命令为:chmod [-R] u=rwx,g=w,o=r file/dir;同时修改这三者,可以用a,如同时赋值可写可执行权限:chmod [-R] a+wx file/dir,去除可读权限:chmod [-R] a-r file/dir,以此类推 重点回顾 欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Linux","slug":"Linux","permalink":"https://blog.mariojd.cn/categories/Linux/"}],"tags":[{"name":"linux thing","slug":"linux-thing","permalink":"https://blog.mariojd.cn/tags/linux-thing/"},{"name":"file permission","slug":"file-permission","permalink":"https://blog.mariojd.cn/tags/file-permission/"},{"name":"directory permission","slug":"directory-permission","permalink":"https://blog.mariojd.cn/tags/directory-permission/"}]},{"title":"Linux 私房菜:回头去学","slug":"Linux 私房菜:回头去学","date":"2018-09-27","updated":"2019-06-02","comments":true,"path":"go-back-to-learn-linux.html","link":"","permalink":"https://blog.mariojd.cn/go-back-to-learn-linux.html","excerpt":"","keywords":"","text":"说来惭愧,从学编程到现在写代码,算是用过不少东西,不过很多只停留在半解半知的状态,包括Linux在内,一直以来还没有较为系统的从基础开始学起。最近逼着自己回头看Linux相关书籍:《鸟哥的Linux私房菜》,此书真是蛮厚的,花点时间和心思啃完先。因为Linux本身的知识体系非常庞大,应用也非常广泛,所以光凭理解和记忆肯定是不行的了,按目前的计划构想,将get的好知识点梳理出来,方便日后再学习回顾。 先整理一些基本的入门知识点: 操作符: root超管:# 普通用户:$ 命令形式:command [-options] *parameters,命令太长用反斜杠\\转义到下一行 小工具: 日期时间:date 日历:cal [ [month] year] 计算器:bc 常用热键: Tab,命令及文件快速补全; Ctrl + c,输入命令快速中断; Ctrl + d,退出相当于exit效果 帮助命令: man command,全称manual (操作说明),用于查看命令或文件的用法 info page,提供一种在线求助的方式 数据同步写入磁盘:sync 关机命令: shutdown reboot halt poweroff 欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Linux","slug":"Linux","permalink":"https://blog.mariojd.cn/categories/Linux/"}],"tags":[{"name":"linux thing","slug":"linux-thing","permalink":"https://blog.mariojd.cn/tags/linux-thing/"}]},{"title":"爬虫进阶:Scrapy 抓取 boss 直聘、拉勾心得经验","slug":"爬虫进阶:Scrapy 抓取 boss 直聘、拉勾心得经验","date":"2018-09-26","updated":"2019-06-02","comments":true,"path":"share-scrapy-experience-in-crawl-zhipin-and-lagou.html","link":"","permalink":"https://blog.mariojd.cn/share-scrapy-experience-in-crawl-zhipin-and-lagou.html","excerpt":"","keywords":"","text":"关于使用Scrapy的体会,最明显的感受就是这种模板化、工程化的脚手架体系,可以说是拿来即可开箱便用,大多仅需按一定的规则套路配置,剩下的就是专注于编写跟爬虫业务有关的代码。绝大多数的反反爬虫策略,大多有以下几种: 忽略robots.txt协议 添加随机请求头,如cookie、user-agent等 sleep休眠 控制并发请求数、设置页面下载延迟 验证码识别(靠谱) 使用ip代理池(最靠谱) 文章的出发点是share本人使用scrapy爬取Boss和拉勾两个招聘网的一些实操总结经验。两个网站的网页结构不尽相同,好在需要及提取出的最终数据基本保持了一致,出于两个网站不同的反爬策略和请求配置(settings.py),因此对应实际的情况是两个Scrapy项目。 友情提醒,这里不介绍scrapy示例及完整代码(Tip: 下方贴有完整代码链接)。如文章标题描述的那样,由于拉勾和Boss都有各自不同的反爬策略,多少也限制了一些爬虫学习者的热情,包括我自身在内,不过多番尝试之后还是有收获的,跑的是定时计划,目前已入库的有近三万条数据。 进入正题,下面分别介绍拉勾网以及Boss直聘网岗位招聘数据的爬取心得,不过网站的反爬策略和网页结构随时都有发生变动的可能,因此还是需要根据实际情况进行再调整。本次分享的内容截止到2018年09月28日,在实际运行项目中依然生效。 拉勾 关闭默认cookie(否则会跟请求头中的Cookie冲突),自定义请求headers并添加随机Cookie属性 在settings.py中找到并设置COOKIES_ENABLED = False 可以在spider代码中为Request手动添加headers,或者修改settings.py中的DEFAULT_REQUEST_HEADERS属性,或者编写自定义的HeadersMiddleware(继承DefaultHeadersMiddleware,重写process_request方法,别忘了配置) 拉勾网的cookie,用uuid随机生成即可,参考如下: def random_cookie(): args = (uuid.uuid4(),) * 5 cookie = 'user_trace_token={}; LGUID={}; JSESSIONID={}; LGSID={}; LGRID={}'.format(*args) return cookie 控制并发请求数及设置下载延迟 在settings.py中找到并设置如下,因此理论上一天可抓60/2 * 2 * 60 * 24 =86400条数据: # 当并发数和下载延迟均设置为2时,没有出现反爬限制(可多次尝试)CONCURRENT_REQUESTS = 2DOWNLOAD_DELAY = 2 补充:上述说明中,请求Cookie是必须要加的,否则会重定向到登录页面;在未使用ip代理的情况下,并发数不宜设置过大,亲测是直接封了IP… Boss直聘对比拉勾网,感觉直聘网的反爬策略会更加严格,不过抓慢一点还是可以的(这样理论上一天是60/5 * 1 * 60 * 24 =17280条数据) 设置随机User-Agent(可能非必需) 控制并发请求数、下载延迟 # 这么慢还是被限制...CONCURRENT_REQUESTS = 1DOWNLOAD_DELAY = 5 加入验证码识别 事实上,这种情况下限制后是被重定向到一个验证码页面。本人目前的解决方案是编写自定义的CustomRedirectMiddleware(继承RedirectMiddleware,重写process_response方法进行验证码识别[第三方API]) 小结分享的都是比较曲中的解决方案,不差钱的建议直接上ip代理(实测过免费的,别指望了)。 相关代码 - 拉勾相关代码 - boss欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.mariojd.cn/categories/Python/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.mariojd.cn/tags/python/"},{"name":"scrapy","slug":"scrapy","permalink":"https://blog.mariojd.cn/tags/scrapy/"},{"name":"boss","slug":"boss","permalink":"https://blog.mariojd.cn/tags/boss/"},{"name":"lagou","slug":"lagou","permalink":"https://blog.mariojd.cn/tags/lagou/"}]},{"title":"Docker + Elasticsearch 集群环境搭建","slug":"Docker + Elasticsearch 集群环境搭建","date":"2018-09-26","updated":"2018-09-29","comments":true,"path":"elasticsearch-cluster-environment-construction-based-docker.html","link":"","permalink":"https://blog.mariojd.cn/elasticsearch-cluster-environment-construction-based-docker.html","excerpt":"","keywords":"","text":"无论是安装包形式还是基于Docker,搭建Elasticsearch集群环境还是较为简单的,实操的时候还遇到过一丢小问题,本文用于记录下操作过程。 运行先用docker分别启动两个es服务,由于后面需要进行配置,这里假设es1所在的机器公网ip为:123.11.23.1,es2所在机器ip:123.11.23.2 # 运行ES1docker run --name es1 -e "ES_JAVA_OPTS=-Xms128m -Xmx128m" -d -p 9200:9200 -p 9300:9300 docker.elastic.co/elasticsearch/elasticsearch-oss:6.3.2# 运行ES2docker run --name es2 -e "ES_JAVA_OPTS=-Xms128m -Xmx128m" -d -p 19200:9200 -p 19300:9300 docker.elastic.co/elasticsearch/elasticsearch-oss:6.3.2 启动的时候指定了jvm参数,此外还可以通过编辑config/jvm.options配置文件进行参数调整。 测试命令:curl localhost:9200 && curl localhost:19200,注意到响应字段cluster_name,不出意料默认应该都是docker-cluster,然后两个es实例对应的name应该不同(随机分配)。 配置Elasticsearch集群环境由多个节点(es实例)之间互相发现并组成,因此核心关键就是正确配置,而主要的配置文件也仅有config/elasticsearch.yml。基于Docker启动服务的时候可以选择将配置目录(或文件)挂载出来,但这里并没有这么做,所以还可以进入到容器内部进行操作,以es1为例: # 进入es1docker exec -it es1 bash# 编辑config目录下的elasticsearch.ymlvi config/elasticsearch.yml 下图Elasticsearch v6.3.2中的配置项较于低版本可是精简了很多(约定大于配置),由于本文仅介绍ES集群的搭建,所以详尽的配置说明可以参考这里。 简单起见,本示例就不修改cluster.name这个配置项了,一个es集群首先就是要保证各节点该配置的相同,还有一个配置属性:node.name,这里应该设置成更为容易理解的节点名称。 接着继续贴上两项配置,这也是让节点之间相互发现的关键所在: # 当前机器公网ipnetwork.publish_host: 123.11.23.1# 其它节点的位置discovery.zen.ping.unicast.hosts: [\"123.11.23.2:19300\"] 总之,参考上述说明,根据实际情况分别配置好两个es实例,最后重启实例即可: docker restart es1 && docker restart es2 测试以下列出的几种方式都可以查看集群状态: 查看日志:docker logs -f es1 节点信息:curl http://localhost:9200/_nodes?pretty 集群健康:curl http://localhost:9200/_cluster/health 插件elasticsearch-head 欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Docker","slug":"Docker","permalink":"https://blog.mariojd.cn/categories/Docker/"},{"name":"Elasticsearch","slug":"Docker/Elasticsearch","permalink":"https://blog.mariojd.cn/categories/Docker/Elasticsearch/"}],"tags":[{"name":"docker","slug":"docker","permalink":"https://blog.mariojd.cn/tags/docker/"},{"name":"elasticsearch cluster","slug":"elasticsearch-cluster","permalink":"https://blog.mariojd.cn/tags/elasticsearch-cluster/"}]},{"title":"Python 中的 ORM 工具:Peewee","slug":"Python 中的 ORM 工具:Peewee","date":"2018-09-21","updated":"2019-06-02","comments":true,"path":"the-orm-tool-peewee-in-python.html","link":"","permalink":"https://blog.mariojd.cn/the-orm-tool-peewee-in-python.html","excerpt":"","keywords":"","text":"上一篇文章介绍了Pyhton中的ORM工具:SQLAlchemy。本文延续之前的风格,介绍另一个ORM模块:Peewee,希望通过简单的CRUD示例可以帮助大家快速上手。 环境说明 python v3.6.5 peewee v3.7.0 faker v0.9.1(生成伪造数据) 安装环境pip install peewee faker CRUD示例同样的,Peewee也支持绝大多数关系型数据库,示例中使用的是PostgreSQL,用法及说明大多已在源代码中注释,请具体参考如下: from peewee import *from faker import Factoryfrom datetime import datetime# Create an instance of a Databasedb = PostgresqlDatabase(database=\"postgres\", host=\"localhost\", port=5432, user=\"postgres\", password=\"password\", )# Define a model classclass User(Model): # If none of the fields are initialized with primary_key=True, # an auto-incrementing primary key will automatically be created and named 'id'. id = PrimaryKeyField() email = CharField(index=True, max_length=64) username = CharField(unique=True, max_length=32) password = CharField(null=True, max_length=64) createTime = DateTimeField(column_name=\"create_time\", default=datetime.now) class Meta: database = db table_name = 'tb_user' # If Models without a Primary Key # primary_key = False def __str__(self) -> str: return \"User(id:{} email:{} username:{} password:{} createTime: {})\".format( self.id, self.email, self.username, self.password, self.createTime)db.connect()db.drop_tables([User])db.create_tables([User])\"\"\" CREATE \"\"\"# 创建User对象user = User(email=\"zs@123.com\", username=\"张三\", password=\"zs\")# 保存Useruser.save()# 创建faker工厂对象faker = Factory.create()# 利用faker创建多个User对象fake_users = [{ 'username': faker.name(), 'password': faker.word(), 'email': faker.email(),} for i in range(5)]# 批量插入User.insert_many(fake_users).execute()\"\"\" RETRIEVE/GET/FIND \"\"\"user = User().select().where(User.id != 1).get()print(user)user = User.select().where(User.username.contains(\"张\")).get()print(user)count = User.select().filter(User.id >= 3).count()print(count)users = User.select().order_by(User.email)for u in users: print(u)\"\"\" UPDATE \"\"\"effect_count = User.update({User.username: \"李四\", User.email: \"ls@163.com\"}).where(User.id == 1).execute()print(effect_count)\"\"\" DELETE \"\"\"effect_count = User().delete_by_id(6)print(effect_count)effect_count = User.delete().where(User.id >= 4).execute()print(effect_count) 参考链接 peewee 3.7.0 documentation 示例源码欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.mariojd.cn/categories/Python/"}],"tags":[{"name":"Python","slug":"Python","permalink":"https://blog.mariojd.cn/tags/Python/"},{"name":"ORM","slug":"ORM","permalink":"https://blog.mariojd.cn/tags/ORM/"},{"name":"Peewee","slug":"Peewee","permalink":"https://blog.mariojd.cn/tags/Peewee/"},{"name":"Faker","slug":"Faker","permalink":"https://blog.mariojd.cn/tags/Faker/"}]},{"title":"Python 中的 ORM 工具:SQLAlchemy","slug":"Python 中的 ORM 工具:SQLAlchemy","date":"2018-09-20","updated":"2019-06-02","comments":true,"path":"the-orm-tool-sqlalchemy-in-python.html","link":"","permalink":"https://blog.mariojd.cn/the-orm-tool-sqlalchemy-in-python.html","excerpt":"","keywords":"","text":"ORM全称Object Relational Mapping, 翻译过来叫对象关系映射。在Python生态中,目前较为流行的ORM模块有SQLAlchemy和peewee,类比Java中有Hibernate和MyBatis。本文关注SQLAlchemy的快速上手,展示一个简单的 CRUD 示例,并结合使用 Faker 生成测试数据。 环境说明 python v3.6.5 sqlalchemy v1.2.11 faker v0.9.1(生成伪造数据) 安装环境pip install sqlalchemy faker CRUD示例SQLAlchemy支持大多数关系型数据库,示例中使用的是PostgreSQL,用法及说明大多已在源代码中注释,具体请参考如下: from faker import Factoryfrom sqlalchemy import or_from sqlalchemy.orm import sessionmakerfrom sqlalchemy.ext.declarative import declarative_basefrom sqlalchemy import Column, String, Integer, create_engine# 创建对象的基类:Base = declarative_base()# 定义User对象并继承上述基类class User(Base): # 表名(继承自Base的类必须要定义__tablename__) __tablename__ = 'tb_user' # 表字段 id = Column(Integer, primary_key=True, autoincrement=True, comment=\"自增主键\") email = Column(String(64), nullable=False, index=True, comment=\"邮箱\") username = Column(String(64), nullable=False, comment=\"用户名\") password = Column(String(64), comment=\"密码\") def __str__(self) -> str: return \"User(id:{} email:{} username:{} password:{})\".format(self.id, self.email, self.username, self.password)# 创建数据库连接('数据库类型+数据库驱动名称://用户名:密码@ip地址:端口/数据库名')conn = \"postgresql+psycopg2://postgres:<password>@<ip>:5432/postgres\"engine = create_engine(conn, encoding='UTF-8', echo=False) # echo=True表示输出执行日志,默认为False# 删除映射数据表(如果存在)Base.metadata.drop_all(engine)# 创建映射数据表(如果不存在)Base.metadata.create_all(engine)# 创建session对象Session = sessionmaker(bind=engine)session = Session()\"\"\" CREATE \"\"\"# 创建User对象user = User(username=\"张三\", password=\"zs\", email=\"123@zs.com\")# 添加到sessionsession.add(user)# 提交到数据库:session.commit()# 创建faker工厂对象faker = Factory.create()# 利用faker创建多个User对象fake_users = [User( username=faker.name(), password=faker.word(), email=faker.email(),) for i in range(5)]# 添加到sessionsession.add_all(fake_users)# 保存到数据库:session.commit()\"\"\" RETRIEVE/GET/FIND \"\"\"# 主键id查找user = session.query(User).get(3)print(user)# 添加过滤条件user = session.query(User).filter(User.id != 1).first()print(user)# one()方法查找不存在或返回结果集不止一个对象会抛异常,first()对应则返回None或首条数据user = session.query(User).filter_by(id=4).one()print(user)# 模糊搜索user = session.query(User).filter(User.username.like('%张%')).first()print(user)user = session.query(User).filter(User.username.notlike('%张%')).first()print(user)# And and Oruser = session.query(User).filter(User.id == 1, User.username.like('%张%')).one()print(user)user = session.query(User).filter(or_(User.id == 2, User.username.like('%张%'))).first()print(user)# 查找全部users = session.query(User).all()for u in users: print(u)\"\"\" UPDATE \"\"\"user = session.query(User).get(1)user.username, user.password = '李四', 'ls'session.add(user)session.commit()\"\"\" DELETE \"\"\"user = session.query(User).get(6)session.delete(user)session.commit()session.close() 参考链接 SQLAlchemy(一)SQLAlchemy(二)使用SQLAlchemy 示例源码欢迎关注我的个人公众号:超级码里奥如果这对您有帮助,欢迎点赞和分享,转载请注明出处","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.mariojd.cn/categories/Python/"}],"tags":[{"name":"Python","slug":"Python","permalink":"https://blog.mariojd.cn/tags/Python/"},{"name":"ORM","slug":"ORM","permalink":"https://blog.mariojd.cn/tags/ORM/"},{"name":"Faker","slug":"Faker","permalink":"https://blog.mariojd.cn/tags/Faker/"},{"name":"SQLAlchemy","slug":"SQLAlchemy","permalink":"https://blog.mariojd.cn/tags/SQLAlchemy/"}]},{"title":"十五个常用经典的 Java8 Stream API 用法示例","slug":"十五个常用经典的 Java8 Stream API 用法示例","date":"2018-09-18","updated":"2019-06-02","comments":true,"path":"15-examples-of-java8-stream-usage.html","link":"","permalink":"https://blog.mariojd.cn/15-examples-of-java8-stream-usage.html","excerpt":"","keywords":"","text":"不出意外的话,再过几天,Java 11就要正式对外发布了,不知各位同行都用上哪个版本了呢?先贴一张截取的调查图,由此可见,目前Java 7/8的使用群体还是相当之大的。 下面列举了十五个常用经典的Java8 Stream API用法示例。为了方便操作,首先抽取并定义公共数组变量array。 private Integer[] array = {10, 3, 3, 15, 9, 23}; map private void map() { List<Integer> collect = Stream.of(array).map(n -> n * 2).collect(Collectors.toList()); System.out.println(\"collect = \" + collect); // [20, 6, 6, 30, 18, 46] collect = Stream.of(array).mapToInt(n -> n * 2).boxed().collect(Collectors.toList()); System.out.println(\"collect = \" + collect); // [20, 6, 6, 30, 18, 46] ArrayList<Long> collect1 = Stream.of(array).mapToLong(Integer::longValue).boxed().collect(Collectors.toCollection(ArrayList::new)); System.out.println(\"collect1 = \" + collect1); // [10, 3, 3, 15, 9, 23] TreeSet<Object> collect2 = Stream.of(array).mapToDouble(Integer::doubleValue).collect(TreeSet::new, TreeSet::add, TreeSet::addAll); System.out.println(\"collect2 = \" + collect2); // [3.0, 9.0, 10.0, 15.0, 23.0]} filter private void filter() { Object[] objects = Stream.of(array).filter(n -> n >= 10).toArray(); System.out.println(\"objects = \" + Arrays.toString(objects));// [10, 15, 23]} sort private void sort() { // naturalOrder List<Integer> collect = Stream.of(array).sorted().collect(Collectors.toList()); System.out.println(\"collect = \" + collect); // [3, 3, 9, 10, 15, 23] // reverseOrder collect = Stream.of(array).sorted(Comparator.reverseOrder()).collect(Collectors.toList()); System.out.println(\"collect = \" + collect); // [23, 15, 10, 9, 3, 3] collect = Stream.of(array).sorted(Comparator.comparingInt(Integer::intValue).reversed()).collect(Collectors.toList()); System.out.println(\"collect = \" + collect); // [23, 15, 10, 9, 3, 3]} skip private void skip() { TreeSet<Integer> collect = Stream.of(array).skip(3).collect(Collectors.toCollection(TreeSet::new)); System.out.println(\"collect = \" + collect); // [9, 15, 23] } distinct private void distinct() { LinkedList<Integer> collect = Stream.of(array).distinct().collect(Collectors.toCollection(LinkedList::new)); System.out.println(\"collect = \" + collect); // [10, 3, 15, 9, 23]} sum and count private void sumAndCount() { int sum = Stream.of(array).mapToInt(Integer::intValue).sum(); System.out.println(\"sum = \" + sum); // 63 long sum1 = Stream.of(array).mapToLong(Integer::intValue).sum(); System.out.println(\"sum1 = \" + sum1); // 63 double sum2 = Stream.of(array).mapToDouble(Integer::intValue).sum(); System.out.println(\"sum2 = \" + sum2); // 63.0 // array.length is equal 6 long count = Stream.of(array).count(); System.out.println(\"sumAndCount = \" + count); // 6} limit private void limit() { Set<Integer> collect = Stream.of(array).limit(3).collect(Collectors.toSet()); System.out.println(\"collect = \" + collect); // [3, 10]} match boolean allMatch = Stream.of(array).allMatch(n -> n > 5);System.out.println(\"allMatch = \" + allMatch); // falseboolean anyMatch = Stream.of(array).anyMatch(n -> n > 5);System.out.println(\"anyMatch = \" + anyMatch); // trueboolean noneMatch = Stream.of(array).noneMatch(n -> n > 5);System.out.println(\"noneMatch = \" + noneMatch); // false find private void find() { Optional<Integer> any = Stream.of(array).filter(n -> n * 2 > 10 && n * 2 < 20).findAny(); any.ifPresent(n -> System.out.println(\"any = \" + n)); // 9 any = Stream.of(array).filter(n -> n < 5).findFirst(); any.ifPresent(n -> System.out.println(\"any = \" + n)); // 3} min and max private void minAndMax() { Stream.of(array).min(Comparator.naturalOrder()).ifPresent(n -> System.out.println(\"n = \" + n)); // 3 Stream.of(array).max(Comparator.comparingInt(Integer::intValue)).ifPresent(n -> System.out.println(\"n = \" + n)); // 23} peek private void peek() { LinkedHashSet<Integer> collect = Stream.of(array).peek(n -> System.out.print(n + \" \")).collect(Collectors.toCollection(LinkedHashSet::new)); System.out.println(); System.out.println(\"collect = \" + collect); // [10, 3, 15, 9, 23]} reduce private void reduce() { Integer reduce = Stream.of(array).reduce(100, Integer::sum); System.out.println(\"reduce = \" + reduce); // 163} flatMap private void flatMap() { List<Integer> collect = Stream.of(array, array).flatMap(Arrays::stream).collect(Collectors.toList()); System.out.println(\"collect = \" + collect); // [10, 3, 3, 15, 9, 23, 10, 3, 3, 15, 9, 23]} present private void present() { boolean isPresent = Stream.of(array).findAny().isPresent(); System.out.println(\"isPresent = \" + isPresent); Stream.of(array).filter(n -> n == 20).findFirst().ifPresent(n -> System.out.println(\"n = \" + n));} summaryStatistics private void summaryStatistics() { IntSummaryStatistics collect = Stream.of(array).collect(Collectors.summarizingInt(Integer::intValue)); System.out.println(collect.getCount() + \" \" + collect.getSum() + \" \" + collect.getMin() + \" \" + collect.getAverage() + \" \" + collect.getMax()); System.out.println(\"collect = \" + collect); // IntSummaryStatistics{sumAndCount=6, sum=63, min=3, average=10.500000, max=23} IntSummaryStatistics intSummaryStatistics = Stream.of(array).mapToInt(Integer::intValue).summaryStatistics(); System.out.println(\"intSummaryStatistics = \" + intSummaryStatistics); // IntSummaryStatistics{sumAndCount=6, sum=63, min=3, average=10.500000, max=23}}","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"java8 stream","slug":"java8-stream","permalink":"https://blog.mariojd.cn/tags/java8-stream/"}]},{"title":"Elasticsearch 地理坐标类型 (Geo-point) 在 Spring Data ES 中的常见使用问题整理解答","slug":"Elasticsearch 地理坐标类型 (Geo-point) 在 Spring Data ES 中的常见使用问题整理解答","date":"2018-09-12","updated":"2019-06-02","comments":true,"path":"common-questions-with-answer-in-spring-data-elasticsearch.html","link":"","permalink":"https://blog.mariojd.cn/common-questions-with-answer-in-spring-data-elasticsearch.html","excerpt":"","keywords":"","text":"  下文整理的几个问答,本人在实际应用中亲身经历或解决过的,主要涉及Elasticsearch地理坐标类型(Geo-point)在Java应用中的一些特殊使用场景,核心依赖如下: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId></dependency> A1. elasticsearch的geo_point类型对应java中的哪种数据类型? Q1. spring data elasticsearch中定义了GeoPoint这个类来实现两者之间的类型映射,此外还需要为当前字段添加@GeoPointField注解进行标志,注意GeoPoint应该使用org.springframework.data.elasticsearch.core.geo包下的。/** * 坐标位置 */@GeoPointFieldprivate GeoPoint location; A2. spring data elasticsearch中,如何以某坐标点为中心搜索指定范围的其它点? Q2. 建议尽可能通过继承ElasticsearchRepository<T, ID extends Serializable>来简化完成相关查询;   实现以某点为中心并搜索指定范围,首先定义如下: public interface TestRepository extends ElasticsearchRepository<Test, String> {}   其次可通过QueryBuilder接口来实现上述功能,参考如下: @Servicepublic class TestService { @Resource private TestRepository testRepository; public Page<Test> findPage(double latitude, double longitude, String distance, Pageable pageable) { // 间接实现了QueryBuilder接口 BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); // 以某点为中心,搜索指定范围 GeoDistanceQueryBuilder distanceQueryBuilder = new GeoDistanceQueryBuilder(\"location\"); distanceQueryBuilder.point(latitude, longitude); // 定义查询单位:公里 distanceQueryBuilder.distance(distance, DistanceUnit.KILOMETERS); boolQueryBuilder.filter(distanceQueryBuilder); return testRepository.search(boolQueryBuilder, pageable); } } A3. spring data elasticsearch中,如何计算两个给定坐标点之间的距离? Q3. 在GeoDistance类中定义了相关的计算方法,参考如下: // 计算两点距离double distance = GeoDistance.ARC.calculate(srcLat, srcLon, dstLat, dstLon, DistanceUnit.KILOMETERS);   关于GeoDistance.ARC和GeoDistance.PLANE,前者比后者计算起来要慢,但精确度要比后者高,具体区别可以看这里。 A4. spring data elasticsearch应用中,如何以某个坐标点为中心,按距离近远排序搜索指定范围? Q4. 通过SearchQuery来实现,参考下面这段代码中GeoDistanceSortBuilder的使用:@Servicepublic class TestService { @Resource private TestRepository testRepository; public Page<Test> findPage(double latitude, double longitude, String distance, Pageable pageable) { // 实现了SearchQuery接口,用于组装QueryBuilder和SortBuilder以及Pageable等 NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder(); nativeSearchQueryBuilder.withPageable(pageable) // 间接实现了QueryBuilder接口 BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); // 以某点为中心,搜索指定范围 GeoDistanceQueryBuilder distanceQueryBuilder = new GeoDistanceQueryBuilder("location"); distanceQueryBuilder.point(latitude, longitude); // 定义查询单位:公里 distanceQueryBuilder.distance(distance, DistanceUnit.KILOMETERS); boolQueryBuilder.filter(distanceQueryBuilder); nativeSearchQueryBuilder.withQuery(boolQueryBuilder); // 按距离升序 GeoDistanceSortBuilder distanceSortBuilder = new GeoDistanceSortBuilder("location", latitude, longitude); distanceSortBuilder.unit(DistanceUnit.KILOMETERS); distanceSortBuilder.order(SortOrder.ASC); nativeSearchQueryBuilder.withSort(distanceSortBuilder); return testRepository.search(nativeSearchQueryBuilder.build()); } }","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"https://blog.mariojd.cn/tags/Spring-Boot/"},{"name":"Spring Data Elasticsearch","slug":"Spring-Data-Elasticsearch","permalink":"https://blog.mariojd.cn/tags/Spring-Data-Elasticsearch/"}]},{"title":"Spring Boot 项目 Docker 化快速上手","slug":"Spring Boot 项目 Docker 化快速上手","date":"2018-09-04","updated":"2019-06-02","comments":true,"path":"build-spring-boot-project-with-docker.html","link":"","permalink":"https://blog.mariojd.cn/build-spring-boot-project-with-docker.html","excerpt":"","keywords":"","text":"开篇  服务部署是应用上线前的必需环节。大道至简,如何做到项目简单化、自动化甚至傻瓜式部署是考验DevOps的一大难题。从docker面世到k8s的脱颖而出,有越来越多的开源工具也在帮助开发和运维人员解决这些问题。   对于开发人员,了解和学习Docker可谓是迫在眉睫。网上有很多优秀的学习资源,当然了,本文的主角不全是docker,但也不无关系。   现在是CI时代。用过Jenkins,但对于个人或小型项目来说太笨重了,不过还有例如像Travis这样的轻量级CI可供我们选择。本文要介绍的不涉及CI工具,只需要借助Maven插件,就可以轻松打包SpringBoot项目到远程服务器,启动镜像即可部署上线。如果这描述适合你最近在捣腾的一些项目,可以继续往下看! 环境说明 Docker v18.06.1-ce Spring Boot v2.0.4.RELEASE 配置准备  通过Maven将本地打包好的SpringBoot Jar包推送到远程Docker服务中,关键步骤就在于为Docker服务配置这个暴露端口。这步操作也简单,直接命令编辑vim /lib/systemd/system/docker.service( 左侧为Ubuntu的路径; CentOS 的路径参考:/usr/lib/systemd/system/docker.service),找到ExecStart=/usr/bin/dockerd -H fd://这行,修改为ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:22375 -H unix:///var/run/docker.sock(表示让Docker服务监听22375这个TCP端口),重启让配置生效:systemctl daemon-reload && systemctl restart docker   下面测试一下: curl localhost:22375/info,如果有一堆信息出来就可以继续往下走: 插件添加  没有Spring Boot项目可以自行新建,然后在pom.xml中添加如下配置: <build> <!-- 最终Maven本地打包出来的jar包名称 --> <finalName>example</finalName> <defaultGoal>package</defaultGoal> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <!-- GitHub: https://github.com/spotify/docker-maven-plugin --> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>1.1.1</version> <configuration> <!-- 远程镜像名称 --> <imageName>example</imageName> <forceTags>true</forceTags> <imageTags> <imageTag>${project.version}</imageTag> <imageTag>latest</imageTag> </imageTags> <dockerHost>http://服务器IP:22375</dockerHost> <dockerDirectory>src/main/docker</dockerDirectory> <resources> <resource> <targetPath>/</targetPath> <directory>${project.build.directory}</directory> <include>${project.build.finalName}.jar</include> </resource> </resources> </configuration> </plugin> </plugins> </build>   参考上述配置,还需要新建docker文件夹,然后创建Dockerfile文件,并添加如下内容: # 指定基础镜像(必需且为第一条指令,scratch是空白镜像)FROM openjdk:8-jre-alpine# MAINTAINER已经过期,具体参考:https://docs.docker.com/engine/reference/builder/#labelLABEL author-name="author-email"# 为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile 中,我们可以事先指定某些目录挂载为匿名卷,# 这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。VOLUME /opt/tmp# 在 COPY 和 ADD 指令中选择的时候,可以遵循这样的原则,# 所有文件复制使用 COPY 指令,仅在需要自动解压缩的场合使用 ADD 指令COPY example.jar example.jar# JVM参数可选CMD ["java","-jar","-Xms64m","-Xmx64m","example.jar"]# EXPOSE 指令仅仅是声明的是运行时容器提供服务的端口# 用于帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;EXPOSE 8080 构建运行  用maven命令构建远程镜像:mvn clean package docker:build -Dmaven.test.skip=true   在远程服务器上运行服务:docker run --name example -d -p 8080:8080 example   可以查看服务启动情况:docker logs -f example 学习资源 Docker學習筆記Docker — 从入门到实践 参考链接 SpringBoot | 第十四章:基于Docker的简单部署docker-socket设置Docker开发部署Node小结","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"docker","slug":"docker","permalink":"https://blog.mariojd.cn/tags/docker/"},{"name":"spring boot","slug":"spring-boot","permalink":"https://blog.mariojd.cn/tags/spring-boot/"}]},{"title":"爬虫进阶:Scrapy 抓取科技平台 Zealer","slug":"爬虫进阶:Scrapy 抓取科技平台 Zealer","date":"2018-09-03","updated":"2019-06-02","comments":true,"path":"use-scrapy-crawl-zealer.html","link":"","permalink":"https://blog.mariojd.cn/use-scrapy-crawl-zealer.html","excerpt":"","keywords":"","text":"开篇  这次的目标网站也是本人一直以来有在关注的科技平台:Zealer,爬取的信息包括全部的科技资讯以及相应的评论。默认配置下运行,大概跑了半个多小时,最终抓取了5000+的资讯以及10几万的评论。 说明及准备  开发环境:Scrapy、Redis、PostgreSQL   数据库表:tb_zealer_series、tb_zealer_media、tb_zealer_comment   下面对上述每张表进行简要说明: tb_zealer_series,用于存放不同科技频道信息: -- ------------------------------ Table structure for tb_zealer_series-- ----------------------------DROP TABLE IF EXISTS \"public\".\"tb_zealer_series\";CREATE TABLE \"public\".\"tb_zealer_series\" ( \"id\" serial2, \"name\" varchar(25) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"cp\" int2 NOT NULL, \"platform\" varchar(20) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"enabled\" bool NOT NULL);COMMENT ON COLUMN \"public\".\"tb_zealer_series\".\"id\" IS '主键id';COMMENT ON COLUMN \"public\".\"tb_zealer_series\".\"name\" IS '系列名称';COMMENT ON COLUMN \"public\".\"tb_zealer_series\".\"cp\" IS '标志id';COMMENT ON COLUMN \"public\".\"tb_zealer_series\".\"platform\" IS '平台类型';COMMENT ON COLUMN \"public\".\"tb_zealer_series\".\"enabled\" IS '开启标志';-- ------------------------------ Indexes structure for table tb_zealer_series-- ----------------------------CREATE UNIQUE INDEX \"uni_cp\" ON \"public\".\"tb_zealer_series\" USING btree ( \"cp\" \"pg_catalog\".\"int2_ops\" ASC NULLS LAST); tb_zealer_media,用于保存科技资讯的表: -- ------------------------------ Table structure for tb_zealer_media-- ----------------------------DROP TABLE IF EXISTS \"public\".\"tb_zealer_media\";CREATE TABLE \"public\".\"tb_zealer_media\" ( \"id\" serial4, \"series_id\" int2 NOT NULL, \"post_id\" int4 NOT NULL, \"title\" varchar(100) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"desc\" varchar(1500) COLLATE \"pg_catalog\".\"default\", \"label\" varchar(100) COLLATE \"pg_catalog\".\"default\", \"cover_picture\" varchar(150) COLLATE \"pg_catalog\".\"default\", \"media_info\" json, \"comment_num\" int4 NOT NULL, \"detail_url\" varchar(100) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"live_time\" varchar(10) COLLATE \"pg_catalog\".\"default\", \"create_time\" timestamp(6) NOT NULL);COMMENT ON COLUMN \"public\".\"tb_zealer_media\".\"id\" IS '主键id';COMMENT ON COLUMN \"public\".\"tb_zealer_media\".\"series_id\" IS '系列id';COMMENT ON COLUMN \"public\".\"tb_zealer_media\".\"post_id\" IS '唯一标志';COMMENT ON COLUMN \"public\".\"tb_zealer_media\".\"title\" IS '标题';COMMENT ON COLUMN \"public\".\"tb_zealer_media\".\"desc\" IS '描述';COMMENT ON COLUMN \"public\".\"tb_zealer_media\".\"label\" IS '标签';COMMENT ON COLUMN \"public\".\"tb_zealer_media\".\"cover_picture\" IS '封面图片';COMMENT ON COLUMN \"public\".\"tb_zealer_media\".\"media_info\" IS '素材信息';COMMENT ON COLUMN \"public\".\"tb_zealer_media\".\"comment_num\" IS '评论数量';COMMENT ON COLUMN \"public\".\"tb_zealer_media\".\"detail_url\" IS '详细地址';COMMENT ON COLUMN \"public\".\"tb_zealer_media\".\"live_time\" IS '视频时长';COMMENT ON COLUMN \"public\".\"tb_zealer_media\".\"create_time\" IS '入库时间';-- ------------------------------ Indexes structure for table tb_zealer_media-- ----------------------------CREATE UNIQUE INDEX \"uni_post_id\" ON \"public\".\"tb_zealer_media\" USING btree ( \"post_id\" \"pg_catalog\".\"int4_ops\" ASC NULLS LAST); tb_zealer_comment,保存每条资讯相应的评论信息: -- ------------------------------ Table structure for tb_zealer_comment-- ----------------------------DROP TABLE IF EXISTS \"public\".\"tb_zealer_comment\";CREATE TABLE \"public\".\"tb_zealer_comment\" ( \"id\" serial4, \"post_id\" int4 NOT NULL, \"username\" varchar(50) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"avatar\" varchar(150) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"content\" varchar(1500) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"comment_time\" timestamp(6) NOT NULL, \"create_time\" timestamp(6) NOT NULL, \"user_id\" int4 NOT NULL);COMMENT ON COLUMN \"public\".\"tb_zealer_comment\".\"id\" IS '主键id';COMMENT ON COLUMN \"public\".\"tb_zealer_comment\".\"post_id\" IS '唯一标志';COMMENT ON COLUMN \"public\".\"tb_zealer_comment\".\"username\" IS '用户名';COMMENT ON COLUMN \"public\".\"tb_zealer_comment\".\"avatar\" IS '用户头像';COMMENT ON COLUMN \"public\".\"tb_zealer_comment\".\"content\" IS '评论内容';COMMENT ON COLUMN \"public\".\"tb_zealer_comment\".\"comment_time\" IS '评论时间';COMMENT ON COLUMN \"public\".\"tb_zealer_comment\".\"create_time\" IS '入库时间';COMMENT ON COLUMN \"public\".\"tb_zealer_comment\".\"user_id\" IS '用户id';-- ------------------------------ Indexes structure for table tb_zealer_comment-- ----------------------------CREATE UNIQUE INDEX \"uni_uid_pid_ctime\" ON \"public\".\"tb_zealer_comment\" USING btree ( \"user_id\" \"pg_catalog\".\"int4_ops\" ASC NULLS LAST, \"post_id\" \"pg_catalog\".\"int4_ops\" ASC NULLS LAST, \"create_time\" \"pg_catalog\".\"timestamp_ops\" ASC NULLS LAST); 抓取”科技频道”信息  考虑到这块的信息比较少且固定(如下图红框所示),所以用Request+BeautifulSoup提前获取。 import appimport requestsfrom bs4 import BeautifulSoupfrom zealer.service import sql# BeautifulSoup+Request获取所有系列postgres = app.postgres()index = 'http://www.zealer.com/list?platform={}'platforms = ['media', 'x'] # 对应Zealer官方(MEDIA)和达人专区(X)for platform in platforms: resp = requests.get(index.format(platform)) bs = BeautifulSoup(resp.text, 'html.parser') nav_list = bs.find('p', class_='nav_inner') for nav in nav_list.find_all('a', class_=''): name, nav_url = nav.text, nav.get('href') cp = int(nav_url.split('cp=')[1]) postgres.handler(sql.save_series(), (name, cp, platform, True)) 环境搭建  新建项目:scrapy startproject zealer   新建爬虫:scrapy genspider tech zealer.com Item定义# -*- coding: utf-8 -*-# Define here the models for your scraped items## See documentation in:# https://doc.scrapy.org/en/latest/topics/items.htmlfrom scrapy import Item, Fieldclass MediaItem(Item): # define the fields for your item here like: # name = scrapy.Field() postId = Field() seriesId = Field() title = Field() desc = Field() label = Field() coverPicture = Field() mediaInfo = Field() commentNum = Field() detailUrl = Field() liveTime = Field() passclass CommentItem(Item): userId = Field() postId = Field() username = Field() avatar = Field() content = Field() commentTime = Field() pass 编写爬虫  先分析下页面数据的渲染形式,通过”开发者工具” -> “Network”查看,相应的资讯以及评论数据都是请求接口获得json后再进行展示的,因此直接请求这两个接口就可以了,参考资讯接口示例 && 评论接口示例,其中资讯接口中的cid表示不同的科技频道,上面已经获取到了保存在tb_zealer_series这个表中,page分页从1开始,评论接口的id参数通过资讯接口获得。   这里注释掉默认给出的start_urls = ['http://zealer.com/'],然后重写start_requests方法来定义起始爬取逻辑。由于上述两个接口中并没有返回任何终止的条件,所以这里用比较曲折的方法来自行加判断解决: # -*- coding: utf-8 -*-import sysimport jsonimport mathimport scrapyfrom utils import mytimefrom scrapy import Requestfrom bs4 import BeautifulSoupfrom zealer.service import app, sqlfrom scrapy.loader import ItemLoaderfrom scrapy.loader.processors import TakeFirstfrom zealer.items import MediaItem, CommentItemclass TechSpider(scrapy.Spider): name = 'tech' allowed_domains = ['zealer.com'] # start_urls = ['http://zealer.com/'] def __init__(self, **kwargs): super().__init__(**kwargs) self.postgres = app.postgres() self.series_list = self.postgres.fetch_all(sql.get_series()) self.series_stop = set() # 用于判断Media抓取终止 self.max_page = sys.maxsize self.post = 'http://www.zealer.com/post/{}' self.sift = 'http://www.zealer.com/x/sift?cid={}&page={}&order=created_at' self.comment = 'http://www.zealer.com/Post/comment?id={}&page={}' def start_requests(self): for series in self.series_list: series_id, cp = series[0], series[1] for page in range(1, self.max_page): if series_id in self.series_stop: self.logger.warning('Stop Media: {}'.format(series_id)) self.series_stop.discard(series_id) break else: sift = self.sift.format(cp, page) yield Request(sift, callback=self.parse, meta={'series_id': series_id}) def parse(self, response): \"\"\"解析请求资讯接口返回的JSON数据\"\"\" data = json.loads(response.body_as_unicode()) status, messages = data.get('status'), data.get('message') self.logger.info('Media URL: {} , status: {} , messages: {}'.format(response.url, status, len(messages))) series_id = response.meta['series_id'] if messages: # 解析数据 for message in messages: loader = ItemLoader(item=MediaItem()) loader.default_output_processor = TakeFirst() post_id = message.get('id') loader.add_value('postId', int(post_id)) loader.add_value('seriesId', series_id) loader.add_value('title', message.get('title')) loader.add_value('coverPicture', message.get('cover')) comment_total = int(message.get('comment_total')) loader.add_value('commentNum', comment_total) loader.add_value('liveTime', message.get('live_time')) detail_url = self.post.format(post_id) loader.add_value('detailUrl', detail_url) yield Request(detail_url, callback=self.parse_detail, meta={'loader': loader}) else: # 终止条件 self.logger.warning('Judge Stop Media: {}'.format(series_id)) self.series_stop.add(series_id) def parse_detail(self, response): \"\"\"获取资讯详情页的数据\"\"\" loader = response.meta['loader'] desc = response.xpath('//p[@class=\"des_content\"]/text()').extract_first() loader.add_value('desc', desc) tag_list = response.xpath('//div[@class=\"right_tag\"]/a/text()').extract() loader.add_value('label', '; '.join(map(str.strip, tag_list))) media_info = response.xpath('//script[@type=\"text/javascript\"]/text()[contains(.,\"option\")]').extract_first() media_info = media_info.split('=')[1].split(';')[0].replace(' ', '') loader.add_value('mediaInfo', media_info) item = loader.load_item() comment_num = item.get('commentNum') if comment_num: \"\"\"抓取评论数据\"\"\" post_id = item.get('postId') comment_max_page = int(math.ceil(comment_num / 20)) for page in range(1, comment_max_page): yield Request(self.comment.format(post_id, page), callback=self.parse_comment, meta={'post_id': post_id}) yield item def parse_comment(self, response): \"\"\"解析获取评论数据\"\"\" data = json.loads(response.body_as_unicode()) status, count = data.get('status'), int(data.get('count')) self.logger.info('Comment URL: {} , status: {} , count: {}'.format(response.url, status, count)) if count: content = data.get('content') post_id = response.meta['post_id'] bs = BeautifulSoup(content, 'html.parser') comment_list = bs.find_all('li') for comment in comment_list: item = CommentItem() item['postId'] = post_id item['userId'] = comment.find('div', class_='list_card')['card'] item['username'] = comment.find('span', class_='mb_name').text item['avatar'] = comment.find('img')['src'] comment_text = comment.find('p') or comment.find('dd') item['content'] = comment_text.text comment_time = comment.find('span', class_='commentTime').text.strip() item['commentTime'] = self.handleCommentTime(comment_time) yield item @staticmethod def handleCommentTime(comment_time): \"\"\"处理日期问题, 当年的评论返回格式为: x月x日 hh:mm\"\"\" if comment_time.find('年') == -1: comment_time = '{}年{}'.format(mytime.now_year(), comment_time) return mytime.str_to_date_with_format(comment_time, '%Y年%m月%d日 %H:%M') 数据入库  在pipelines.py中操作数据入库,别忘了还要在settings.py中配置pipelines开启: # -*- coding: utf-8 -*-# Define your item pipelines here## Don't forget to add your pipeline to the ITEM_PIPELINES setting# See: https://doc.scrapy.org/en/latest/topics/item-pipeline.htmlfrom utils import mytimefrom zealer import itemsfrom zealer.service import app, sqlclass ZealerPipeline(object): def __init__(self) -> None: self.redis = app.redis() self.postgres = app.postgres() def process_item(self, item, spider): if isinstance(item, items.MediaItem): series_id, post_id = item.get('seriesId'), item.get('postId') key = \"zealer:seriesId:{}\".format(series_id) if not self.redis.sismember(key, post_id): item_field = ['title', 'desc', 'label', 'coverPicture', 'mediaInfo', 'commentNum', 'detailUrl', 'liveTime'] data = [item.get(field) for field in item_field] data.insert(0, series_id), data.insert(1, post_id), data.append(mytime.now_date()) effect_count = self.postgres.handler(sql.save_media(), tuple(data)) if effect_count: self.redis.sadd(key, post_id) elif isinstance(item, items.CommentItem): post_id, user_id = item.get('postId'), item.get('userId') key = \"zealer:postId:{}\".format(post_id) if not self.redis.sismember(key, user_id): item_field = ['username', 'avatar', 'content', 'commentTime'] data = [item.get(field) for field in item_field] data.insert(0, post_id), data.append(user_id), data.append(mytime.now_date()) effect_count = self.postgres.handler(sql.save_comment(), tuple(data)) if effect_count: self.redis.sadd(key, user_id) return item 效果展示 示例代码 - GitHub","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.mariojd.cn/categories/Python/"}],"tags":[{"name":"scrapy","slug":"scrapy","permalink":"https://blog.mariojd.cn/tags/scrapy/"},{"name":"zealer","slug":"zealer","permalink":"https://blog.mariojd.cn/tags/zealer/"}]},{"title":"爬虫进阶:Scrapy 抓取慕课网","slug":"爬虫进阶:Scrapy 抓取慕课网","date":"2018-09-02","updated":"2019-06-02","comments":true,"path":"use-scrapy-crawl-imooc.html","link":"","permalink":"https://blog.mariojd.cn/use-scrapy-crawl-imooc.html","excerpt":"","keywords":"","text":"前言  Scrapy抓取慕课网免费以及实战课程信息,相关环境列举如下: scrapy v1.5.1 redis psycopg2 (操作并保存数据到PostgreSQL) 数据表  完整的爬虫流程大致是这样的:分析页面结构 -> 确定提取信息 -> 设计相应表结构 -> 编写爬虫脚本 -> 数据保存入库;入库可以选择mongo这样的文档数据库,也可以选择mysql这样的关系型数据库。废话不多讲,这里暂且跳过页面分析,现给出如下两张数据表设计: -- ------------------------------ Table structure for tb_imooc_course-- ----------------------------DROP TABLE IF EXISTS \"public\".\"tb_imooc_course\";CREATE TABLE \"public\".\"tb_imooc_course\" ( \"id\" serial4, \"course_id\" int4 NOT NULL, \"name\" varchar(100) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"difficult\" varchar(30) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"student\" int4 NOT NULL, \"desc\" varchar(250) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"label\" varchar(50) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"image_urls\" varchar(250) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"detail\" varchar(250) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"duration\" varchar(50) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"overall_score\" float4, \"content_score\" float4, \"concise_score\" float4, \"logic_score\" float4, \"summary\" varchar(800) COLLATE \"pg_catalog\".\"default\", \"teacher_nickname\" varchar(30) COLLATE \"pg_catalog\".\"default\", \"teacher_avatar\" varchar(250) COLLATE \"pg_catalog\".\"default\", \"teacher_job\" varchar(30) COLLATE \"pg_catalog\".\"default\", \"tip\" varchar(500) COLLATE \"pg_catalog\".\"default\", \"can_learn\" varchar(500) COLLATE \"pg_catalog\".\"default\", \"update_time\" timestamp(6) NOT NULL, \"create_time\" timestamp(6) NOT NULL);COMMENT ON COLUMN \"public\".\"tb_imooc_course\".\"id\" IS '自增主键';COMMENT ON COLUMN \"public\".\"tb_imooc_course\".\"course_id\" IS '课程id';COMMENT ON COLUMN \"public\".\"tb_imooc_course\".\"name\" IS '课程名称';COMMENT ON COLUMN \"public\".\"tb_imooc_course\".\"difficult\" IS '难度级别';COMMENT ON COLUMN \"public\".\"tb_imooc_course\".\"student\" IS '学习人数';COMMENT ON COLUMN \"public\".\"tb_imooc_course\".\"desc\" IS '课程描述';COMMENT ON COLUMN \"public\".\"tb_imooc_course\".\"label\" IS '分类标签';COMMENT ON COLUMN \"public\".\"tb_imooc_course\".\"image_urls\" IS '封面图片';COMMENT ON COLUMN \"public\".\"tb_imooc_course\".\"detail\" IS '详情地址';COMMENT ON COLUMN \"public\".\"tb_imooc_course\".\"duration\" IS '课程时长';COMMENT ON COLUMN \"public\".\"tb_imooc_course\".\"overall_score\" IS '综合评分';COMMENT ON COLUMN \"public\".\"tb_imooc_course\".\"content_score\" IS '内容实用';COMMENT ON COLUMN \"public\".\"tb_imooc_course\".\"concise_score\" IS '简洁易懂';COMMENT ON COLUMN \"public\".\"tb_imooc_course\".\"logic_score\" IS '逻辑清晰';COMMENT ON COLUMN \"public\".\"tb_imooc_course\".\"summary\" IS '课程简介';COMMENT ON COLUMN \"public\".\"tb_imooc_course\".\"teacher_nickname\" IS '教师昵称';COMMENT ON COLUMN \"public\".\"tb_imooc_course\".\"teacher_avatar\" IS '教师头像';COMMENT ON COLUMN \"public\".\"tb_imooc_course\".\"teacher_job\" IS '教师职位';COMMENT ON COLUMN \"public\".\"tb_imooc_course\".\"tip\" IS '课程须知';COMMENT ON COLUMN \"public\".\"tb_imooc_course\".\"can_learn\" IS '能学什么';COMMENT ON COLUMN \"public\".\"tb_imooc_course\".\"update_time\" IS '更新时间';COMMENT ON COLUMN \"public\".\"tb_imooc_course\".\"create_time\" IS '入库时间';COMMENT ON TABLE \"public\".\"tb_imooc_course\" IS '免费课程表';-- ------------------------------ Indexes structure for table tb_imooc_course-- ----------------------------CREATE UNIQUE INDEX \"uni_cid\" ON \"public\".\"tb_imooc_course\" USING btree ( \"course_id\" \"pg_catalog\".\"int4_ops\" ASC NULLS LAST); -- ------------------------------ Table structure for tb_imooc_coding-- ----------------------------DROP TABLE IF EXISTS \"public\".\"tb_imooc_coding\";CREATE TABLE \"public\".\"tb_imooc_coding\" ( \"id\" serial4, \"coding_id\" int4 NOT NULL, \"name\" varchar(100) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"difficult\" varchar(30) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"student\" int4 NOT NULL, \"desc\" varchar(250) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"image_urls\" varchar(250) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"price\" varchar(50) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"detail\" varchar(250) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"overall_score\" varchar(20) COLLATE \"pg_catalog\".\"default\", \"teacher_nickname\" varchar(30) COLLATE \"pg_catalog\".\"default\", \"teacher_avatar\" varchar(250) COLLATE \"pg_catalog\".\"default\", \"duration\" varchar(50) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"video\" varchar(250) COLLATE \"pg_catalog\".\"default\", \"small_title\" varchar(250) COLLATE \"pg_catalog\".\"default\", \"detail_desc\" varchar(800) COLLATE \"pg_catalog\".\"default\", \"teacher_job\" varchar(50) COLLATE \"pg_catalog\".\"default\", \"suit_crowd\" varchar(500) COLLATE \"pg_catalog\".\"default\", \"skill_require\" varchar(500) COLLATE \"pg_catalog\".\"default\", \"update_time\" timestamp(6) NOT NULL, \"create_time\" timestamp(6) NOT NULL);COMMENT ON COLUMN \"public\".\"tb_imooc_coding\".\"id\" IS '自增主键';COMMENT ON COLUMN \"public\".\"tb_imooc_coding\".\"coding_id\" IS '课程id';COMMENT ON COLUMN \"public\".\"tb_imooc_coding\".\"name\" IS '课程名称';COMMENT ON COLUMN \"public\".\"tb_imooc_coding\".\"difficult\" IS '难度级别';COMMENT ON COLUMN \"public\".\"tb_imooc_coding\".\"student\" IS '学习人数';COMMENT ON COLUMN \"public\".\"tb_imooc_coding\".\"desc\" IS '课程描述';COMMENT ON COLUMN \"public\".\"tb_imooc_coding\".\"image_urls\" IS '封面图片';COMMENT ON COLUMN \"public\".\"tb_imooc_coding\".\"price\" IS '课程价格';COMMENT ON COLUMN \"public\".\"tb_imooc_coding\".\"detail\" IS '详情地址';COMMENT ON COLUMN \"public\".\"tb_imooc_coding\".\"overall_score\" IS '评价得分';COMMENT ON COLUMN \"public\".\"tb_imooc_coding\".\"teacher_nickname\" IS '教师昵称';COMMENT ON COLUMN \"public\".\"tb_imooc_coding\".\"teacher_avatar\" IS '教师头像';COMMENT ON COLUMN \"public\".\"tb_imooc_coding\".\"duration\" IS '课程时长';COMMENT ON COLUMN \"public\".\"tb_imooc_coding\".\"video\" IS '演示视频';COMMENT ON COLUMN \"public\".\"tb_imooc_coding\".\"small_title\" IS '详情标题';COMMENT ON COLUMN \"public\".\"tb_imooc_coding\".\"detail_desc\" IS '详情简介';COMMENT ON COLUMN \"public\".\"tb_imooc_coding\".\"teacher_job\" IS '教师职位';COMMENT ON COLUMN \"public\".\"tb_imooc_coding\".\"suit_crowd\" IS '适合人群';COMMENT ON COLUMN \"public\".\"tb_imooc_coding\".\"skill_require\" IS '技术要求';COMMENT ON COLUMN \"public\".\"tb_imooc_coding\".\"update_time\" IS '更新时间';COMMENT ON COLUMN \"public\".\"tb_imooc_coding\".\"create_time\" IS '入库时间';COMMENT ON TABLE \"public\".\"tb_imooc_coding\" IS '实战课程表';-- ------------------------------ Indexes structure for table tb_imooc_coding-- ----------------------------CREATE UNIQUE INDEX \"uni_coding_id\" ON \"public\".\"tb_imooc_coding\" USING btree ( \"coding_id\" \"pg_catalog\".\"int4_ops\" ASC NULLS LAST); 新建项目  创建项目:scrapy startproject imooc,接着在items.py中定义相应的Item: # -*- coding: utf-8 -*-# Define here the models for your scraped items## See documentation in:# https://doc.scrapy.org/en/latest/topics/items.htmlfrom scrapy.item import Item, Fieldfrom scrapy.loader.processors import TakeFirst# 免费课程class CourseItem(Item): # define the fields for your item here like: # name = scrapy.Field() name = Field() # 课程名称 difficult = Field() # 难度级别 student = Field() # 学习人数 desc = Field() # 课程描述 label = Field() # 分类标签 image_urls = Field() # 封面图片 detail = Field() # 详情地址 course_id = Field() # 课程id duration = Field() # 课程时长 overall_score = Field() # 综合评分 content_score = Field() # 内容实用 concise_score = Field() # 简洁易懂 logic_score = Field() # 逻辑清晰 summary = Field() # 课程简介 teacher_nickname = Field() # 教师昵称 teacher_avatar = Field() # 教师头像 teacher_job = Field() # 教师职位 tip = Field() # 课程须知 can_learn = Field() # 能学什么# 实战课程class CodingItem(Item): name = Field() # 课程名称 difficult = Field() # 难度级别 student = Field() # 学习人数 desc = Field() # 课程描述 image_urls = Field() # 封面图片 price = Field() # 课程价格 detail = Field() # 详情地址 coding_id = Field() # 课程id overall_score = Field() # 评价得分 teacher_nickname = Field() # 教师昵称 teacher_avatar = Field() # 教师头像 duration = Field() # 课程时长 video = Field() # 演示视频 small_title = Field() # 详情标题 detail_desc = Field() # 详情简介 teacher_job = Field() # 教师职位 suit_crowd = Field() # 适合人群 skill_require = Field() # 技术要求 “免费课程”爬虫编写  下面分析下慕课网免费课程页面的爬虫编写。简单分析下页面情况,以上定义的数据表(tb_imooc_course)信息,分别需要从列表页和课程详情页获取(如下图红框所示):   在项目的spiders目录下创建该爬虫:scrapy genspider course。关于页面数据解析这块强烈建议使用scrapy shell xxx进行调试,以下是这部分内容的代码: # -*- coding: utf-8 -*-import refrom urllib import parseimport scrapyfrom scrapy.http import Requestfrom imooc.items import *class CourseSpider(scrapy.Spider): name = 'course' allowed_domains = ['www.imooc.com'] start_urls = ['https://www.imooc.com/course/list/?page=0'] https = \"https:\" def parse(self, response): \"\"\"抓取课程列表页面\"\"\" url = response.url self.logger.info(\"Response url is %s\" % url) # 根据Scrapy默认的后入先出(LIFO)深度爬取策略,这里应先提交下一页请求 next_btn = response.xpath('//a[contains(.//text(),\"下一页\")]/@href').extract_first() if next_btn: # 存在下一页按钮,爬取下一页 next_page = parse.urljoin(url, next_btn) yield Request(next_page, callback=self.parse) course_list = response.xpath('//div[@class=\"course-card-container\"]') for index, course in enumerate(course_list): course_item = CourseItem() # 课程名称 course_item['name'] = course.xpath('.//h3[@class=\"course-card-name\"]/text()').extract_first() # 课程难度 course_item['difficult'] = course.xpath( './/div[@class=\"course-card-info\"]/span[1]/text()').extract_first() # 学习人次 course_student = course.xpath('.//div[@class=\"course-card-info\"]/span[2]/text()').extract_first() course_item['student'] = int(course_student) # 课程描述 course_item['desc'] = course.xpath('.//p[@class=\"course-card-desc\"]/text()').extract_first() # 课程分类 course_label = course.xpath('.//div[@class=\"course-label\"]/label/text()').extract() course_item['label'] = ', '.join(course_label) # 课程封面 course_banner = course.xpath('.//div[@class=\"course-card-top\"]/img/@src').extract_first() course_item['image_urls'] = [\"{0}{1}\".format(CourseSpider.https, course_banner)] # 详情地址 course_detail = course.xpath('.//a/@href').extract_first() course_item['detail'] = parse.urljoin(url, course_detail) # 课程id course_id = re.split('/', course_detail)[-1] course_item['course_id'] = int(course_id) self.log(\"Item: %s\" % course_item) # 爬取详情页 yield Request(course_item['detail'], callback=self.parse_detail, meta={'course_item': course_item}) def parse_detail(self, response): \"\"\" 抓取课程详情页面 \"\"\" url = response.url self.logger.info(\"Response url is %s\" % url) course_item = response.meta['course_item'] meta_value = response.xpath('//span[@class=\"meta-value\"]/text()').extract() # 课程时长 course_item['duration'] = meta_value[1].strip() # 综合得分 course_item['overall_score'] = meta_value[2] # 内容实用 course_item['content_score'] = meta_value[3] # 简洁易懂 course_item['concise_score'] = meta_value[4] # 逻辑清晰 course_item['logic_score'] = meta_value[5] # 课程简介 course_item['summary'] = response.xpath('//div[@class=\"course-description course-wrap\"]/text()') \\ .extract_first().strip() # 教师昵称 course_item['teacher_nickname'] = response.xpath('//span[@class=\"tit\"]/a/text()').extract_first() # 教师头像 avatar = response.xpath('//img[@class=\"js-usercard-dialog\"]/@src').extract_first() if avatar: course_item['teacher_avatar'] = \"{0}{1}\".format(CourseSpider.https, avatar) # 教师职位 course_item['teacher_job'] = response.xpath('//span[@class=\"job\"]/text()').extract_first() # 课程须知 course_item['tip'] = response.xpath('//dl[@class=\"first\"]/dd/text()').extract_first() # 能学什么 course_item['can_learn'] = response.xpath( '//div[@class=\"course-info-tip\"]/dl[not(@class)]/dd/text()').extract_first() yield course_item   数据的入库在pipelines.py中,后面跟”实战课程”爬虫再一起介绍。 “实战课程”爬虫编写  继续介绍慕课网实战课程页面的爬虫编写,同样简单分析下页面情况,实战课程定义的数据表(tb_imooc_coding)信息,同样需要从列表页和课程详情页获取(如下图红框所示):   同样在spiders目录下创建该爬虫:scrapy genspider coding。以下是这部分内容的代码: # -*- coding: utf-8 -*-import refrom urllib import parseimport scrapyfrom scrapy.http import Requestfrom imooc.items import *class CodingSpider(scrapy.Spider): name = 'coding' allowed_domains = ['coding.imooc.com'] start_urls = ['https://coding.imooc.com/?page=0'] https = \"https:\" def parse(self, response): \"\"\"抓取课程列表页面\"\"\" url = response.url self.logger.info(\"Response url is %s\" % url) next_btn = response.xpath('//a[contains(.//text(),\"下一页\")]/@href').extract_first() if next_btn: next_page = parse.urljoin(url, next_btn) yield Request(next_page, callback=self.parse) coding_list = response.xpath('//div[@class=\"shizhan-course-wrap l \"]') for index, coding in enumerate(coding_list): coding_item = CodingItem() # 课程名称 coding_item['name'] = coding.xpath('.//p[@class=\"shizan-name\"]/text()').extract_first() # 课程难度 coding_item['difficult'] = coding.xpath('.//span[@class=\"grade\"]/text()').extract_first() # 学习人次 coding_student = coding.xpath('.//div[@class=\"shizhan-info\"]/span[2]/text()').extract_first() coding_item['student'] = int(coding_student) # 课程描述 coding_item['desc'] = coding.xpath('.//p[@class=\"shizan-desc\"]/text()').extract_first() # 课程封面 coding_banner = coding.xpath('.//img[@class=\"shizhan-course-img\"]/@src').extract_first() coding_item['image_urls'] = [\"{0}{1}\".format(CodingSpider.https, coding_banner)] # 课程价格 coding_item['price'] = coding.xpath('.//div[@class=\"course-card-price\"]/text()').extract_first() # 详情地址 coding_detail = coding.xpath('.//a/@href').extract_first() coding_item['detail'] = parse.urljoin(url, coding_detail) # 课程id coding_id = re.split('/', coding_detail)[-1].replace('.html', '') coding_item['coding_id'] = int(coding_id) # 评价得分 coding_item['overall_score'] = coding.xpath('.//span[@class=\"r\"]/text()').extract_first().replace('评价:', '') # 教师昵称 coding_item['teacher_nickname'] = coding.xpath('.//div[@class=\"lecturer-info\"]/span/text()').extract_first() # 教师头像 avatar = coding.xpath('.//div[@class=\"lecturer-info\"]/img/@src').extract_first() coding_item['teacher_avatar'] = \"{0}{1}\".format(CodingSpider.https, avatar) self.log(\"Item: %s\" % coding_item) # 爬取详情页 yield Request(coding_item['detail'], callback=self.parse_detail, meta={'coding_item': coding_item}) def parse_detail(self, response): \"\"\" 抓取课程详情页面 \"\"\" url = response.url self.logger.info(\"Response url is %s\" % url) coding_item = response.meta['coding_item'] # 课程时长 coding_item['duration'] = response.xpath( '//div[@class=\"static-item static-time\"]/span/strong/text()').extract_first() # 演示视频 video = response.xpath('//div[@id=\"js-video-content\"]/@data-vurl').extract_first() coding_item['video'] = parse.urljoin(CodingSpider.https, video) # 详情标题 coding_item['small_title'] = response.xpath('//div[@class=\"title-box \"]/h2/text()').extract_first() # 详情简介 coding_item['detail_desc'] = response.xpath('//div[@class=\"info-desc\"]/text()').extract_first() # 教师职位 coding_item['teacher_job'] = response.xpath('//div[@class=\"teacher\"]/p/text()').extract_first() yield coding_item 数据入库  项目中有用到redis,用来简单判断下数据应该是入库保存还是更新,用mongo这种有版本号的文档数据库可能会更好一些: # -*- coding: utf-8 -*-# Define your item pipelines here## Don't forget to add your pipeline to the ITEM_PIPELINES setting# See: https://doc.scrapy.org/en/latest/topics/item-pipeline.htmlfrom utils import rdsfrom utils import pgsfrom imooc import itemsfrom datetime import datetimeclass ImoocPipeline(object): def __init__(self): # PostgreSQL和Redis连接应自行更改 # PostgreSQL host = 'localhost' port = 12432 db_name = 'scrapy' username = db_name password = db_name self.postgres = pgs.Pgs(host=host, port=port, db_name=db_name, user=username, password=password) # Redis self.redis = rds.Rds(host=host, port=12379, db=1, password='redis6379').redis_cli def process_item(self, item, spider): name = item['name'] difficult = item['difficult'] student = item['student'] desc = item['desc'] image_urls = item['image_urls'][0] detail = item['detail'] duration = item['duration'] overall_score = item['overall_score'] teacher_nickname = item['teacher_nickname'] teacher_avatar = item.get('teacher_avatar') teacher_job = item['teacher_job'] now = datetime.now() str_now = now.strftime('%Y-%m-%d %H:%M:%S') if isinstance(item, items.CourseItem): # 免费课程 course_id = item['course_id'] label = item['label'] overall_score = float(overall_score) content_score = float(item['content_score']) concise_score = float(item['concise_score']) logic_score = float(item['logic_score']) summary = item['summary'] tip = item['tip'] can_learn = item['can_learn'] key = 'imooc:course:{0}'.format(course_id) if self.redis.exists(key): params = (student, overall_score, content_score, concise_score, logic_score, now, course_id) self.postgres.handler(update_course(), params) else: params = (course_id, name, difficult, student, desc, label, image_urls, detail, duration, overall_score, content_score, concise_score, logic_score, summary, teacher_nickname, teacher_avatar, teacher_job, tip, can_learn, now, now) effect_count = self.postgres.handler(add_course(), params) if effect_count > 0: self.redis.set(key, str_now) if isinstance(item, items.CodingItem): # 实战课程 price = item['price'] coding_id = item['coding_id'] video = item['video'] small_title = item['small_title'] detail_desc = item['detail_desc'] key = 'imooc:coding:{0}'.format(coding_id) if self.redis.exists(key): params = (student, price, overall_score, now, coding_id) self.postgres.handler(update_coding(), params) else: params = (coding_id, name, difficult, student, desc, image_urls, price, detail, overall_score, teacher_nickname, teacher_avatar, duration, video, small_title, detail_desc, teacher_job, now, now) effect_count = self.postgres.handler(add_coding(), params) if effect_count > 0: self.redis.set(key, str_now) return item def close_spider(self, spider): self.postgres.close()def add_course(): sql = 'insert into tb_imooc_course(course_id,\"name\",difficult,student,\"desc\",label,image_urls,' \\ 'detail,duration,overall_score,content_score,concise_score,logic_score,summary,' \\ 'teacher_nickname,teacher_avatar,teacher_job,tip,can_learn,update_time,create_time) ' \\ 'values(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)' return sqldef update_course(): sql = 'update tb_imooc_course set student=%s,overall_score=%s,content_score=%s,concise_score=%s,' \\ 'logic_score=%s,update_time=%s where course_id = %s' return sqldef add_coding(): sql = 'insert into tb_imooc_coding(coding_id,\"name\",difficult,student,\"desc\",image_urls,price,detail,' \\ 'overall_score,teacher_nickname,teacher_avatar,duration,video,small_title,detail_desc,teacher_job,' \\ 'update_time,create_time) values(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)' return sqldef update_coding(): sql = 'update tb_imooc_coding set student=%s,price=%s,overall_score=%s,update_time=%s where coding_id = %s' return sql   最后,别忘了要在settings.py中手动开启pipelines配置: 运行爬虫  启动上述Scrapy爬虫,可分别使用命令scrapy crawl course和scrapy crawl coding运行,如果不想每次都要输入这么麻烦, 可以Scrapy提供的API将启动命令编码到py中,再用python命令运行该脚本即可,具体可参考如下: from scrapy.cmdline import execute# 免费课程execute(['scrapy', 'crawl', 'course'])# 实战课程execute('scrapy crawl coding'.split(' ')) 数据展示  实际运行中并没有看到有相关的反爬虫限制,可能是因为整体数据量不大(免费课程有900多,实战课程有100多门),借助Scrapy的多线程能力(setting.py中的CONCURRENT_REQUESTS配置,默认是16)很快也就抓取完了: 实例代码 - GitHub","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.mariojd.cn/categories/Python/"}],"tags":[{"name":"scrapy","slug":"scrapy","permalink":"https://blog.mariojd.cn/tags/scrapy/"},{"name":"imooc","slug":"imooc","permalink":"https://blog.mariojd.cn/tags/imooc/"}]},{"title":"爬虫进阶:Scrapy 入门","slug":"爬虫进阶:Scrapy 入门","date":"2018-08-31","updated":"2019-06-02","comments":true,"path":"getting-started-with-scrapy.html","link":"","permalink":"https://blog.mariojd.cn/getting-started-with-scrapy.html","excerpt":"","keywords":"","text":"进阶前言  学Py和写爬虫都有很长一段时间了,虽然工作方面主要还是做Java开发,但事实上用python写东西真的很爽。之前都是用Requests+BeautifulSoup这样的第三方库爬一些简单的网站,好处简单上手快,坏处也明显,单线程速度慢,偶尔想要跑快点还得自己写多线程或者多进程。其实早已久仰Scrpay大名,无奈一直没有主动去接触,前不久买了一本相关的书籍,看完之后便陆陆续续试手了几个实战项目(后续介绍),现在应该算是半梦半醒迈入半个大门了。其实Java也有好几个不错的爬虫框架,那为什么不选择Java?呵呵,人生苦短,用Python没错,何况它现在这么火。   大多数学习是没有捷径的,如果你也想学Scrapy,根据个人经验,可以先买一两本相关书籍翻翻,然后写写小项目,接着再继续往深入学习,网上有很多不错的关于Scrapy的电子书,文末会推荐一波自己瞎逛已收藏的,可以根据自己的实际情况进行帅选和甄别。 目录结构  万事开头难,安装好Scrapy环境后输入命令scrapy startproject start,这样就创建好了第一个scrapy项目,目录结构如下: - start - start - spiders # 爬虫编写及存放的目录 - __init__.py - __init__.py - items.py # 定义爬虫数据结构的类 - middlewares.py # 定义一些中间件的类,包括代理、请求头这些 - pipelines.py # 数据流出的管道类,将爬取数据保存入库等 - settings.py # 配置相关类,包括像日志、middlewares和pipelines等 - scrapy.cfg # 主要用于将爬虫部署到第三方,一般可不理会   项目框架已经搭起来了,紧接着示例下如何第一个爬虫,可以自己在spiders目录下手动创建爬虫类,也可以用scrapy提供的快捷命令scrapy genspider {spider-name} {target-website}快速生成指定名称的目标站点爬虫(参考如下)。例如spider-name可以定义为example,target-website指定为example.com。 # -*- coding: utf-8 -*-import scrapyclass ExampleSpider(scrapy.Spider): name = 'example' # 爬虫名称,运行的时候需指定 allowed_domains = ['example.com'] # 允许爬取的域名 start_urls = ['http://example.com/'] # 第一个爬取的目标网址 def parse(self, response): """scrapy爬取完首个目标网页后会回调到这个方法""" pass   更多优秀和详细的Scrapy入门知识应该从书中或者其它学习资源获取,最后再附上Scrapy学习必备的经典架构图: 常见命令  这里记录和列举一些常用的scrapy命令及其作用: 命令 作用 可选参数 scrapy startproject {project-name} 创建scrapy项目 scrapy genspider {spider-name} {target-domain} 创建目标站点指定名称爬虫 scrapy shell {url} 调试抓取的指定网页 -s USER_AGENT=’xxx’,加上指定请求头 scrapy crawl {spider-name} 运行指定爬虫 -o output.{json or xml or cvs},将抓取结果输出为指定格式文件保存; -s {CLOSESPIDER_PAGECOUNT or CLOSESPIDER_ITEMCOUNT}=n,抓取指定数量网页或ITEM后自动停止爬虫 scrapy check {spider-name} 检测爬虫是否存在错误 学习资源 scrapy-cookbook 网络爬虫教程 Python3网络爬虫开发实战 (PS:有纸质书,在线只能看一部分) 参考链接 scrapy命令行工具Scrapy DocumentationScrapy 中文文档 下一篇开始实战介绍。示例代码 - GitHub","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.mariojd.cn/categories/Python/"}],"tags":[{"name":"scrapy","slug":"scrapy","permalink":"https://blog.mariojd.cn/tags/scrapy/"},{"name":"spider","slug":"spider","permalink":"https://blog.mariojd.cn/tags/spider/"}]},{"title":"Spring Boot2 集成 Elasticsearch、PostgreSQL 遇到的问题","slug":"Spring Boot2 集成 Elasticsearch、PostgreSQL遇到的问题","date":"2018-08-29","updated":"2019-06-02","comments":true,"path":"problems-in-spring-boot2-with-elasticsearch-and-postgresql.html","link":"","permalink":"https://blog.mariojd.cn/problems-in-spring-boot2-with-elasticsearch-and-postgresql.html","excerpt":"","keywords":"","text":"项目背景  在描述和还原事故之前,简单说明下相关环境: spring boot v2.0.4.RELEASE spring-boot-starter-data-elasticsearch (以前做项目的时候,Spring Data ES跟ES服务存在版本匹配关系,但目前在spring boot v2.0.4.RELEASE中使用未发现有版本不兼容情况) spring-boot-starter-data-jpa (用于操作PostgreSQL) PostgreSQL启动连接报错  启动项目的时候出现错误,具体异常信息如下: 2018-08-29 21:33:18,397 INFO org.hibernate.dialect.Dialect[157]: HHH000400: Using dialect: org.hibernate.dialect.PostgreSQL95Dialect2018-08-29 21:33:21,479 INFO o.h.e.j.e.i.LobCreatorBuilderImpl[124]: HHH000424: Disabling contextual LOB creation as createClob() method threw error : java.lang.reflect.InvocationTargetExceptionjava.lang.reflect.InvocationTargetException: null at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.__invoke(DelegatingMethodAccessorImpl.java:43) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:45009) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:45012) at java.lang.reflect.Method.invoke(Method.java:497) at org.hibernate.engine.jdbc.env.internal.LobCreatorBuilderImpl.useContextualLobCreation(LobCreatorBuilderImpl.java:113) at org.hibernate.engine.jdbc.env.internal.LobCreatorBuilderImpl.makeLobCreatorBuilder(LobCreatorBuilderImpl.java:54) at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentImpl.<init>(JdbcEnvironmentImpl.java:271) at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:114) at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:35) at org.hibernate.boot.registry.internal.StandardServiceRegistryImpl.initiateService(StandardServiceRegistryImpl.java:88) at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:259) at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:233) at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:210) at org.hibernate.engine.jdbc.internal.JdbcServicesImpl.configure(JdbcServicesImpl.java:51) at org.hibernate.boot.registry.internal.StandardServiceRegistryImpl.configureService(StandardServiceRegistryImpl.java:94) at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:242) at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:210) at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.handleTypes(MetadataBuildingProcess.java:352) at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:111) at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:861) at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:888) at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.__createEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:57) at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:40002) at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.__createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:365) at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:42002) at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:390) at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:377) at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:341) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1758) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1695) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:573) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:495) at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:317) at org.springframework.beans.factory.support.AbstractBeanFactory$$Lambda$109/1509220174.getObject(Unknown Source) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:315) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1089) at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:859) at org.springframework.context.support.AbstractApplicationContext.__refresh(AbstractApplicationContext.java:550) at org.springframework.context.support.AbstractApplicationContext.jrLockAndRefresh(AbstractApplicationContext.java:40002) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:41008) at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:140) at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:762) at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:398) at org.springframework.boot.SpringApplication.run(SpringApplication.java:330) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1258) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1246) at cn.mariojd.nearjob.NearjobApplication.main(NearjobApplication.java:19)Caused by: java.sql.SQLFeatureNotSupportedException: 这个 org.postgresql.jdbc.PgConnection.createClob() 方法尚未被实作。 at org.postgresql.Driver.notImplemented(Driver.java:688) at org.postgresql.jdbc.PgConnection.createClob(PgConnection.java:1269) ... 51 common frames omitted   这个错误确实有点奇怪,不过好在Github上已经有相关Issue,有兴趣的可以去看看,该问题的解决方法是添加配置项:spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation: true JPA实体继承映射数据表  当多个实体间有多个属性相同时,可以考虑抽取抽象实体类的方式复用属性定义,并在抽象父类上使用@MappedSuperclass注解(注意此父类不能再标注@Entity或@Table注解): BaseEntity @Data@MappedSuperclasspublic abstract class BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private Integer gender; @Column(name = "username") private String name;} Student @Entity@Table@Datapublic class Student extends BaseEntity { private Integer score;} Teacher @Entity@Table@Datapublic class Teacher extends BaseEntity { private String phone;}   此外,JPA中还有不同的遗传策略来解决多实体间的继承映射关系,同样可以实现上述一样的效果(@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)): Spirng Data抽取抽象Repository  这种情况跟上面那种情况有一定的关联,我们借助泛型来解决,首先建立一个BaseDao(需指定为@NoRepositoryBean): @NoRepositoryBeanpublic interface BaseDao<T extends BaseEntity> extends JpaRepository<T, Integer> { // 部分字段查询需添加相对应构造器 @Query(value = "SELECT new #{#entityName}(id,username) FROM #{#entityName} WHERE username=?1") T findByUsername(String username); T findByIdAndUsername(int id,String username);}   这样多个子Dao只需要继承这个BaseDao便可以拥有这些扩展方法。 Reactive Web集成ES启动冲突  该问题出现在使用webflux集成elasticsearch启动项目的时候,异常信息打印如下: 2018-08-30 08:43:20.286 INFO 16636 --- [ main] o.elasticsearch.plugins.PluginsService : loaded plugin [org.elasticsearch.transport.Netty4Plugin]2018-08-30 08:43:22.999 WARN 16636 --- [ main] onfigReactiveWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'elasticsearchClient' defined in class path resource [org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.elasticsearch.client.transport.TransportClient]: Factory method 'elasticsearchClient' threw exception; nested exception is java.lang.IllegalStateException: availableProcessors is already set to [4], rejecting [4]   具体解决方案可参考如下: @SpringBootApplicationpublic class DemoApplication { public static void main(String[] args) { // 添加该配置项即可 System.setProperty("es.set.netty.runtime.available.processors", "false"); SpringApplication.run(DemoApplication.class, args); } } Spring Data Elasticsearch与ES mapping字段不一致  如果没有主动创建mapping,Spring Data ES默认会在第一次添加数据的时候创建,对应mapping的字段名跟实体属性保持一致。如果原本已经创建好mapping,或是想自定义mapping字段跟实体属性的对照关系,这里有两种解决方案: 方案1   借助@JsonProperty更改ES字段与实体属性的映射关系 @Data@Document(indexName = "school", type = "primary_school")public class Student { @Id private String id; private Integer gender; @JsonProperty("student_name") private String studentName;} 方案2   使用@JsonNaming注解并指定相应的映射策略。如果当前实体需要使用多个@JsonProperty才能定义这种关系,可以考虑使用这种更快捷的方式 @Data@Document(indexName = "school", type = "primary_school")@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)public class Student { @Id private String id; private Integer gender; private String studentName;}   从上图可以看到jackson包中已经定义好有五种不同的映射策略,如果都不满足实际需求的话还可以自行扩展,只需要继承PropertyNamingStrategyBase这个抽象类并重写它的translate()方法即可。 ES一个Index对应多个type问题  如果出现下面这个错误信息,说明定义了多个Type对应在一个Index。实际上在ES6.0之后,官方已经不推荐这种映射关系。按以前那种思路,Index对应Database,然后type对应table的关系(所以一个database中有多个table,那么一个index也就可以有多个type)是不严谨的,在官网上有这个Reference。 Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.data.elasticsearch.repository.support.SimpleElasticsearchRepository]: Constructor threw exception; nested exception is java.lang.IllegalArgumentException: Rejecting mapping update to [school] as the final mapping would have more than 1 type: [teacher, student] 参考链接 Spring-data自定义Repositoryelasticsearch常见的问题JPA实体继承实体的映射策略SpringData ES 关于字段名和索引中的列名字不一致导致的查询问题","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"postgresql","slug":"postgresql","permalink":"https://blog.mariojd.cn/tags/postgresql/"},{"name":"elasticsearch","slug":"elasticsearch","permalink":"https://blog.mariojd.cn/tags/elasticsearch/"},{"name":"spring boot2","slug":"spring-boot2","permalink":"https://blog.mariojd.cn/tags/spring-boot2/"}]},{"title":"Elasticsearch 6.3.2 版本踩填坑指南","slug":"Elasticsearch 6.3.2 版本踩填坑指南","date":"2018-08-28","updated":"2019-06-02","comments":true,"path":"elasticsearch6.3.2-in-practice.html","link":"","permalink":"https://blog.mariojd.cn/elasticsearch6.3.2-in-practice.html","excerpt":"","keywords":"","text":"前言  前端时间利用ES开发一个”附近地理位置+其它信息”查询搜索的功能(据了解,Redis和PostgreSQL也能实现同样的功能),实践中遇到了不少的问题,所以通过这篇文章记录下踩填坑过程。 es with docker  个人喜好,一般使用中间件都喜欢用Docker运行较新版本的,用docker pull elasticsearch命令拉下来的版本一般不会是最新的,所以可以从这里找到最新版本的拉取命令,稍加改造后我需要的是这样的:docker run --name elasticsearch -e "ES_JAVA_OPTS=-Xms256m -Xmx256m" -d -p 9200:9200 -p 9300:9300 docker.elastic.co/elasticsearch/elasticsearch-oss:6.3.2   注意到这里指定的镜像是elasticsearch-oss:6.3.2,这个-oss表示不包括X-Pack的ES镜像,这也是在6.0+版本后划分的,剩下两种类型是basic(默认)和platinum,具体官方说明可以看下图。   如果启动失败,使用命令docker logs elasticsearch查看日志即可,-f参数用于监听,其中一种启动错误是要求你修改vm.max_map_count这个系统环境参数,Linux可参考命令sysctl -w vm.max_map_count=262144设置即可(其它系统在文末官方参考链接中有更详细介绍)。 create mapping  这部分是重点,之前遇到的坑就是type mapping这块。 “_id is not configurable”   es2.0+版本中,_id是可以配置的,网上也有一堆告诉你怎么设置,但es6.3.2中创建mapping并指定_id配置的时候,es返回错误中就出现了上面那句,在社区可以找到了这个Discuss。高版本中的_id是不能配置了,一般来说,在添加Document的时候,如果只指定Index和Document Type,那么es会随机给这个_id分配一个值,但如果添加的时候指定这个_id值,那么ES就不会再随机分配这个值。不过注意,即使你指定的_id是一个数值,但在实际保存和返回中都是字符串类型; “Mapping with Index not_analyzed is not working”   这个问题在Github上也有相关的Issues,我在这里先还原下当初创建mapping的配置: { "properties": { # 当初的想法是指定shopId这个字段不要分析,然后keyword字段要进行分析 "shopId": { "type": "integer", "index": "not_analyzed" }, "location": { "type": "geo_point" }, "keyword":{ "type": "text", "index": "analyzed", "analyzer": "ik_max_word" } }}   创建完上述mapping后访问http://localhost:9200/{index}/{type}/_mapping,但返回结果(如下)和上面指定的mapping并不相同: { "{index}": { "mappings": { "{type}": { "properties": { "shopId": { "type": "integer" }, "location": { "type": "geo_point" }, "keyword": { "type": "text", "analyzer": "ik_max_word" }, } } } }}   之后还是在官方文档中找到答案,发现确实有两点值得记录一下。 第一点:es5.0之后,为字符串新增了keyword类型,而之前的版本中只有text类型,通过index属性判断是否需要分词(默认分词)。es5.0之后使用keyword type代替index这个属性,所以指定"type": "text"就是分词,指定"type": "keyword"就是不分词; 第二点:不需要为type为数字类型integer、long,日期类型date、布尔类型boolean等指定"index": "not_analyzed"属性(而且在高版本es中这也是错误的语法,index只能指定为"index": true | false,false意味着不可查询),因为这些类型就是不分词的,如果要分词请修改为text类型。 ik analysis  IK是国内用得比较多的中文分词器,与ES安装集成也比较简单,首先进入dockerdocker exec -it elasticsearch bash,然后用命令./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.3.2/elasticsearch-analysis-ik-6.3.2.zip安装即可(需对应es版本),安装完使用docker restart elasticsearch重启服务即可。IK支持两种分词方式,ik_smart和ik_max_word,前者分词粒度没有后者细,可以针对实际情况进行选择。 head plugin  elasticsearch-head插件也是测试的时候用得比较多的插件,以前用ES2的时候是借助plugin脚本安装的,但这种方式在ES5.0之后被废弃了,然后作者也推荐了好几种方式,可以借助npm运行该服务,或者用docker运行服务,不过为了简单起见我最后选的是Chrome extension这种方式。 参考链接 Install Elasticsearch with DockerMappingText datatypeKeyword datatypehow-to-not-analyze-in-elasticsearchelasticsearch-analysis-ikelasticsearch-head","categories":[{"name":"中间件","slug":"中间件","permalink":"https://blog.mariojd.cn/categories/中间件/"}],"tags":[{"name":"elasticsearch","slug":"elasticsearch","permalink":"https://blog.mariojd.cn/tags/elasticsearch/"},{"name":"ik analysis","slug":"ik-analysis","permalink":"https://blog.mariojd.cn/tags/ik-analysis/"},{"name":"head plugin","slug":"head-plugin","permalink":"https://blog.mariojd.cn/tags/head-plugin/"}]},{"title":"Fiddler 抓包升级,安装 HTTPS 证书","slug":"Fiddler 抓包升级,安装 HTTPS 证书","date":"2018-08-27","updated":"2019-06-02","comments":true,"path":"install-https-certificate-for-fiddler-capture.html","link":"","permalink":"https://blog.mariojd.cn/install-https-certificate-for-fiddler-capture.html","excerpt":"","keywords":"","text":"前言  之前借助过Fiddler来抓取微信公众号的文章和留言(Https协议的,下一篇介绍),所以安装https证书又是必不可少的,这里简单还原下安装步骤。如果没有正确安装证书,Fiddler抓包的时候会提示:HTTPS traffic decryption error: System.Security.Authentication.AuthenticationException 安装步骤 安装CertMaker插件:   由于默认的Fiddler证书是不符合抓包要求的,这里通过Fiddler插件扩展下载安装CertMaker插件解决该问题,安装完之后重启Fiddler即可; 配置Fiddler选型:   从左上角选择栏依次选择”Tools” -> “Options”, 参考下面几张图配置好即可: 手机端安装证书   Https认证是双向的,因此手机端也必需要安装这个证书,获取该证书有以下两种方法参考: 方法一   在手机浏览器输入http://ip:port (表示代理ip和端口,不太懂的可以找找之前写的文章), 点击最下边的”FiddlerRoot certificate”,先下载到SD卡根目录; 方法二   从左上角选择栏依次选择”Tools” -> “Options”-> “Https”, 找到”Actions”里面的”Export Root Certificert to Desktop”(参考步骤2图2), 下载到桌面再传到手机根目录即可;   下面是在手机端按照该证书操作:   从手机”设置”中找到类似”安装证书”这一选项(不同手机不太一样,这里是在”安全与隐私”里面),点击后选择上面下载好的证书,接着进行安装即可。 参考说明 Fiddler抓包不支持Http2、TCP、UDP、WebSocket等协议; Fiddler抓包类似于中间代理,可以理解成需要同时欺骗客户端&&服务器端。如果当前待抓包APP的Https证书是跟代码一起打包的,这时候再用上述方式配置是无效的; WireShark抓包更专业更强大。 参考链接 fiddler 手机 https 抓包","categories":[],"tags":[{"name":"fiddler","slug":"fiddler","permalink":"https://blog.mariojd.cn/tags/fiddler/"},{"name":"https抓包","slug":"https抓包","permalink":"https://blog.mariojd.cn/tags/https抓包/"}]},{"title":"Python 爬取微信公众号文章和评论 (基于 Fiddler 抓包分析)","slug":"Python 爬取微信公众号文章和评论 (基于 Fiddler 抓包分析)","date":"2018-08-27","updated":"2019-06-02","comments":true,"path":"crawl-wechat-articles-and-comments-with-fiddler-and-python.html","link":"","permalink":"https://blog.mariojd.cn/crawl-wechat-articles-and-comments-with-fiddler-and-python.html","excerpt":"","keywords":"","text":"背景说明  感觉微信公众号算得是比较难爬的平台之一,不过一番折腾之后还是小有收获的。没有用Scrapy(估计爬太快也有反爬限制),但后面会开始整理写一些实战出来。简单介绍下本次的开发环境: python3 requests psycopg2 (操作postgres数据库) 抓包分析  前一篇文章介绍过抓包前要做的准备,这里不再做相关说明。本次实战对抓取的公众号没有限制,但不同公众号每次抓取之前都要进行分析。打开Fiddler,将手机配置好相关代理,为避免干扰过多,这里给Fiddler加个过滤规则,只需要指定微信域名mp.weixin.qq.com就好:   平时关注的公众号也比较多,本次实战以“36氪”公众号为例,继续往下看:   在公众号主页,右上角有三个实心圆点,点击进入消息界面,下滑找到并点击“全部消息”,往下请求加载几次历史文章,然后回到Fiddler界面,不出意外的话应该可以看到这几次请求,可以看到返回的数据是json格式的,同时文章数据是以json字符串的形式定义在general_msg_list字段中: 分析文章列表接口  把请求URL和Cookie贴上来进行分析: https://mp.weixin.qq.com/mp/profile_ext?action=getmsg&__biz=MzI2NDk5NzA0Mw==&f=json&offset=10&count=10&is_ok=1&scene=126&uin=777&key=777&pass_ticket=QhOypNwH5dAr5w6UgMjyBrTSOdMEUT86vWc73GANoziWFl8xJd1hIMbMZ82KgCpN&wxtoken=&appmsg_token=971_LwY7Z%252BFBoaEv5z8k_dFWfJkdySbNkMR4OmFxNw~~&x5=1&f=json Cookie: pgv_pvid=2027337976; pgv_info=ssid=s3015512850; rewardsn=; wxtokenkey=777; wxuin=2089823341; devicetype=android-26; version=26070237; lang=zh_CN;pass_ticket=NDndxxaZ7p6Z9PYulWpLqMbI0i3ULFeCPIHBFu1sf5pX2IhkGfyxZ6b9JieSYRUy;wap_sid2=CO3YwOQHEogBQnN4VTNhNmxQWmc3UHI2U3kteWhUeVExZHFVMnN0QXlsbzVJRUJKc1pkdVFUU2Y5UzhSVEtOZmt1VVlYTkR4SEllQ2huejlTTThJWndMQzZfYUw2SldLVGVMQUthUjc3QWdVMUdoaGN0Nml2SU05cXR1dTN2RkhRUVd1V2Y3SFJ5d01BQUF+fjCB1pLcBTgNQJVO   下面把重要的参数说明一下,没提到的说明就不那么重要了: __biz:相当于是当前公众号的id(唯一固定标志) offset:文章数据接口请求偏移量标志(从0开始),每次返回的json数据中会有下一次请求的offset,注意这里并不是按某些规则递增的 count:每次请求的数据量(亲测最多可以是10) pass_ticket:可以理解是请求票据,而且隔一段时间后(大概几个小时)就会过期,这也是为什么微信公众号比较难按固定规则进行抓取的原因 appmsg_token:同样理解为非固定有过期策略的票据 Cookie:使用的时候可以把整段贴上去,但最少仅需要wap_sid2这部分   是不是感觉有点麻烦,毕竟不是要搞大规模专业的爬虫,所以单就一个公众号这么分析下来,还是可以往下继续的,贴上截取的一段json数据,用于设计文章数据表: { \"ret\": 0, \"errmsg\": \"ok\", \"msg_count\": 10, \"can_msg_continue\": 1, \"general_msg_list\": \"{\\\"list\\\":[{\\\"comm_msg_info\\\":{\\\"id\\\":1000005700,\\\"type\\\":49,\\\"datetime\\\":1535100943,\\\"fakeid\\\":\\\"3264997043\\\",\\\"status\\\":2,\\\"content\\\":\\\"\\\"},\\\"app_msg_ext_info\\\":{\\\"title\\\":\\\"金融危机又十年:钱荒之下,二手基金迎来高光时刻\\\",\\\"digest\\\":\\\"退出永远是基金的主旋律。\\\",\\\"content\\\":\\\"\\\",\\\"fileid\\\":100034824,\\\"content_url\\\":\\\"http:\\\\/\\\\/mp.weixin.qq.com\\\\/s?__biz=MzI2NDk5NzA0Mw==&mid=2247518479&idx=1&sn=124ab52f7478c1069a6b4592cdf3c5f5&chksm=eaa6d8d3ddd151c5bb95a7ae118de6d080023246aa0a419e1d53bfe48a8d9a77e52b752d9b80&scene=27#wechat_redirect\\\",\\\"source_url\\\":\\\"\\\",\\\"cover\\\":\\\"http:\\\\/\\\\/mmbiz.qpic.cn\\\\/mmbiz_jpg\\\\/QicyPhNHD5vYgdpprkibtnWCAN7l4ZaqibKvopNyCWWLQAwX7QpzWicnQSVfcBZmPrR5YuHS45JIUzVjb0dZTiaLPyA\\\\/0?wx_fmt=jpeg\\\",\\\"subtype\\\":9,\\\"is_multi\\\":0,\\\"multi_app_msg_item_list\\\":[],\\\"author\\\":\\\"石亚琼\\\",\\\"copyright_stat\\\":11,\\\"duration\\\":0,\\\"del_flag\\\":1,\\\"item_show_type\\\":0,\\\"audio_fileid\\\":0,\\\"play_url\\\":\\\"\\\",\\\"malicious_title_reason_id\\\":0,\\\"malicious_content_type\\\":0}}]}\", \"next_offset\": 20, \"video_count\": 1, \"use_video_tab\": 1, \"real_type\": 0}   可以简单抽取想要的数据,这里将文章表结构定义如下,顺便贴上建表的SQL语句: -- ------------------------------ Table structure for tb_article-- ----------------------------DROP TABLE IF EXISTS \"public\".\"tb_article\";CREATE TABLE \"public\".\"tb_article\" ( \"id\" serial4 PRIMARY KEY, \"msg_id\" int8 NOT NULL, \"title\" varchar(200) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"author\" varchar(20) COLLATE \"pg_catalog\".\"default\", \"cover\" varchar(500) COLLATE \"pg_catalog\".\"default\", \"digest\" varchar(200) COLLATE \"pg_catalog\".\"default\", \"source_url\" varchar(800) COLLATE \"pg_catalog\".\"default\", \"content_url\" varchar(600) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"post_time\" timestamp(6), \"create_time\" timestamp(6) NOT NULL);COMMENT ON COLUMN \"public\".\"tb_article\".\"id\" IS '自增主键';COMMENT ON COLUMN \"public\".\"tb_article\".\"msg_id\" IS '消息id (唯一)';COMMENT ON COLUMN \"public\".\"tb_article\".\"title\" IS '标题';COMMENT ON COLUMN \"public\".\"tb_article\".\"author\" IS '作者';COMMENT ON COLUMN \"public\".\"tb_article\".\"cover\" IS '封面图';COMMENT ON COLUMN \"public\".\"tb_article\".\"digest\" IS '关键字';COMMENT ON COLUMN \"public\".\"tb_article\".\"source_url\" IS '原文地址';COMMENT ON COLUMN \"public\".\"tb_article\".\"content_url\" IS '文章地址';COMMENT ON COLUMN \"public\".\"tb_article\".\"post_time\" IS '发布时间';COMMENT ON COLUMN \"public\".\"tb_article\".\"create_time\" IS '入库时间';COMMENT ON TABLE \"public\".\"tb_article\" IS '公众号文章表';-- ------------------------------ Indexes structure for table tb_article-- ----------------------------CREATE UNIQUE INDEX \"unique_msg_id\" ON \"public\".\"tb_article\" USING btree ( \"msg_id\" \"pg_catalog\".\"int8_ops\" ASC NULLS LAST);   附请求文章接口并解析数据保存到数据库的相关代码: class WxMps(object): \"\"\"微信公众号文章、评论抓取爬虫\"\"\" def __init__(self, _biz, _pass_ticket, _app_msg_token, _cookie, _offset=0): self.offset = _offset self.biz = _biz # 公众号标志 self.msg_token = _app_msg_token # 票据(非固定) self.pass_ticket = _pass_ticket # 票据(非固定) self.headers = { 'Cookie': _cookie, # Cookie(非固定) 'User-Agent': 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.132 ' } wx_mps = 'wxmps' # 这里数据库、用户、密码一致(需替换成实际的) self.postgres = pgs.Pgs(host='localhost', port='5432', db_name=wx_mps, user=wx_mps, password=wx_mps) def start(self): \"\"\"请求获取公众号的文章接口\"\"\" offset = self.offset while True: api = 'https://mp.weixin.qq.com/mp/profile_ext?action=getmsg&__biz={0}&f=json&offset={1}' \\ '&count=10&is_ok=1&scene=124&uin=777&key=777&pass_ticket={2}&wxtoken=&appmsg_token' \\ '={3}&x5=1&f=json'.format(self.biz, offset, self.pass_ticket, self.msg_token) resp = requests.get(api, headers=self.headers).json() ret, status = resp.get('ret'), resp.get('errmsg') # 状态信息 if ret == 0 or status == 'ok': print('Crawl article: ' + api) offset = resp['next_offset'] # 下一次请求偏移量 general_msg_list = resp['general_msg_list'] msg_list = json.loads(general_msg_list)['list'] # 获取文章列表 for msg in msg_list: comm_msg_info = msg['comm_msg_info'] # 该数据是本次推送多篇文章公共的 msg_id = comm_msg_info['id'] # 文章id post_time = datetime.fromtimestamp(comm_msg_info['datetime']) # 发布时间 # msg_type = comm_msg_info['type'] # 文章类型 # msg_data = json.dumps(comm_msg_info, ensure_ascii=False) # msg原数据 app_msg_ext_info = msg.get('app_msg_ext_info') # article原数据 if app_msg_ext_info: # 本次推送的首条文章 self._parse_articles(app_msg_ext_info, msg_id, post_time) # 本次推送的其余文章 multi_app_msg_item_list = app_msg_ext_info.get('multi_app_msg_item_list') if multi_app_msg_item_list: for item in multi_app_msg_item_list: msg_id = item['fileid'] # 文章id if msg_id == 0: msg_id = int(time.time() * 1000) # 设置唯一id,解决部分文章id=0出现唯一索引冲突的情况 self._parse_articles(item, msg_id, post_time) print('next offset is %d' % offset) else: print('Before break , Current offset is %d' % offset) break def _parse_articles(self, info, msg_id, post_time): \"\"\"解析嵌套文章数据并保存入库\"\"\" title = info.get('title') # 标题 cover = info.get('cover') # 封面图 author = info.get('author') # 作者 digest = info.get('digest') # 关键字 source_url = info.get('source_url') # 原文地址 content_url = info.get('content_url') # 微信地址 # ext_data = json.dumps(info, ensure_ascii=False) # 原始数据 self.postgres.handler(self._save_article(), (msg_id, title, author, cover, digest, source_url, content_url, post_time, datetime.now()), fetch=True) @staticmethod def _save_article(): sql = 'insert into tb_article(msg_id,title,author,cover,digest,source_url,content_url,post_time,create_time) ' \\ 'values(%s,%s,%s,%s,%s,%s,%s,%s,%s)' return sql if __name__ == '__main__': biz = 'MzI2NDk5NzA0Mw==' # \"36氪\" pass_ticket = 'NDndxxaZ7p6Z9PYulWpLqMbI0i3ULFeCPIHBFu1sf5pX2IhkGfyxZ6b9JieSYRUy' app_msg_token = '971_Z0lVNQBcGsWColSubRO9H13ZjrPhjuljyxLtiQ~~' cookie = 'wap_sid2=CO3YwOQHEogBQnN4VTNhNmxQWmc3UHI2U3kteWhUeVExZHFVMnN0QXlsbzVJRUJKc1pkdVFUU2Y5UzhSVEtOZmt1VVlYTkR4SEllQ2huejlTTThJWndMQzZfYUw2SldLVGVMQUthUjc3QWdVMUdoaGN0Nml2SU05cXR1dTN2RkhRUVd1V2Y3SFJ5d01BQUF+fjCB1pLcBTgNQJVO' # 以上信息不同公众号每次抓取都需要借助抓包工具做修改 wxMps = WxMps(biz, pass_ticket, app_msg_token, cookie) wxMps.start() # 开始爬取文章 分析文章评论接口  获取评论的思路大致是一样的,只是会更加麻烦一点。首先在手机端点开一篇有评论的文章,然后查看Fiddler抓取的请求:   提取其中的URL和Cookie再次分析: https://mp.weixin.qq.com/mp/appmsg_comment?action=getcomment&scene=0&__biz=MzI2NDk5NzA0Mw==&appmsgid=2247518723&idx=1&comment_id=433253969406607362&offset=0&limit=100&uin=777&key=777&pass_ticket=NDndxxaZ7p6Z9PYulWpLqMbI0i3ULFeCPIHBFu1sf5pX2IhkGfyxZ6b9JieSYRUy&wxtoken=777&devicetype=android-26&clientversion=26070237&appmsg_token=971_dLK7htA1j8LbMUk8pvJKRlC_o218HEgwDbS9uARPOyQ34_vfXv3iDstqYnq2gAyze1dBKm4ZMTlKeyfx&x5=1&f=json Cookie: pgv_pvid=2027337976; pgv_info=ssid=s3015512850; rewardsn=; wxuin=2089823341; devicetype=android-26; version=26070237; lang=zh_CN; pass_ticket=NDndxxaZ7p6Z9PYulWpLqMbI0i3ULFeCPIHBFu1sf5pX2IhkGfyxZ6b9JieSYRUy; wap_sid2=CO3YwOQHEogBdENPSVdaS3pHOWc1V2QzY1NvZG9PYk1DMndPS3NfbGlHM0Vfal8zLU9kcUdkWTQxdUYwckFBT3RZM1VYUXFaWkFad3NVaWFXZ28zbEFIQ2pTa1lqZktfb01vcGdPLTQ0aGdJQ2xOSXoxTVFvNUg3SVpBMV9GRU1lbnotci1MWWl5d01BQUF+fjCj45PcBTgNQAE=; wxtokenkey=777   接着分析参数: __biz:同上 pass_ticket:同上 Cookie:同上 offset和limit:代表偏移量和请求数量,由于公众号评论最多展示100条,所以这两个参数也不用改它 comment_id:获取文章评论数据的标记id,固定但需要从当前文章结构(Html)解析提取 appmsgid:票据id,非固定每次需要从当前文章结构(Html)解析提取 appmsg_token:票据token,非固定每次需要从当前文章结构(Html)解析提取   可以看到最后三个参数要解析html获取(当初真的找了好久才想到看文章网页结构)。从文章请求接口可以获得文章地址,对应上面的content_url字段,但请求该地址前仍需要对url做相关处理,不然上面三个参数会有缺失,也就获取不到后面评论内容: def _parse_article_detail(self, content_url, article_id): \"\"\"从文章页提取相关参数用于获取评论,article_id是已保存的文章id\"\"\" try: api = content_url.replace('amp;', '').replace('#wechat_redirect', '').replace('http', 'https') html = requests.get(api, headers=self.headers).text except: print('获取评论失败' + content_url) else: # group(0) is current line str_comment = re.search(r'var comment_id = \"(.*)\" \\|\\| \"(.*)\" \\* 1;', html) str_msg = re.search(r\"var appmsgid = '' \\|\\| '(.*)'\\|\\|\", html) str_token = re.search(r'window.appmsg_token = \"(.*)\";', html) if str_comment and str_msg and str_token: comment_id = str_comment.group(1) # 评论id(固定) app_msg_id = str_msg.group(1) # 票据id(非固定) appmsg_token = str_token.group(1) # 票据token(非固定)   再回来看该接口返回的json数据,分析结构后然后定义数据表(含SQL): -- ------------------------------ Table structure for tb_article_comment-- ----------------------------DROP TABLE IF EXISTS \"public\".\"tb_article_comment\";CREATE TABLE \"public\".\"tb_article_comment\" ( \"id\" serial4 PRIMARY KEY, \"article_id\" int4 NOT NULL, \"comment_id\" varchar(50) COLLATE \"pg_catalog\".\"default\", \"nick_name\" varchar(50) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"logo_url\" varchar(300) COLLATE \"pg_catalog\".\"default\", \"content_id\" varchar(50) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"content\" varchar(3000) COLLATE \"pg_catalog\".\"default\" NOT NULL, \"like_num\" int2, \"comment_time\" timestamp(6), \"create_time\" timestamp(6) NOT NULL);COMMENT ON COLUMN \"public\".\"tb_article_comment\".\"id\" IS '自增主键';COMMENT ON COLUMN \"public\".\"tb_article_comment\".\"article_id\" IS '文章外键id';COMMENT ON COLUMN \"public\".\"tb_article_comment\".\"comment_id\" IS '评论接口id';COMMENT ON COLUMN \"public\".\"tb_article_comment\".\"nick_name\" IS '用户昵称';COMMENT ON COLUMN \"public\".\"tb_article_comment\".\"logo_url\" IS '头像地址';COMMENT ON COLUMN \"public\".\"tb_article_comment\".\"content_id\" IS '评论id (唯一)';COMMENT ON COLUMN \"public\".\"tb_article_comment\".\"content\" IS '评论内容';COMMENT ON COLUMN \"public\".\"tb_article_comment\".\"like_num\" IS '点赞数';COMMENT ON COLUMN \"public\".\"tb_article_comment\".\"comment_time\" IS '评论时间';COMMENT ON COLUMN \"public\".\"tb_article_comment\".\"create_time\" IS '入库时间';COMMENT ON TABLE \"public\".\"tb_article_comment\" IS '公众号文章评论表';-- ------------------------------ Indexes structure for table tb_article_comment-- ----------------------------CREATE UNIQUE INDEX \"unique_content_id\" ON \"public\".\"tb_article_comment\" USING btree ( \"content_id\" COLLATE \"pg_catalog\".\"default\" \"pg_catalog\".\"text_ops\" ASC NULLS LAST);   万里长征快到头了,最后贴上这部分代码,由于要先获取文章地址,所以和上面获取文章数据的代码是一起的: import jsonimport reimport timefrom datetime import datetimeimport requestsfrom utils import pgsclass WxMps(object): \"\"\"微信公众号文章、评论抓取爬虫\"\"\" def __init__(self, _biz, _pass_ticket, _app_msg_token, _cookie, _offset=0): self.offset = _offset self.biz = _biz # 公众号标志 self.msg_token = _app_msg_token # 票据(非固定) self.pass_ticket = _pass_ticket # 票据(非固定) self.headers = { 'Cookie': _cookie, # Cookie(非固定) 'User-Agent': 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.132 ' } wx_mps = 'wxmps' # 这里数据库、用户、密码一致(需替换成实际的) self.postgres = pgs.Pgs(host='localhost', port='5432', db_name=wx_mps, user=wx_mps, password=wx_mps) def start(self): \"\"\"请求获取公众号的文章接口\"\"\" offset = self.offset while True: api = 'https://mp.weixin.qq.com/mp/profile_ext?action=getmsg&__biz={0}&f=json&offset={1}' \\ '&count=10&is_ok=1&scene=124&uin=777&key=777&pass_ticket={2}&wxtoken=&appmsg_token' \\ '={3}&x5=1&f=json'.format(self.biz, offset, self.pass_ticket, self.msg_token) resp = requests.get(api, headers=self.headers).json() ret, status = resp.get('ret'), resp.get('errmsg') # 状态信息 if ret == 0 or status == 'ok': print('Crawl article: ' + api) offset = resp['next_offset'] # 下一次请求偏移量 general_msg_list = resp['general_msg_list'] msg_list = json.loads(general_msg_list)['list'] # 获取文章列表 for msg in msg_list: comm_msg_info = msg['comm_msg_info'] # 该数据是本次推送多篇文章公共的 msg_id = comm_msg_info['id'] # 文章id post_time = datetime.fromtimestamp(comm_msg_info['datetime']) # 发布时间 # msg_type = comm_msg_info['type'] # 文章类型 # msg_data = json.dumps(comm_msg_info, ensure_ascii=False) # msg原数据 app_msg_ext_info = msg.get('app_msg_ext_info') # article原数据 if app_msg_ext_info: # 本次推送的首条文章 self._parse_articles(app_msg_ext_info, msg_id, post_time) # 本次推送的其余文章 multi_app_msg_item_list = app_msg_ext_info.get('multi_app_msg_item_list') if multi_app_msg_item_list: for item in multi_app_msg_item_list: msg_id = item['fileid'] # 文章id if msg_id == 0: msg_id = int(time.time() * 1000) # 设置唯一id,解决部分文章id=0出现唯一索引冲突的情况 self._parse_articles(item, msg_id, post_time) print('next offset is %d' % offset) else: print('Before break , Current offset is %d' % offset) break def _parse_articles(self, info, msg_id, post_time): \"\"\"解析嵌套文章数据并保存入库\"\"\" title = info.get('title') # 标题 cover = info.get('cover') # 封面图 author = info.get('author') # 作者 digest = info.get('digest') # 关键字 source_url = info.get('source_url') # 原文地址 content_url = info.get('content_url') # 微信地址 # ext_data = json.dumps(info, ensure_ascii=False) # 原始数据 content_url = content_url.replace('amp;', '').replace('#wechat_redirect', '').replace('http', 'https') article_id = self.postgres.handler(self._save_article(), (msg_id, title, author, cover, digest, source_url, content_url, post_time, datetime.now()), fetch=True) if article_id: self._parse_article_detail(content_url, article_id) def _parse_article_detail(self, content_url, article_id): \"\"\"从文章页提取相关参数用于获取评论,article_id是已保存的文章id\"\"\" try: html = requests.get(content_url, headers=self.headers).text except: print('获取评论失败' + content_url) else: # group(0) is current line str_comment = re.search(r'var comment_id = \"(.*)\" \\|\\| \"(.*)\" \\* 1;', html) str_msg = re.search(r\"var appmsgid = '' \\|\\| '(.*)'\\|\\|\", html) str_token = re.search(r'window.appmsg_token = \"(.*)\";', html) if str_comment and str_msg and str_token: comment_id = str_comment.group(1) # 评论id(固定) app_msg_id = str_msg.group(1) # 票据id(非固定) appmsg_token = str_token.group(1) # 票据token(非固定) # 缺一不可 if appmsg_token and app_msg_id and comment_id: print('Crawl article comments: ' + content_url) self._crawl_comments(app_msg_id, comment_id, appmsg_token, article_id) def _crawl_comments(self, app_msg_id, comment_id, appmsg_token, article_id): \"\"\"抓取文章的评论\"\"\" api = 'https://mp.weixin.qq.com/mp/appmsg_comment?action=getcomment&scene=0&__biz={0}' \\ '&appmsgid={1}&idx=1&comment_id={2}&offset=0&limit=100&uin=777&key=777' \\ '&pass_ticket={3}&wxtoken=777&devicetype=android-26&clientversion=26060739' \\ '&appmsg_token={4}&x5=1&f=json'.format(self.biz, app_msg_id, comment_id, self.pass_ticket, appmsg_token) resp = requests.get(api, headers=self.headers).json() ret, status = resp['base_resp']['ret'], resp['base_resp']['errmsg'] if ret == 0 or status == 'ok': elected_comment = resp['elected_comment'] for comment in elected_comment: nick_name = comment.get('nick_name') # 昵称 logo_url = comment.get('logo_url') # 头像 comment_time = datetime.fromtimestamp(comment.get('create_time')) # 评论时间 content = comment.get('content') # 评论内容 content_id = comment.get('content_id') # id like_num = comment.get('like_num') # 点赞数 # reply_list = comment.get('reply')['reply_list'] # 回复数据 self.postgres.handler(self._save_article_comment(), (article_id, comment_id, nick_name, logo_url, content_id, content, like_num, comment_time, datetime.now())) @staticmethod def _save_article(): sql = 'insert into tb_article(msg_id,title,author,cover,digest,source_url,content_url,post_time,create_time) ' \\ 'values(%s,%s,%s,%s,%s,%s,%s,%s,%s) returning id' return sql @staticmethod def _save_article_comment(): sql = 'insert into tb_article_comment(article_id,comment_id,nick_name,logo_url,content_id,content,like_num,' \\ 'comment_time,create_time) values(%s,%s,%s,%s,%s,%s,%s,%s,%s)' return sqlif __name__ == '__main__': biz = 'MzI2NDk5NzA0Mw==' # \"36氪\" pass_ticket = 'NDndxxaZ7p6Z9PYulWpLqMbI0i3ULFeCPIHBFu1sf5pX2IhkGfyxZ6b9JieSYRUy' app_msg_token = '971_Z0lVNQBcGsWColSubRO9H13ZjrPhjuljyxLtiQ~~' cookie = 'wap_sid2=CO3YwOQHEogBQnN4VTNhNmxQWmc3UHI2U3kteWhUeVExZHFVMnN0QXlsbzVJRUJKc1pkdVFUU2Y5UzhSVEtOZmt1VVlYTkR4SEllQ2huejlTTThJWndMQzZfYUw2SldLVGVMQUthUjc3QWdVMUdoaGN0Nml2SU05cXR1dTN2RkhRUVd1V2Y3SFJ5d01BQUF+fjCB1pLcBTgNQJVO' # 以上信息不同公众号每次抓取都需要借助抓包工具做修改 wxMps = WxMps(biz, pass_ticket, app_msg_token, cookie) wxMps.start() # 开始爬取文章及评论 文末小结  最后展示下数据库里的数据,单线程爬的慢而且又没这方面的数据需求,所以也只是随便试了下手:   有时候写爬虫是个细心活,如果觉得太麻烦的话,推荐了解下WechatSogou这个工具。有问题的欢迎底部留言讨论。 完整代码:GitHub","categories":[{"name":"python","slug":"python","permalink":"https://blog.mariojd.cn/categories/python/"}],"tags":[{"name":"spider crawl","slug":"spider-crawl","permalink":"https://blog.mariojd.cn/tags/spider-crawl/"},{"name":"fiddler capture","slug":"fiddler-capture","permalink":"https://blog.mariojd.cn/tags/fiddler-capture/"}]},{"title":"用 Python 快速分析你的微信好友","slug":"用 Python 快速分析你的微信好友","date":"2018-08-26","updated":"2019-06-02","comments":true,"path":"use-python-to-analyze-wechat-friends.html","link":"","permalink":"https://blog.mariojd.cn/use-python-to-analyze-wechat-friends.html","excerpt":"","keywords":"","text":"写在前面  itchat基于python开发,封装了大量调取微信功能的接口,使得开发人员可以快速基于这个框架来完成一些微信操作,在这之前我们要做的就是扫码登录,实际上这相当于登录网页版的微信(新注册的账号似乎不支持)。更多介绍在官网和Github上都有详细的文档。  下面介绍基于itchat完成微信数据(好友、群聊等)的分析和展示。 环境说明 python3 numpy matplotlib pillow 相关代码 wx_itchat.py import osimport mathimport itchatimport numpy as npimport PIL.Image as Imageimport matplotlib.pyplot as plt# 抽取出来的工具类from utils import match_utilclass WxChat(object): def __init__(self): \"\"\"初始化(扫码登录或手机端确认)\"\"\" itchat.auto_login(hotReload=True) # 登录用户 self.login_user = None # 好友总数 self.num_of_friend = 0 # 男性好友数 self.male_num = 0 # 女性好友数 self.female_num = 0 # 未知性别好友数 self.unknown_gender = 0 # K=省名 V=人数 self.num_of_province = {} # 未知省份 self.unknown_province = '其它' # 绘图大小 plt.figure(figsize=(6.4, 4.8)) # 图片存放文件夹 self.images_dir = 'wxImages' self.avatar_dir = '{}{}{}'.format(self.images_dir, os.sep, \"avatarImages\") if not os.path.exists(self.images_dir): os.mkdir(self.images_dir) os.mkdir(self.avatar_dir) def _reset_data(self): \"\"\"数据重置\"\"\" self.male_num = 0 self.female_num = 0 self.unknown_gender = 0 self.num_of_province = {} def analysis_friends(self): \"\"\"获取好友列表 , 第一个是本人\"\"\" print(\"--->开始分析您的好友数据\") all_user = itchat.get_friends(update=True) self.login_user = all_user[0] self.num_of_friend = len(all_user) - 1 friends = all_user[1:] for i, friend in enumerate(friends): self._count_sex(friend['Sex']) self._count_province(friend['Province']) nickname = friend['NickName'] # 昵称 remark = friend['RemarkName'] # 备注 signature = ''.join(friend['Signature'].split()) # 个性签名 # city = friend['City'] # 所在城市 # 下载好友头像(不包括自己) avatar_data = itchat.get_head_img(userName=friend[\"UserName\"]) with open(\"{}{}{}.jpg\".format(self.avatar_dir, os.sep, i), 'wb') as f: f.write(avatar_data) print(\"%d.好友昵称: %s ,备注: %s ,个性签名: %s \" % (i + 1, nickname, remark, signature)) print('共有 %d 位微信好友 , 其中男性好友 %d 位,女性好友 %d 位 , %d 位未知性别好友' % ( self.num_of_friend, self.male_num, self.female_num, self.unknown_gender)) # 省份数据处理(可选) self._handle_province(self.num_of_friend) # 绘制性别柱图, 绘制省份饼图(可选) title = 'WeChatFriends' wx._plt_gender_bar(title), wx._plt_province_pie(title) # 将头像拼图(可选) self._puzzle_avatar(title) self._reset_data() def analysis_chat_rooms(self): \"\"\"获取群聊列表\"\"\" print(\"--->开始分析您的群聊数据\") chat_rooms = itchat.get_chatrooms(update=True) for chat_room in chat_rooms[:-1]: chat_room_name = chat_room['NickName'] # 群聊名称 user_name = chat_room['UserName'] # 群聊id标志 room = itchat.update_chatroom(user_name, detailedMember=True) member_count = room['MemberCount'] # 群聊人数 # 群聊用户列表 for member in room['MemberList']: # avatar = 'https://wx.qq.com'.format(member['HeadImgUrl']) # 头像 self._count_sex(member['Sex']) self._count_province(member['Province']) print('群聊 %s 共有 %d 人 , 其中 %d 位男性 , %d 位女性 , %d 位性别未知' % ( chat_room_name, member_count, self.male_num, self.female_num, self.unknown_gender)) # 省份数据处理(可选) self._handle_province(member_count) # 绘制性别柱图, 省份饼图(可选) self._plt_gender_bar(chat_room_name), self._plt_province_pie(chat_room_name) self._reset_data() def _count_sex(self, sex): \"\"\"统计性别情况\"\"\" # 0-未知 , 1-男 , 2-女 if sex == 1: self.male_num += 1 elif sex == 2: self.female_num += 1 else: self.unknown_gender += 1 def _count_province(self, province_name): \"\"\"统计省份情况\"\"\" if not province_name or not match_util.is_all_chinese(province_name): # 未设置省份名或非国内城市 other_province_num = self.num_of_province.get(self.unknown_province) self.num_of_province.__setitem__(self.unknown_province, 1 if other_province_num is None else other_province_num + 1) else: province_num = self.num_of_province.get(province_name) self.num_of_province.__setitem__(province_name, 1 if province_num is None else province_num + 1) def _handle_province(self, member_count): \"\"\"处理所在省份人数占比较低(少于2%)的数据 :param member_count: 总人数 \"\"\" for province in list(self.num_of_province.keys()): number = self.num_of_province[province] if number / member_count < .02: other_province_num = self.num_of_province.get(self.unknown_province) self.num_of_province.__setitem__(self.unknown_province, other_province_num + number) del self.num_of_province[province] def _plt_gender_bar(self, title): \"\"\" 绘制性别柱状图 :param title: 名称 \"\"\" plt.bar('男', self.male_num, color='yellow') plt.bar('女', self.female_num, color='pink') plt.bar('未知', self.unknown_gender, color='gray') plt.xlabel('性别'), plt.ylabel('人数'), plt.title(title) for a, b in zip([0, 1, 2], np.array([self.male_num, self.female_num, self.unknown_gender])): plt.text(a, b, '%.0f' % b, ha='center', va='bottom') plt.savefig('{}{}{}(性别统计).png'.format(self.images_dir, os.sep, title)) plt.show() def _plt_province_pie(self, title): \"\"\" 绘制省份圆饼图 :param title: 名称 \"\"\" data = np.array(list(self.num_of_province.values())) labels = list(self.num_of_province.keys()) plt.pie(data, labels=labels, autopct='%.1f%%', ) plt.axis('equal') plt.legend(loc=2, prop={'size': 5.5}) plt.title(title) plt.savefig('{}{}{}(省份分布).png'.format(self.images_dir, os.sep, title)) plt.show() def _puzzle_avatar(self, title='wx'): \"\"\"用pillow拼图\"\"\" pic_size = 1080 # (正方形) avatar_size = int(math.sqrt(float(pic_size * pic_size) / self.num_of_friend)) # 定义头像的尺寸 lines = int(pic_size / avatar_size) + 1 # 一共要画多少行 new_image = Image.new('RGB', (pic_size, pic_size)) # 定义空白底图 x, y = 0, 0 # 起始坐标 for i in range(0, self.num_of_friend): try: img = Image.open(\"{}{}{}.jpg\".format(self.avatar_dir, os.sep, i)) except IOError: print(\"Error: No file or Read file error\") else: img = img.resize((avatar_size, avatar_size), Image.ANTIALIAS) # 调整原头像大小为定义尺寸 new_image.paste(img, (x * avatar_size, y * avatar_size)) # 将该头像绘制到底图 # 改变绘制坐标 x += 1 if x == lines: x, y = 0, y + 1 file_path = '{}{}{}.jpg'.format(self.images_dir, os.sep, title) new_image.save(file_path) # 发送到手机上(可选) self.send_image_to_filehelper(file_path) @staticmethod def send_msg_to_file_helper(msg): \"\"\"给文件助手发消息\"\"\" itchat.send(msg, 'filehelper') @staticmethod def send_image_to_filehelper(img): \"\"\"给文件助手发图片\"\"\" itchat.send_image(img, 'filehelper') @staticmethod def get_mps(): \"\"\"获取公众号列表\"\"\" return itchat.get_mps() @staticmethod def logout(): \"\"\"退出登录\"\"\" itchat.logout()if __name__ == '__main__': wx = WxChat() # 分析微信好友 wx.analysis_friends() # 分析微信群聊 wx.analysis_chat_rooms() wx.logout() match_util.py def is_all_chinese(text): \"\"\"判断text是否全部为中文 :param text: :return: True or False \"\"\" return u'\\u4e00' <= text <= u'\\u9fff' 效果展示  原是三张清晰的图片,这里截成一张图大概展示下效果。 示例代码:Github","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.mariojd.cn/categories/Python/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.mariojd.cn/tags/python/"},{"name":"itchat","slug":"itchat","permalink":"https://blog.mariojd.cn/tags/itchat/"},{"name":"weixin","slug":"weixin","permalink":"https://blog.mariojd.cn/tags/weixin/"}]},{"title":"如何用 Python 简单褥羊毛 (京东京豆)","slug":"如何用 Python 简单褥羊毛 (京东京豆)","date":"2018-08-20","updated":"2019-06-02","comments":true,"path":"use-python-to-bedding-jd-wool.html","link":"","permalink":"https://blog.mariojd.cn/use-python-to-bedding-jd-wool.html","excerpt":"","keywords":"","text":"  PS:本文适合有一点点Python基础的人阅读。 前言  干我们这行的,碰到搬轮子、写代码便能轻松解决的事情要尽早去做,个人认为日常生活中这样的事并不少,走点心或许就是一个学习或是发财机会ヾ(๑╹◡╹)ノ”  这里介绍的“羊毛”主是指京东平台的虚拟货币:京豆,其实钢镚完善一下代码也是可以搞的,说真的没多少行代码但作为辅助工具真的够用了,如果按每天能褥一波计算,少则有几十京豆,多则一两百也是有可能是。 战果展示  贴代码前先秀一下不久前撸的战果,真的不多,但坚持下来搞两包辣条还是可以有的٩(๑❛ᴗ❛๑)۶ 相关说明 Python3 Requests BeautifulSoup4 Selenium (配置好Chrome Driver、Firefox Driver或是PhantomJS环境) 京东账号得关联QQ,且当前QQ在线 (用于QQ授权登录京东,可自行扩展登录方式) 相关代码  py写的代码已经很简洁了,注释也完善了很多,有兴趣的继续往下看。 wx_turing.py import timefrom urllib.parse import parse_qsimport requestsfrom bs4 import BeautifulSoupfrom selenium import webdriverfrom selenium.common.exceptions import *from selenium.webdriver.support.wait import WebDriverWait# 额外抽取的授权模块from utils import authclass QMM(object): \"\"\"借助券妈妈平台褥京东京豆\"\"\" def __init__(self, sleep=3, months=None, days=None): self.timeout, self.months, self.days = sleep, None, None # 爬取规则 if months: month_interval = months.split('-') start_month, end_month = int(month_interval[0]), int(month_interval[-1]) self.months = list(map(lambda m: '{}月'.format(m), range(start_month, end_month + 1))) if days: day_interval = days.split('-') start_day, end_day = int(day_interval[0]), int(day_interval[-1]) self.days = list(map(lambda d: '{}日'.format(d), range(start_day, end_day + 1))) # 手机店铺(用作提醒输出,可复制链接到手机端领取) self.m_shop = [] # 统计京豆总数 self.jing_dou = 0 def _crawl_url(self): \"\"\" 抓取京豆更新页, 获得店铺京豆领取地址\"\"\" # 日期更新页 qmm_collect = 'http://www.quanmama.com/zhidemai/2459063.html' bs = BeautifulSoup(requests.get(qmm_collect).text, 'html.parser') for link in bs.tbody.find_all('a'): text = link.text if self.months: if not list(filter(lambda m: m in text, self.months)): continue if self.days: if not list(filter(lambda d: d in text, self.days)): continue qmm_detail = link.get('href') # 店铺领取页 resp = requests.get(qmm_detail) bs = BeautifulSoup(resp.text, 'html.parser') for body in bs.find_all('tbody'): for mall in body.find_all('a'): url = self._parse_url(mall.get('href')) if 'shop.m.jd.com' in url: self.m_shop.append(url) else: yield url @staticmethod def _parse_url(url): \"\"\"提取URL中的url参数\"\"\" mall_url = parse_qs(url).get('url') return mall_url.pop() if mall_url else url def start(self): \"\"\" 登录京东,领取店铺羊毛\"\"\" malls = set(self._crawl_url()) print('共有 %d 个可褥羊毛PC端店铺页面' % len(malls)) m_malls = self.m_shop print('共有 %d 个可褥羊毛手机端店铺页面' % len(m_malls)) for m_mall in m_malls: print(m_mall) if malls: # 登陆京东(Chrome、PhantomJS or FireFox) driver = webdriver.Chrome() # driver = webdriver.PhantomJS() jd_login = 'https://passport.jd.com/new/login.aspx' driver.get(jd_login) # 窗口最大化 driver.maximize_window() # QQ授权登录 driver.find_element_by_xpath('//*[@id=\"kbCoagent\"]/ul/li[1]/a').click() auth.qq(driver) time.sleep(self.timeout) # 开始褥羊毛 for i, detail in enumerate(malls): driver.get(detail) print('%d.店铺: %s' % (i + 1, detail), end='') try: # 查找\"领取\"按钮 btn = WebDriverWait(driver, self.timeout).until( lambda d: d.find_element_by_css_selector(\"[class='J_drawGift d-btn']\")) except TimeoutException: # 失败大多数情况下是无羊毛可褥(券妈妈平台只是简单汇总但不一定就有羊毛) print(' 领取失败, TimeoutException ') else: try: # 输出羊毛战绩 items = WebDriverWait(driver, self.timeout).until( lambda d: d.find_elements_by_css_selector(\"[class='d-item']\")) for item in items: item_type = item.find_element_by_css_selector(\"[class='d-type']\").text item_num = item.find_element_by_css_selector(\"[class='d-num']\").text if item_type == '京豆': self.jing_dou += item_num print(' {}{} '.format(item_type, item_num), end='') except: # 此处异常不太重要, 忽略 pass finally: btn.click() print(' 领取成功') # 以下附加功能可选 self._print_jing_dou() self._un_subscribe(driver) self._finance_sign(driver) def _print_jing_dou(self): print('O(∩_∩)O哈哈~, 共褥到了{}个京豆,相当于RMB{}元', self.jing_dou, self.jing_dou / 100) def _un_subscribe(self, driver): \"\"\"批量取消店铺关注\"\"\" # 进入关注店铺 subscribe_shop = 'https://t.jd.com/vender/followVenderList.action' driver.get(subscribe_shop) try: # 批量操作 batch_btn = WebDriverWait(driver, self.timeout).until( lambda d: d.find_element_by_xpath('//*[@id=\"main\"]/div/div[2]/div[1]/div[2]/div[2]/div/a')) batch_btn.click() # 全选店铺 all_btn = WebDriverWait(driver, self.timeout).until( lambda d: d.find_element_by_xpath('//*[@id=\"main\"]/div/div[2]/div[1]/div[2]/div[2]/div/div/span[1]')) all_btn.click() # 取消关注 cancel_btn = WebDriverWait(driver, self.timeout).until( lambda d: d.find_element_by_xpath('//*[@id=\"main\"]/div/div[2]/div[1]/div[2]/div[2]/div/div/span[2]')) cancel_btn.click() # 弹框确认 confirm_btn = WebDriverWait(driver, self.timeout).until( lambda d: d.find_element_by_xpath(\"/html/body/div[7]/div[3]/a[1]\")) except TimeoutException: print(' 批量取关店铺失败, TimeoutException ') else: confirm_btn.click() print(' 已批量取消关注店铺') def _finance_sign(self, driver): \"\"\"京东金融签到领钢镚\"\"\" # 进入京东金融 jr_login = 'https://jr.jd.com/' driver.get(jr_login) try: # 点击签到按钮 sign_btn = WebDriverWait(driver, self.timeout).until( lambda d: d.find_element_by_xpath('//*[@id=\"primeWrap\"]/div[1]/div[3]/div[1]/a')) except TimeoutException: print(' 京东金融签到失败, TimeoutException ') else: sign_btn.click() print(' 京东金融签到成功')if __name__ == '__main__': qmm = QMM(sleep=3, months='7-8', days='16-31') qmm.start() auth.py from selenium.webdriver.support.wait import WebDriverWait# QQ授权登录, 使用前提是QQ客户端在线def qq(driver, timeout=3): # 切换到最新打开的窗口 window_handles = driver.window_handles driver.switch_to.window(window_handles[-1]) print('Auth QQ: ', driver.title) # 切换iframe i_frame = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_id('ptlogin_iframe')) driver.switch_to.frame(i_frame) # 点击头像进行授权登录 login = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_xpath('//*[@id=\"qlogin_list\"]/a[1]')) login.click() 后续展望  代码要是出错了可能网页结构发生了变化,这个可以自行调整。还有很多待完善的地方和可扩展的空间,有兴趣的可以参考实现以下几点: 加入每日定时功能 扩展登录京东方式 多线程褥羊毛(需求不大) Appium抓取手机店铺主页 其它… 示例代码:Github","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.mariojd.cn/categories/Python/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.mariojd.cn/tags/python/"},{"name":"selenium","slug":"selenium","permalink":"https://blog.mariojd.cn/tags/selenium/"},{"name":"requests","slug":"requests","permalink":"https://blog.mariojd.cn/tags/requests/"},{"name":"beautifulsoup4","slug":"beautifulsoup4","permalink":"https://blog.mariojd.cn/tags/beautifulsoup4/"}]},{"title":"设计模式入门:建造者模式","slug":"设计模式入门:建造者模式","date":"2018-08-01","updated":"2018-09-29","comments":true,"path":"builder-of-design-pattern.html","link":"","permalink":"https://blog.mariojd.cn/builder-of-design-pattern.html","excerpt":"","keywords":"","text":"UML类图 代码示例 Product /** * 表示具体的产品,由多个部件组成 */public class Product { /** * 存放产品每个部件的集合 */ private List<String> parts = new LinkedList<>(); /** * 添加某一部件到该集合中 * * @param part */ void addPart(String part) { parts.add(part); } /** * 展示产品的部件组成 */ void show() { parts.forEach(part -> System.out.print(part + \" \")); }} Builder /** * 抽象建造者,用于确定产品的组成及获取 */public interface Builder { /** * 部件A */ void partA(); /** * 部件B */ void partB(); /** * 获取产品 * * @return */ Product getProduct();} BuilderImpl /** * 具体建造者 */public class BuilderImpl implements Builder { private Product product = new Product(); @Override public void partA() { product.addPart(\"部件A\"); } @Override public void partB() { product.addPart(\"部件B\"); } @Override public Product getProduct() { return product; }} Director /** * 指挥者类(控制建造流程) */public class Director { /** * 控制并构造所需产品的最终形态 * * @param builder */ void buildProduct(Builder builder) { builder.partB(); builder.partA(); }} 测试 public class Main { public static void main(String[] args) { // 指挥者 Director director = new Director(); Builder builder = new BuilderImpl(); // 指挥者调用所控制的建造过程 director.buildProduct(builder); // 建造完成后返回最终产品 Product product = builder.getProduct(); // 调用展示 product.show(); }} 总结思考  建造者模式中,一些基本部件的实现应该是不怎么变化的,它更关注于指挥者装配并组合部件的过程,对应生活中的KFC,汉堡、可乐、薯条等是不变的,而其组合是经常变化的,演变出各式各样的套餐。 示例代码","categories":[{"name":"设计模式","slug":"设计模式","permalink":"https://blog.mariojd.cn/categories/设计模式/"}],"tags":[{"name":"design patterns","slug":"design-patterns","permalink":"https://blog.mariojd.cn/tags/design-patterns/"},{"name":"builder","slug":"builder","permalink":"https://blog.mariojd.cn/tags/builder/"},{"name":"建造者","slug":"建造者","permalink":"https://blog.mariojd.cn/tags/建造者/"}]},{"title":"设计模式入门:原型模式","slug":"设计模式入门:原型模式","date":"2018-07-31","updated":"2018-09-29","comments":true,"path":"prototype-of-design-pattern.html","link":"","permalink":"https://blog.mariojd.cn/prototype-of-design-pattern.html","excerpt":"","keywords":"","text":"UML类图 代码示例  实际应用中,原型模式可以简单理解为克隆操作。在大多数面向对象编程语言中,实现克隆操作并不复杂,对于Java,我们只需继承Cloneable接口,并重写Object的clone()即可(非必须)。 public class Teacher implements Cloneable { /** * 姓名 */ private String name; private Teacher(String name) { try { // 模拟初始化资源耗时较多 TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } this.name = name; } @Override public String toString() { return \"Teacher{\" + \"name='\" + name + '\\'' + '}'; } public static void main(String[] args) throws CloneNotSupportedException { Teacher teacher = new Teacher(\"李老师\"); // output: teacher = Teacher{name='李老师'} System.out.println(\"teacher = \" + teacher); // clone is much faster Teacher cloneTeacher = (Teacher) teacher.clone(); // output: cloneTeacher = Teacher{name='李老师'} System.out.println(\"cloneTeacher = \" + cloneTeacher); }}   clone()方法会抛出未检查的CloneNotSupportedException,通过源码了解下什么情况下会产生该异常。   该方法有native修饰,这说明它的具体实现在底层(参考JNI)。文档说明,在当前克隆类没有实现Cloneable接口的情况下,该异常将会触发。另外,还有这么一句值得我们留意,Thus, this method performs a "shallow copy" of this object, not a "deep copy" operation.,这说明clone()实现的是浅拷贝而不是深拷贝(下个话题深入)。最后,重写clone()是非必须的,一般重写只是用来提高方法的访问权限。 总结思考  原型模式通过(浅)拷贝现有对象以生成新的对象,属于创建型模式中的一种。对比通过new的方式来实例化对象,这种模式不用重新初始化对象,可以动态的获得对象运行时的状态,也适用于需要优化性能的场景,例如当类的初始化需要消耗非常多的资源,就可以考虑使用这种模式了。  接触原型模式,不可避免的就是关于浅拷贝和深拷贝的问题,下一篇准备深入了解下这个话题。 示例代码","categories":[{"name":"设计模式","slug":"设计模式","permalink":"https://blog.mariojd.cn/categories/设计模式/"}],"tags":[{"name":"design patterns","slug":"design-patterns","permalink":"https://blog.mariojd.cn/tags/design-patterns/"},{"name":"prototype","slug":"prototype","permalink":"https://blog.mariojd.cn/tags/prototype/"},{"name":"原型","slug":"原型","permalink":"https://blog.mariojd.cn/tags/原型/"}]},{"title":"设计模式入门:单例模式","slug":"设计模式入门:单例模式","date":"2018-07-27","updated":"2018-09-29","comments":true,"path":"singleton-of-design-pattern.html","link":"","permalink":"https://blog.mariojd.cn/singleton-of-design-pattern.html","excerpt":"","keywords":"","text":"UML类图 代码示例 饿汉式 /** * 单例模式:饿汉式 * <p> * a.线程安全 * b.提前初始化(占用资源) * c.类加载慢但获取对象快 */public class HungrySingleton { private static final HungrySingleton INSTANCE = new HungrySingleton(); /** * 私有构造器 */ private HungrySingleton() { } /** * 获取当前实例的唯一入口 * * @return 当前实例 */ public static HungrySingleton getInstance() { return INSTANCE; }} 2.1 懒汉式 /** * 单例模式:懒汉式 * <p> * a.非线程安全 * b.实现延迟加载 * c.类加载快但获取对象慢 */public class LazySingleton { private static LazySingleton instance = null; /** * 私有构造器 */ private LazySingleton() { } /** * 获取当前实例的唯一入口 * 多线程下非线程安全 * * @return 当前实例 */ public static LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; }} 2.2 懒汉式(同步锁synchronized) /** * 单例模式:懒汉式(synchronized) * <p> * a.非线程安全 * b.实现延迟加载 * c.类加载快但获取对象慢 */public class SynchronizedSingleton { private static SynchronizedSingleton instance = null; /** * 私有构造器 */ private SynchronizedSingleton() { } /** * 获取当前实例的唯一入口 * 通过synchronized机制保证多线程安全,但同时也消耗较多的性能 * * @return 当前实例 */ public static synchronized SynchronizedSingleton getInstance() { if (instance == null) { instance = new SynchronizedSingleton(); } return instance; }} 2.3 懒汉式(DCL) /*** 单例模式:懒汉式(双重检测锁。DCL , Double-checked Locking) * <p> * a.非线程安全 * b.实现延迟加载 * c.类加载快但获取对象慢 */public class DCLSingleton { /** * volatile确保了实例的可视性 */ private static volatile DCLSingleton instance = null; /** * 私有构造器 */ private DCLSingleton() { } /** * 获取当前实例的唯一入口 * 通过DCL机制保证多线程安全,同时消耗较少的性能 * * @return 当前实例 */ public static DCLSingleton getInstance() { if (instance == null) { synchronized (DCLSingleton.class) { if (instance == null) { instance = new DCLSingleton(); } } } return instance; }} 静态内部类 /** * 单例模式:登记式(静态内部类) * <p> * a.线程安全 * b.实现延迟加载 */public class InnerClassSingleton { /** * 静态内部类,用于初始化调用时实例化当前 instance */ private static class InnerStaticClassHolder { private static final InnerClassSingleton INSTANCE = new InnerClassSingleton(); } /** * 私有构造器 */ private InnerClassSingleton() { } /** * 获取当前实例的唯一入口 * 通过对静态域使用延迟初始化达到延迟加载的效果 * (只有通过显式调用 getInstance 方法时,才会显式装载 InnerStaticClassHolder 类,从而实例化 instance) * * @return 当前实例 */ private static InnerClassSingleton getInstance() { return InnerStaticClassHolder.INSTANCE; }} 枚举 /** * 单例模式:枚举 * <p> * a.线程安全 * b.非延迟加载 * c.代码更简洁,支持序列化机制 * ps: 这是实现单例模式的最佳方式,但未被广泛使用。 */public enum EnumSingleton { /** * 该定义用于返回当前实例 */ INSTANCE;} 总结思考  单例模式属于创建型模式,是一种较为简单的设计模式,但也是最容易让人犯错的。在不同的单例模式实现中,首先要确保构造函数是私有的,然后提供一个静态入口(方法)用于获取唯一的实例。  大多数情况下,不建议使用非线程安全的以及synchronized监视器锁实现的懒汉方式,在资源允许的情况下尽可能使用饿汉模式。如果明确要实现 lazy loading 效果时,可以使用静态内部类形式。如果涉及到反序列化创建对象时,可以尝试使用枚举方式。如果有其他特殊的需求,可以考虑使用DCL双检锁的方式。 示例代码","categories":[{"name":"设计模式","slug":"设计模式","permalink":"https://blog.mariojd.cn/categories/设计模式/"}],"tags":[{"name":"design patterns","slug":"design-patterns","permalink":"https://blog.mariojd.cn/tags/design-patterns/"},{"name":"singleton","slug":"singleton","permalink":"https://blog.mariojd.cn/tags/singleton/"},{"name":"单例","slug":"单例","permalink":"https://blog.mariojd.cn/tags/单例/"}]},{"title":"设计模式入门:抽象工厂模式","slug":"设计模式入门:抽象工厂模式","date":"2018-07-24","updated":"2018-09-29","comments":true,"path":"abstract-factory-of-design-pattern.html","link":"","permalink":"https://blog.mariojd.cn/abstract-factory-of-design-pattern.html","excerpt":"","keywords":"","text":"UML类图 代码示例 定义苹果类接口,包含一个描述方法 /** * 苹果抽象类 */public interface IApple { /** * 具体描述 */ void describe();} 红苹果,实现了接口苹果和定义的方法 /** * 红苹果 */public class RedApple implements IApple { @Override public void describe() { System.out.println(\"This is red apple\"); }} 青苹果,实现了接口苹果和定义的方法 /** * 青苹果 */public class GreenApple implements IApple { @Override public void describe() { System.out.println(\"This is green apple\"); }} 定义梨子类接口,包含一个描述方法 /** * 梨子抽象类 */public interface IPear { /** * 具体描述 */ void describe();} 青梨,实现了接口梨子和定义的方法 /** * 青梨 */public class GreenPear implements IPear { @Override public void describe() { System.out.println(\"This is green pear\"); }} 黄梨,实现了接口梨子和定义的方法 /** * 黄梨 */public class YellowPear implements IPear { @Override public void describe() { System.out.println(\"This is yellow pear\"); }} 水果工厂抽象类 /** * 水果工厂抽象类 */public interface IFruitFactory { /** * 创建苹果实例 * * @return */ IApple createApple(); /** * 创建梨子实例 * * @return */ IPear createPear();} 工厂A: 负责生产青苹果和青梨 /** * 工厂A: 负责生产青苹果和青梨 */public class FactoryA implements IFruitFactory { @Override public IApple createApple() { return new GreenApple(); } @Override public IPear createPear() { return new GreenPear(); }} 工厂B: 负责生产红苹果和黄梨 /** * 工厂B: 负责生产红苹果和黄梨 */public class FactoryB implements IFruitFactory { @Override public IApple createApple() { return new RedApple(); } @Override public IPear createPear() { return new YellowPear(); }} 客户端测试 public class Main { public static void main(String[] args) { IFruitFactory factoryA = new FactoryA(); IApple apple = factoryA.createApple(); apple.describe(); IPear pear = factoryA.createPear(); pear.describe(); System.out.println(\"-------------------\"); IFruitFactory factoryB = new FactoryB(); apple = factoryB.createApple(); apple.describe(); pear = factoryB.createPear(); pear.describe(); }} 总结思考  抽象工厂用于提供创建一系列相关或互相依赖的接口,而无需指定它们具体的类。对比工厂方法,抽象工厂面对的是整个产品族,而工厂方法面对的是独立的产品。 相关链接 示例代码设计模式:简单工厂、工厂方法、抽象工厂之小结与区别","categories":[{"name":"设计模式","slug":"设计模式","permalink":"https://blog.mariojd.cn/categories/设计模式/"}],"tags":[{"name":"design patterns","slug":"design-patterns","permalink":"https://blog.mariojd.cn/tags/design-patterns/"},{"name":"simple factory","slug":"simple-factory","permalink":"https://blog.mariojd.cn/tags/simple-factory/"},{"name":"抽象工厂","slug":"抽象工厂","permalink":"https://blog.mariojd.cn/tags/抽象工厂/"}]},{"title":"设计模式入门:工厂方法模式","slug":"设计模式入门:工厂方法模式","date":"2018-07-23","updated":"2018-09-29","comments":true,"path":"factory-method-of-design-pattern.html","link":"","permalink":"https://blog.mariojd.cn/factory-method-of-design-pattern.html","excerpt":"","keywords":"","text":"UML类图 代码示例 定义运算枚举 /** * 运算枚举 */public enum TypeEnum { /** * 加 */ ADD, /** * 减 */ SUB,;} 定义抽象产品类,包含一个抽象方法 /** * 抽象产品(计算器) */public interface ICalculator { /** * 计算numberA和numberB并返回操作结果 * * @param numberA * @param numberB * @return 操作结果 */ int compute(int numberA, int numberB);} 产品A,继承了抽象产品类并实现其定义的方法 /** * 具有加法运算功能的计算器 */public class CalculatorAdd implements ICalculator { /** * 将两个数进行加法运算 * * @param numberA * @param numberB * @return 相加结果 */ @Override public int compute(int numberA, int numberB) { return numberA + numberB; }} 产品B,继承了抽象产品类并实现其定义的方法 /** * 具有减法运算功能的计算器 */public class CalculatorSub implements ICalculator { /** * 将两个数进行减法运算 * * @param numberA * @param numberB * @return 相减结果 */ @Override public int compute(int numberA, int numberB) { return numberA - numberB; }} 抽象工厂,用于获取相应的产品 public interface ICalculatorFactory { /** * 获取生产某一功能的计算器工厂实例 * * @return 返回生产某一功能计算器的工厂实例 */ ICalculator productCalculator();} 具体工厂A,实现了获取相关产品的功能 public class CalculatorAddFactory implements ICalculatorFactory { /** * 获取具有加法运算功能的计算器实例 * * @return 返回当前实例 */ @Override public ICalculator productCalculator() { return new CalculatorAdd(); }} 具体工厂B,实现了获取相关产品的功能 public class CalculatorSubFactory implements ICalculatorFactory { /** * 获取具有减法运算功能的计算器实例 * * @return 返回当前实例 */ @Override public ICalculator productCalculator() { return new CalculatorSub(); }} 客户端测试 public class CalculatorMain { public static void main(String[] args) { int numberA = 23; int numberB = 18; // 从抽象工厂获取生产某一功能计算器的工厂实例 ICalculatorFactory calculatorFactory = new CalculatorSubFactory(); // 从当前工厂获取制造某一功能的计算器实例 ICalculator calculator = calculatorFactory.productCalculator(); int result = calculator.compute(numberA, numberB); System.out.println(\"result = \" + result); }} 总结思考  工厂方法同样也是一种创建型模式,是简单工厂模式的进一步抽象和推广。根据开闭原则,在工厂方法模式中新增功能,需要添加相应的功能类和工厂类(对扩展开放,对修改关闭),而具体的实例化则延迟到了工厂子类,并由客户端决定实例化哪一个工厂类。   对比简单工厂,工厂方法把简单工厂内部的逻辑判断移动到了客户端,当添加功能时,原本是要改工厂类的,而现在是修改客户端。 示例代码","categories":[{"name":"设计模式","slug":"设计模式","permalink":"https://blog.mariojd.cn/categories/设计模式/"}],"tags":[{"name":"design patterns","slug":"design-patterns","permalink":"https://blog.mariojd.cn/tags/design-patterns/"},{"name":"simple factory","slug":"simple-factory","permalink":"https://blog.mariojd.cn/tags/simple-factory/"},{"name":"工厂方法","slug":"工厂方法","permalink":"https://blog.mariojd.cn/tags/工厂方法/"}]},{"title":"工程师文化:Chrome 快捷键","slug":"工程师文化:Chrome 快捷键","date":"2018-07-19","updated":"2019-06-02","comments":true,"path":"chrome-shortcut-key-for-engineer-culture.html","link":"","permalink":"https://blog.mariojd.cn/chrome-shortcut-key-for-engineer-culture.html","excerpt":"","keywords":"","text":"  我工作时间不长,加上实习至今(2018.07)也就一年;我没进过大厂,也就意味着没有接受过正规的系统培训;这条路我走着普通的不能再平凡,没感受过优秀的工程师文化。大家都说程序员木讷,但我们也应该有自己的工程师文化,只不过该名词太泛太广不好定义,今天只聊聊Chrome快捷键。以前我是经常用鼠标的,但年后直到现在,从IDE到Chrome再到Other Application,我都尝试用快捷键来办公和娱乐,曾经有人问过,“你怎么不用鼠标?”,那会我不太敢确定,至少是不够熟悉,所以感觉不一定比鼠标点的快,但起码比鼠标用得神奇,至少我是这么理解的。   个人计算机的发展,从早期手工操作机器和CLI,到如今百家齐放的GUI和触摸设备,这期间最多也只不过五十年。如果想了解计算机科学,或者有这方面的学习兴趣,这里推荐一个国外的非常优秀的教学视频:计算机科学速成课。   作为一名软件工程师,在大多数情况下我只使用Chrome来浏览并获取Web资源。Google公司据说有着当今最为优秀的工程师文化,所以他们的产品大多数也是。Smart Phone上使用最多的当属社交类App了,PC作为仍是当今主流的办公设备,浏览器有着非常高频的使用,但大多数人是不完全了解浏览器快捷键的。从个人所理解的工程师文化来说,熟悉和掌握这些快捷键是很有必要的,特别当你是工程师身份,当你渴望变得更加优秀。   Google早就梳理好了各种键盘的快捷键,以帮助大家成为Chrome使用达人,下面是它的学习地址: Chrome 键盘快捷键 Chrome keyboard shortcuts   我把上面的快捷键(Windows)分成了几部分,然后截图并制作成为桌面背景,以帮助我更快的熟悉和掌握这些快捷键,希望这对你会有一定的帮助。   Chrome快捷键不算多,个人感觉熟悉之后效率能提高不少,我之前写过IDEA快捷键系列的文章,下来的计划是接触有“编辑器之神”美誉的Vim,路漫漫其修远兮,吾将上下而求索。","categories":[{"name":"开源","slug":"开源","permalink":"https://blog.mariojd.cn/categories/开源/"}],"tags":[{"name":"chrome","slug":"chrome","permalink":"https://blog.mariojd.cn/tags/chrome/"},{"name":"hot key","slug":"hot-key","permalink":"https://blog.mariojd.cn/tags/hot-key/"},{"name":"shortcuts key","slug":"shortcuts-key","permalink":"https://blog.mariojd.cn/tags/shortcuts-key/"}]},{"title":"设计模式入门:简单工厂模式","slug":"设计模式入门:简单工厂模式","date":"2018-07-18","updated":"2018-09-29","comments":true,"path":"simple-factory-of-design-pattern.html","link":"","permalink":"https://blog.mariojd.cn/simple-factory-of-design-pattern.html","excerpt":"","keywords":"","text":"UML类图 代码示例 定义运算枚举 /** * 运算枚举 */public enum TypeEnum { /** * 加 */ ADD, /** * 减 */ SUB,;} 定义抽象产品类,包含一个抽象方法 /** * 抽象产品(计算器) */public interface ICalculator { /** * 计算numberA和numberB并返回操作结果 * * @param numberA * @param numberB * @return 操作结果 */ int compute(int numberA, int numberB);} 产品A,继承了抽象产品类并实现其定义的方法 /** * 具有加法运算功能的计算器 */public class CalculatorAdd implements ICalculator { /** * 将两个数进行加法运算 * * @param numberA * @param numberB * @return 相加结果 */ @Override public int compute(int numberA, int numberB) { return numberA + numberB; }} 产品B,继承了抽象产品类并实现其定义的方法 /** * 具有减法运算功能的计算器 */public class CalculatorSub implements ICalculator { /** * 将两个数进行减法运算 * * @param numberA * @param numberB * @return 相减结果 */ @Override public int compute(int numberA, int numberB) { return numberA - numberB; }} 工厂类,用于获取相应的产品 /** * 简单工厂 */public class CalculatorFactory { /** * 根据操作指令获取计算器操作对象 * * @param type 操作指令 * @return 具有某一功能的计算器 */ public static ICalculator calculate(TypeEnum type) { ICalculator calculator = null; switch (type) { case ADD: calculator = new CalculatorAdd(); break; case SUB: calculator = new CalculatorSub(); break; default: break; } return calculator; }} 客户端测试 public class CalculatorMain { public static void main(String[] args) { int numberA = 23; int numberB = 18; // 从简单工厂获取任意拥有某一功能的计算器 ICalculator calculate = CalculatorFactory.calculate(TypeEnum.ADD); if (Objects.nonNull(calculate)) { int result = calculate.compute(numberA, numberB); System.out.println(\"result = \" + result); } }} 总结思考  简单工厂是一种创建型模式。根据开闭原则推导,在简单工厂模式应用中新增功能,需要添加相应的子类(对扩展开放,但同时也对修改开放了),并修改核心工厂类的相应方法(增加分支判断),具体的实例化延迟到了客户端进行选择,对于客户端来说,这去除了其与具体产品的直接依赖。 示例代码","categories":[{"name":"设计模式","slug":"设计模式","permalink":"https://blog.mariojd.cn/categories/设计模式/"}],"tags":[{"name":"design patterns","slug":"design-patterns","permalink":"https://blog.mariojd.cn/tags/design-patterns/"},{"name":"simple factory","slug":"simple-factory","permalink":"https://blog.mariojd.cn/tags/simple-factory/"},{"name":"简单工厂","slug":"简单工厂","permalink":"https://blog.mariojd.cn/tags/简单工厂/"}]},{"title":"设计模式入门","slug":"设计模式入门","date":"2018-07-18","updated":"2018-09-29","comments":true,"path":"head-first-design-patterns.html","link":"","permalink":"https://blog.mariojd.cn/head-first-design-patterns.html","excerpt":"","keywords":"","text":"前言  俗话说,好记性也不如烂笔头,最近开始阅读设计模式这方面的书籍,算是借此开个好头,把一些理解的和不太理解的都写下来。本人工作时间不长,经验、资历各方面也还比较欠缺,但目前来说还是很有决心多看一点好书,做好一些事情的。   去年就入手了好几本设计模式类的书籍,有《大话设计模式》、《设计模式之禅》和《Head First设计模式》,不过直到最近也是一次都没翻开过。前不久决心从《大话设计模式》看起,并开始记录这个设计模式入门。 百科设计原则  面向对象编程中一般遵循以下几个原则,设计模式就是为了实现这些原则,从而达到了代码复用、提高可维护性的目的: 开放封闭原则 (OCP, Open Closed Principle) 对扩展开放,对更改封闭 里氏替换原则 (LSP, Liskov Substitution Principle) 子类型必须能够替换掉它们的父类型 依赖倒转原则 (DIP, Dependency Inversion Principle) 依赖抽象而不依赖于具体,高层模块不能依赖低层 接口隔离原则 (ISP, Interface Segregation Principle) 使用多个隔离的接口,比使用单个接口要好,降低类之间的耦合 单一职责原则 (SRP, Single Responsiblity Principle) 就一个类而言,应该只有一个引起它变化的原因 合成/聚合复用原则 (CARP, Composite/Aggregate Reuse Principle) 尽量使用合成/聚合,而不是使用类继承 最小知识原则,也叫迪米特法则 (PLK, Principle of Least Knowledge) 实体应尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立 设计模式  GOF在《Design Patterns: Elements of Reusable Object-Oriented Software》(《设计模式:可复用面向对象软件的基础》)一书中将设计模式划分为三种类型,共计23种: 创建型(5种):单例模式、工厂方法模式、抽象工厂模式、建造者模式、原型模式; 结构型(7种):适配器模式、装饰器模式、桥接模式、组合模式、外观模式、享元模式、代理模式; 行为型(11种):模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、责任链模式、访问者模式。 关系图","categories":[{"name":"设计模式","slug":"设计模式","permalink":"https://blog.mariojd.cn/categories/设计模式/"}],"tags":[{"name":"design patterns","slug":"design-patterns","permalink":"https://blog.mariojd.cn/tags/design-patterns/"}]},{"title":"Spring Boot 中读取配置属性的几种方式","slug":"Spring Boot 中读取配置属性的几种方式","date":"2018-07-12","updated":"2019-06-02","comments":true,"path":"several-ways-to-read-configuration-properties-in-spring-boot.html","link":"","permalink":"https://blog.mariojd.cn/several-ways-to-read-configuration-properties-in-spring-boot.html","excerpt":"","keywords":"","text":"前言  本文介绍Spring Boot中读取配置属性的几种方式,项目示例中用到的application.yml和application.properties定义如下: @Value  @Value是比较常见的注入方式,功能强大但一般可读性较差。 @Value(\"str\")private String str; // 注入普通字符串 @Value(\"${hello}\")private String hello; // 注入配置属性@Value(\"#{systemProperties['os.name']}\")private String systemPropertiesName; // 注入操作系统属性@Value(\"#{ T(java.lang.Math).random() * 100.0 }\")private double randomNumber; //注入表达式结果@Value(\"#{userBean.name}\")private String name; // 注入Bean属性   下面通过@Value注解获取定义在配置文件的属性值: @SpringBootApplicationpublic class AttributeApplication { private static final String SPRING_BOOT_HELLO = \"spring-boot.hello\"; @Value(\"${\" + SPRING_BOOT_HELLO + \"}\") private String hello; /** * 1. 通过@Value注解获取值 */ public void getAttrByValueAnnotation() { System.out.println(\"1. 通过@Value注解获取值: \" + hello); } public static void main(String[] args) { ConfigurableApplicationContext applicationContext = SpringApplication.run(AttributeApplication.class, args); AttributeApplication bean = applicationContext.getBean(AttributeApplication.class); bean.getAttrByValueAnnotation(); } }   扩展说明: @SpringBootApplication public class AttributeApplication { private static final String SPRING_BOOT_STR_ARRAY = \"spring-boot.str-array\"; private static final String SPRING_BOOT_INT_ARRAY = \"spring-boot.int-array\"; /** * Attention : it is error if use Integer[] */ @Value(\"${\" + SPRING_BOOT_INT_ARRAY + \"}\") private int[] array; /** * 通过@Value注解获取数组 */ public void getArrayAttr() { System.out.println(\"5. 通过@Value注解获取数组: \" + Arrays.toString(array)); } @Value(\"#{'${\" + SPRING_BOOT_STR_ARRAY + \"}'.split(',')}\") private List<String> list; /** * 通过@Value注解获取List */ public void getListAttr() { System.out.println(\"6. 通过@Value注解获取List: \" + list.toString()); } public static void main(String[] args) { ConfigurableApplicationContext applicationContext = SpringApplication.run(AttributeApplication.class, args); AttributeApplication bean = applicationContext.getBean(AttributeApplication.class); bean.getArrayAttr(); bean.getListAttr(); } } Environment  通过注入获取Environment对象,然后再获取定义在配置文件的属性值: @SpringBootApplicationpublic class AttributeApplication { private static final String SPRING_BOOT_HELLO = \"spring-boot.hello\"; @Resource private Environment environment; /** * 2. 通过注入Environment获取值 */ public void getAttrByEnvironment() { String property = environment.getProperty(SPRING_BOOT_HELLO); System.out.println(\"2-1. 通过注入Environment获取值: \" + property); } public static void main(String[] args) { ConfigurableApplicationContext applicationContext = SpringApplication.run(AttributeApplication.class, args); AttributeApplication bean = applicationContext.getBean(AttributeApplication.class); bean.getAttrByEnvironment(); }}   还可以在启动类中通过ApplicationContext获取Environment对象后再获取值: @SpringBootApplicationpublic class AttributeApplication { private static final String UNDEFINED = \"undefined\"; private static final String SPRING_BOOT_HELLO = \"spring-boot.hello\"; public static void main(String[] args) { ConfigurableApplicationContext applicationContext = SpringApplication.run(AttributeApplication.class, args); System.out.println(\"2-2. 通过ApplicationContext获取Environment后再获取值: \" + applicationContext .getEnvironment().getProperty(SPRING_BOOT_HELLO, UNDEFINED)); }} @ConfigurationProperties  @ConfigurationProperties作用在类上,用于注入Bean属性,然后再通过当前Bean获取注入值: @SpringBootApplicationpublic class AttributeApplication { private static final String APPLICATION_YML = \"application.yml\"; private static final String SPRING_BOOT_PREFIX = \"spring-boot\"; @Data @Component @PropertySource(\"classpath:\" + APPLICATION_YML) @ConfigurationProperties(prefix = SPRING_BOOT_PREFIX) class Attribute { private String hello; private String world; } @Resource private Attribute attribute; /** * 3. 通过@ConfigurationProperties注入对象属性获取 */ public void getAttrByConfigurationPropertiesAnnotation() { System.out.println(\"3. 通过@ConfigurationProperties注入对象属性获取: \" + attribute); } public static void main(String[] args) { ConfigurableApplicationContext applicationContext = SpringApplication.run(AttributeApplication.class, args); AttributeApplication bean = applicationContext.getBean(AttributeApplication.class); bean.getAttrByConfigurationPropertiesAnnotation(); } } PropertiesLoaderUtils@SpringBootApplicationpublic class AttributeApplication { private static final String UNDEFINED = \"undefined\"; private static final String APPLICATION_PROPERTIES = \"application.properties\"; private static final String SPRING_BOOT_HELLO = \"spring-boot.hello\"; /** * 4. 通过PropertiesLoaderUtils获取(注意,此工具类仅可处理.properties或.xml配置文件) */ public void getAttrByPropertiesLoaderUtils() { try { ClassPathResource resource = new ClassPathResource(APPLICATION_PROPERTIES); Properties properties = PropertiesLoaderUtils.loadProperties(resource); String property = properties.getProperty(SPRING_BOOT_HELLO, UNDEFINED); System.out.println(\"4. 通过PropertiesLoaderUtils获取: \" + property); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { ConfigurableApplicationContext applicationContext = SpringApplication.run(AttributeApplication.class, args); AttributeApplication bean = applicationContext.getBean(AttributeApplication.class); bean.getAttrByPropertiesLoaderUtils(); }} 说明  源码地址","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"https://blog.mariojd.cn/tags/Spring-Boot/"},{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/tags/Java/"}]},{"title":"基于 Docker 的 Redis 高可用集群搭建(redis-sentinel)","slug":"基于 Docker 的 Redis 高可用集群搭建(redis-sentinel)","date":"2018-07-04","updated":"2019-06-02","comments":true,"path":"construction-of-redis-high-availability-cluster-based-on-docker.html","link":"","permalink":"https://blog.mariojd.cn/construction-of-redis-high-availability-cluster-based-on-docker.html","excerpt":"","keywords":"","text":"前言  之前介绍了用docker来搭建redis主从环境,但这只是对数据添加了从库备份(主从复制),当主库down掉的时候,从库是不会自动升级为主库的,也就是说,该redis主从集群并非是高可用的。  目前来说,高可用(主从复制、主从切换)redis集群有两种方案,一种是redis-sentinel,只有一个master,各实例数据保持一致;一种是redis-cluster,也叫分布式redis集群,可以有多个master,数据分片分布在这些master上。  本文介绍基于docker和redis-sentinel的高可用redis集群搭建,大多数情况下,redis-sentinel也需要做高可用,这里先对redis搭建一主二从环境,另外需要3个redis-sentinel监控redis master。   很显然,只使用单个redis-sentinel进程来监控redis集群是不可靠的,由于redis-sentinel本身也有single-point-of-failure-problem(单点问题),当出现问题时整个redis集群系统将无法按照预期的方式切换主从。官方推荐:一个健康的集群部署,至少需要3个Sentinel实例。另外,redis-sentinel只需要配置监控redis master,而集群之间可以通过master相互通信。 redis-sentinel  redis-sentinel作为独立的服务,用于管理多个redis实例,该系统主要执行以下三个任务: 监控 (Monitor): 检查redis主、从实例是否正常运作 通知 (Notification): 监控的redis服务出现问题时,可通过API发送通知告警 自动故障迁移 (Automatic Failover): 当检测到redis主库不能正常工作时,redis-sentinel会开始做自动故障判断、迁移等操作,先是移除失效redis主服务,然后将其中一个从服务器升级为新的主服务器,并让失效主服务器的其他从服务器改为复制新的主服务器。当客户端试图连接失效的主服务器时,集群也会向客户端返回最新主服务器的地址,使得集群可以使用新的主服务器来代替失效服务器 环境说明 Docker Ubuntu/CentOS Redis v4.0.10 sentinel.conf  sentinel.conf是启动redis-sentinel的核心配置文件,可以从官网下载:wget http://download.redis.io/redis-stable/sentinel.conf 一主二从  先搭建好Redis一主二从环境,这里仅给出操作过程,可以参考之前写的《Docker + Redis (4.0.10) 主从环境搭建》,在master上使用info Replication查看集群状态(注意,为了让redis-sentinel可以发现slave,这里要确保redis服务端口和容器映射端口一致): # 主库docker run -it --name redis-master -d -p 6300:6300 redis redis-server --requirepass redispassword --port 6300docker exec -it redis-master bashredis-cli -a redispassword -p 6300config set masterauth redispassword# 从库1docker run -it --name redis-slave -d -p 6301:6301 redis redis-server --requirepass redispassword --port 6301docker exec -it redis-slave bashredis-cli -a redispassword -p 6301slaveof <master-ip> <master-port>config set masterauth redispassword# 从库2docker run -it --name redis-slave2 -d -p 6302:6302 redis redis-server --requirepass redispassword --port 6302docker exec -it redis-slave2 bashredis-cli -a redispassword -p 6302slaveof <master-ip> <master-port>config set masterauth redispassword 配置  根据上面下载好的sentinel.conf,找到并修改如下配置: # mymaster:自定义集群名,如果需要监控多个redis集群,只需要配置多次并定义不同的<master-name> <master-redis-ip>:主库ip <master-redis-port>:主库port <quorum>:最小投票数,由于有三台redis-sentinel实例,所以可以设置成2sentinel monitor mymaster <master-redis-ip> <master-redis-port> <quorum># 注:redis主从库搭建的时候,要么都不配置密码(这样下面这句也不需要配置),不然都需要设置成一样的密码sentinel auth-pass mymaster redispassword# 添加后台运行daemonize yes   将上面的sentinel.conf复制三份,分别为sentinel1.conf,sentinel2.conf和sentinel3.conf,再次编辑修改port为26000,26001和26002。 启动  redis-sentinel启动有以下两种方式: redis-sentinel /path/to/sentinel.conf redis-server /path/to/sentinel.conf --sentinel   大多数版本的redis都支持以上两种方式启动。实战中,为了让redis-sentinel作为独立的服务运行,这里用docker搭建环境: # redis-sentinel实例1docker run -it --name redis-sentinel1 -v /root/redis/sentinel1.conf:/usr/local/etc/redis/sentinel.conf -d redis /bin/bash# redis-sentinel实例2docker run -it --name redis-sentinel2 -v /root/redis/sentinel2.conf:/usr/local/etc/redis/sentinel.conf -d redis /bin/bash# redis-sentinel实例3docker run -it --name redis-sentinel3 -v /root/redis/sentinel3.conf:/usr/local/etc/redis/sentinel.conf -d redis /bin/bash   分别进入以上三个容器启动redis-sentinel: docker exec -it redis-sentinel(x) bash# 或redis-server /usr/local/etc/redis/sentinel.conf --sentinelredis-sentinel /usr/local/etc/redis/sentinel.conf   连接并使用redis-sentinel API查看监控状况: redis-cli -p 26000 (26001 | 26002)sentinel master mymaster 或 sentinel slaves mymaster 测试  进入redis-master容器,休眠60秒redis服务: docker exec -it redis-master bashredis-cli -a redispassword -p 6300 DEBUG sleep 60   进入redis-slave或redis-slave2容器,查看info Replication,可以看到master已经完成了切换。   60秒后原redis主库恢复服务,但降级后当前redis服务已无法恢复原主库身份。 参考 Docker化高可用redis集群Redis Sentinel DocumentationRedis Sentinel 机制与用法(4.0.0版本)Redis practise(二)使用Docker部署Redis高可用,分布式集群","categories":[{"name":"Docker","slug":"Docker","permalink":"https://blog.mariojd.cn/categories/Docker/"}],"tags":[{"name":"docker","slug":"docker","permalink":"https://blog.mariojd.cn/tags/docker/"},{"name":"redis-sentinel","slug":"redis-sentinel","permalink":"https://blog.mariojd.cn/tags/redis-sentinel/"}]},{"title":"Docker + Redis 主从环境搭建","slug":"Docker + Redis 主从环境搭建","date":"2018-07-02","updated":"2018-09-29","comments":true,"path":"redis-master-and-slave-environment-construction-based-docker.html","link":"","permalink":"https://blog.mariojd.cn/redis-master-and-slave-environment-construction-based-docker.html","excerpt":"","keywords":"","text":"环境说明 Docker Ubuntu/CentOS Redis v4.0.10 redis.conf  redis.conf是Redis的核心配置文件,默认docker运行的redis是不存在配置文件的,这里可以先从官网下载:wget http://download.redis.io/redis-stable/redis.conf   下面分别介绍是否指定redis.conf来搭建Redis主从。 不指定redis.conf1. 运行Redis1.1 master(主库)# 运行服务docker run -it --name redis-master -d -p 6300:6379 redis redis-server --requirepass masterpassword# 测试连接redisdocker exec -it redis-master redis-cli -a <master-password> 1.2 slave(从库)# 运行服务docker run -it --name redis-slave -d -p 6301:6379 redis redis-server --requirepass slavepassword # 设定从库密码,可选# 测试连接redisdocker exec -it redis-slave redis-cli# 进行密码认证auth <slave-password> 2. 主从连接及查看2.1 从库配置  slaveof <master-ip> <master-port>。<master-ip>为主库服务ip,<master-port>表示主库所在端口,默认6379 2.2 密码认证  config set masterauth <master-password>。<master-password>即为主库访问密码 2.3 测试命令  输入info或info Replication 指定redis.conf  将上面下载好的redis.conf复制,分别为redis-master.conf和redis-slave.conf,找到指定配置并修改如下: redis-master.conf # bind 127.0.0.1 # 注释当前行,表示任意ip可连daemonize yes # 让redis服务后台运行requirepass masterpassword # 设定密码 redis-slave.conf # bind 127.0.0.1daemonize yesrequirepass slavepassword # 从库密码,可选配置# <masterip>表示主库所在的ip,而<masterport>则表示主库启动的端口,默认是6379slaveof <masterip> <masterport># 主库有密码必需要配置,<master-password>代表主库的访问密码masterauth <master-password> 1. 运行Redis1.1 master(主库)# 运行服务docker run -it --name redis-master -v /root/redis/redis-master.conf:/usr/local/etc/redis/redis.conf -d -p 6300:6379 redis /bin/bash# 进入容器docker exec -it redis-master bash# 加载配置redis-server /usr/local/etc/redis/redis.conf# 测试连接redis-cli -a <master-password> 1.2 slave(从库)# 运行服务docker run -it --name redis-slave -v /root/redis/redis-slave.conf:/usr/local/etc/redis/redis.conf -d -p 6301:6379 redis /bin/bash# 进入容器docker exec -it redis-slave bash# 加载配置redis-server /usr/local/etc/redis/redis.conf# 测试连接redis-cli# 密码认证auth <slave-password> 2. 主从查看  输入info或info Replication 相关链接 Redis commandsRedis 命令参考Docker环境搭建redis集群(主从模式)","categories":[{"name":"Docker","slug":"Docker","permalink":"https://blog.mariojd.cn/categories/Docker/"},{"name":"Redis","slug":"Docker/Redis","permalink":"https://blog.mariojd.cn/categories/Docker/Redis/"}],"tags":[{"name":"docker","slug":"docker","permalink":"https://blog.mariojd.cn/tags/docker/"},{"name":"主从搭建","slug":"主从搭建","permalink":"https://blog.mariojd.cn/tags/主从搭建/"},{"name":"redis","slug":"redis","permalink":"https://blog.mariojd.cn/tags/redis/"}]},{"title":"解决百度蜘蛛无法爬取 Hexo 博客的问题","slug":"解决百度蜘蛛无法爬取 Hexo 博客的问题","date":"2018-06-28","updated":"2019-06-02","comments":true,"path":"solve-the-problem-that-baidu-spiders-are-unable-to-crawl-hexo-blogs.html","link":"","permalink":"https://blog.mariojd.cn/solve-the-problem-that-baidu-spiders-are-unable-to-crawl-hexo-blogs.html","excerpt":"","keywords":"","text":"写在前面  先上两张图对比下:   由于GitHub Pages是拒绝百度爬虫爬取的,包括用Hexo或Jekyll搭建的博客,因此你的站点再怎么SEO优化,这在国内也是收录和搜索不到的。本文介绍的方案同大多数一样,利用DNS将国内的线路请求到Coding Pages,然后国外的线路还是走GitHub Pages。   上面是在完成以下几步操作几天后截的图,总之谷歌是很快就收录了我的博客站点(听说最多一两天),相反国内的百度是迟迟没有动静,上站长平台各种地址、sitemap提交也是毫无反馈,还是提问后才了解到百度收录站点的速度确实会比较慢,短则一两周长则几个月,还是先做好时间、心理的持久战准备吧! 准备说明 Coding DNSPod 解决过程 在Coding.net上创建当前用户名仓库 修改hexo根目录下的_config.yml # 注意这里要修改成你的实际仓库地址,都是走SSH的方式所以要提前配置相关环境# 如果实际分支不是默认的master,请自行修改并在平台上配置deploy: type: git repo: github: git@github.com:happyjared/happyjared.github.io.git,master coding: git@git.coding.net:happyjared/happyjared.git,master 注册DNSPod并修改域名解析   这里需要两条CNAME主机记录。线路类型是国外的,记录值为:\\.github.io.;线路类型是国内的,记录值为:pages.coding.me.(注意,对应静态Pages来说这是个固定值)。 重新部署Hexo并测试访问   完成上述操作后使用hexo g -d命令重新部署双平台博客,然后通过访问http://<github username>.github.io或http://<coding username>.coding.me查看操作是否成功。 添加自定义域名和Https GitHub Pages的配置在该仓库的Settings,参考下图: Coding Pages的配置在当前仓库的Pages服务,如下图所示:   提示:关于Coding Page的SSL/TLS证书申请 ,主要就是在DNSPod平台上正确的配置解析记录,如果出现验证错误,请参考Coding Pages常见问题。当时我遇到的错误信息是:Fetching http://exmaple.com/.well-known/acme-challenge/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx: xxxxxxxx,解决方案是先将上述CNAME记录值为pages.coding.me.的线路类型修改为默认,待验证通过后再调整为国内。 关于Coding Pages跳转页   参考Coding.net官网说明,可以配置文字版或图片版,然后验证并解决。我这里使用的是文字版方案,修改了hexo主题layout\\_partial目录下的footer.ejs。 验证方案   完成上述操作后,百度蜘蛛爬虫应该就可以成功抓取我们的站点了,下面列出两种测试方案。 模拟百度爬虫请求站点curl -A "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)" https://blog.mariojd.cn 通过百度站长平台抓取验证 验证地址:抓取诊断 - 百度站长 站长平台收录及验证  为了加快站点收录,可以通过站长平台(先验证)主动提交站点sitemap,这里可以通过hexo插件生成sitemap,参考如下: npm install hexo-generator-sitemap // 这是传统的sitemap npm install hexo-generator-baidu-sitemap //这是百度专有的sitemap   安装完插件后,需要在Hexo根目录下配置_config.yml,使用hexo g可以在public目录下看到生成的sitemap.xml文件,最后hexo d部署到远程仓库即可: sitemap: path: sitemap.xmlbaidusitemap: path: baidusitemap.xml 谷歌收录 谷歌站长平台 添加站点后,在“抓取”->“站点地图”中提交sitemap 百度收录 百度站长平台 添加站点后,在“数据引入”->“链接提交”中提交sitemap 保险起见,可以在每个页面上嵌入百度的自动推送工具代码: <script>(function(){ var bp = document.createElement('script'); var curProtocol = window.location.protocol.split(':')[0]; if (curProtocol === 'https') { bp.src = 'https://zz.bdstatic.com/linksubmit/push.js'; } else { bp.src = 'http://push.zhanzhang.baidu.com/push.js'; } var s = document.getElementsByTagName(\"script\")[0]; s.parentNode.insertBefore(bp, s);})();</script> 收录验证  通过浏览器输入site:站点并搜索 site:https://blog.mariojd.cn 参考链接 解决 Github Pages 禁止百度爬虫的方法2–从gitcafe迁移到coding.net迁移 Github pages 到 coding.nethexo干货系列:(四)将hexo博客同时托管到github和codinghexo干货系列:(六)hexo提交搜索引擎(百度+谷歌Hexo 博客 Coding+Github 双线部署求 https 方案","categories":[{"name":"开源","slug":"开源","permalink":"https://blog.mariojd.cn/categories/开源/"}],"tags":[{"name":"hexo","slug":"hexo","permalink":"https://blog.mariojd.cn/tags/hexo/"},{"name":"coding.net","slug":"coding-net","permalink":"https://blog.mariojd.cn/tags/coding-net/"}]},{"title":"Hexo,添加标题翻译插件","slug":"Hexo,添加标题翻译插件","date":"2018-06-28","updated":"2018-09-29","comments":true,"path":"add-the-title-translation-plugin-for-hexo.html","link":"","permalink":"https://blog.mariojd.cn/add-the-title-translation-plugin-for-hexo.html","excerpt":"","keywords":"","text":"文章背景  hexo生成的默认文章链接格式是这样的:https://blog.mariojd.cn/2013/07/14/<Markdown file name>/,这个配置在hexo根目录下的_config.yml里面:permalink: :year/:month/:day/:title/,这种默认的配置缺点很明显,当文件名是中文的时候url链接里就有中文出现,看起来low的同时也非常不利于seo优化,下面介绍两种解决方案。 方案一:添加urlname属性(手动)  在md文件的Front-matter区域新增urlname属性,可以是文章的英文Title也可以是其它自定义标识,所以每次编写Markdown你都得这么做,参考如下: ---title: 解决百度蜘蛛无法爬取Hexo博客的问题urlname: solve-the-problem-that-baidu-spider-cannot-crawl-hexo-blogdate: 2018-6-28 categories: hexoauthor: Jared Qiutags: hexocover_picture: http://xxx.xx/xxx.jpgtop: 1--- 方案二:利用hexo插件(自动)  在hexo plugins搜索“link”,已经有几个插件可以将url转换成不同的格式(如下),我这里选用了hexo-translate-title,也是觉得这个比较适合自身风格。 hexo-translate-title: Translate the chinese title of Hexo blog to english words automatially hexo-permalink-pinyin: A Hexo plugin which convert Chinese title to transliterate permalink. hexo-abbrlink: Auto create one and only link for every post for hexo hexo-number-title: The hexo blog post url is displayed as a number.   下面简单介绍一下安装操作过程。 安装// 用cnpm会比较好npm install hexo-translate-title --save 配置  修改hexo根项目下的_config.yml,建议用google翻译毕竟贴地气,但实测并不稳定,所以也可以用baidu_no_appid,这样也不需要额外的认证配置。 translate_title: translate_way: google # google,youdao,baidu_with_appid,baidu_no_appid is_need_proxy: false # true | false # proxy_url: http://localhost:50018 # Your proxy_url # youdao_api_key: '' # Your youdao_api_key # youdao_keyfrom: xxxx-blog # Your youdao_keyfrom # baidu_appid: '' # Your baidu_appid # baidu_appkey: '' # Your baidu_appkey # 修改原链接格式:permalink: :year/:month/:day/:title/permalink: :year:month:day/:translate_title.html 测试  用hexo d重新生成,留意控制台输出以及public目录下是否有生成相应的<hexo-translate-title>.html文件,如果有代表成功了,再用hexo d部署即可。 记坑  按上述流程安装、配置和部署,但生成的链接都是undefined.html,随后给作者提issue,回复后留意hexo g输出,并且看了源码,用hexo config查看配置信息后未发现问题,最终定位为Google翻译服务的不稳定。 相关链接hexo-translate-title - GitHubhexo-abbrlink介绍hexo链接持久化终极解决之道hexo-abbrlink - GitHub","categories":[{"name":"hexo","slug":"hexo","permalink":"https://blog.mariojd.cn/categories/hexo/"}],"tags":[{"name":"hexo","slug":"hexo","permalink":"https://blog.mariojd.cn/tags/hexo/"},{"name":"hexo-translate-title","slug":"hexo-translate-title","permalink":"https://blog.mariojd.cn/tags/hexo-translate-title/"}]},{"title":"Vimium - 让 Chrome 高效工作的神器","slug":"Vimium - 让 Chrome 高效工作的神器","date":"2018-06-27","updated":"2019-06-02","comments":true,"path":"vimium-that-allows-chrome-to-work-efficiently.html","link":"","permalink":"https://blog.mariojd.cn/vimium-that-allows-chrome-to-work-efficiently.html","excerpt":"","keywords":"","text":"What’s Vimium  开源是一种精神,So分享也可以算是,何况这是好东西呢。说重点,那Vimium是什么?Vimium is a Chrome extension that provides keyboard-based navigation and control of the web in the spirit of the Vim editor.(Vimium是一款基于Chrome的扩展程序(插件),它以Vim编辑器的精神提供了基于键盘的导航和Web控制),通俗点就是可以让我们像用VIM(快捷键)一样来高效使用Chrome(理论上基于Chromium开发的浏览器都支持)。 Install Vimium 方法1:下载Vimium CRX扩展程序文件并安装(下方有下载地址) 方法2:进入Chrome Store搜索“Vimium”添加(请自行准备梯子) About Vimium   支持自定义快捷键,但多数默认快捷键已经符合VIM习惯了。Vimium的快捷键很多,即使一个个列举说明也没多大作用,最好的方式还是多查多用,熟能生巧。怎么查呢,这是最重要的Show help快捷键:?,可以快速调出如下图所示。 Related LinksVimium - GitHubVimium_v1.63.3.crx - download","categories":[{"name":"开源","slug":"开源","permalink":"https://blog.mariojd.cn/categories/开源/"}],"tags":[{"name":"vimium","slug":"vimium","permalink":"https://blog.mariojd.cn/tags/vimium/"},{"name":"chrome plugin","slug":"chrome-plugin","permalink":"https://blog.mariojd.cn/tags/chrome-plugin/"}]},{"title":"YApi - 高效、易用、功能强大的可视化接口管理平台","slug":"YApi - 高效、易用、功能强大的可视化接口管理平台","date":"2018-06-26","updated":"2018-09-29","comments":true,"path":"efficient-easily-powerful-visual-interface-management-platform-about-yapi.html","link":"","permalink":"https://blog.mariojd.cn/efficient-easily-powerful-visual-interface-management-platform-about-yapi.html","excerpt":"","keywords":"","text":"YApi  YApi是由去哪儿网移动架构组(简称YMFE,一群由FE、iOS和Android工程师共同组成的最具想象力、创造力和影响力的大前端团队)开源的可视化接口管理工具,一个可本地部署的、打通前后端及QA的接口管理平台,YApi旨在为开发、产品和测试人员提供更优雅的接口管理服务,可以帮助开发者轻松创建、发布和维护不同项目,不同平台的API。有了YApi,我们可以很方便的测试、管理和维护多个项目的API接口,不像Swagger那样是随应用生和灭的(且线上环境下大多数须关闭),YApi是一个独立的服务平台。 安装YApi1. 环境说明 Centos7 NodeJS v7.6+ MongoDB v2.6+ Git (可选) 2. NodeJS安装  安装这个遇到点坑,因为系统是CentOS的,所以一开始很自然用上了yum -y install nodejs,没想到安装出来的node版本太低了,无奈又用命令yum remove -y nodejs npm卸载掉并重新安装,之后找到了Node官网的安装说明 ,参考操作如下: sudo yum install gcc-c++ makecurl -sL https://dl.yarnpkg.com/rpm/yarn.repo | sudo tee /etc/yum.repos.d/yarn.reposudo yum install yarncurl --silent --location https://rpm.nodesource.com/setup_10.x | sudo bash -sudo yum -y install nodejs 3. MongoDB安装  为了简便,这里用docker运行mongo服务。 // 1.运行mongo服务,因为是测试所以也不设置用户名密码了docker run -d --name yapi-mongo -p 27017:27017 mongo// 2.进入mongo服务docker exec -it yapi-mongo bash// 3.进入mongo交互命令环境mongo// 4.创建名为yapi的DB,这个在可视化部署用得上use yapi// 5.查看所有DBshow dbs 4.1 可视化部署及启动[推荐]// 1. 用npm安装yapi客户端 npm install -g yapi-cli --registry https://registry.npm.taobao.org// 2. 启动客户端安装服务 yapi server   执行完第2步的yapi server后系统将自动运行可视化部署程序(http://localhost:9090),输入相应的配置并点击开始部署。   部署完成之后,根据部署日志提示信息切换到部署目录,输入指令node vendors/server/app.js启动服务器。 4.2 命令行部署及启动[此方法请自行测试]mkdir yapicd yapigit clone https://github.com/YMFE/yapi.git vendors //或者下载 zip 包解压到 vendors 目录cp vendors/config_example.json ./config.json //复制完成后请修改相关配置cd vendorsnpm install --production --registry https://registry.npm.taobao.orgnpm run install-server //安装程序会初始化数据库索引和管理员账号,管理员账号名可在 config.json 配置node server/app.js //启动服务器后,请访问 127.0.0.1:{config.json配置的端口},初次运行会有个编译的过程,请耐心等候 使用YApi  如果部署的时候使用的是默认端口配置,那网站访问的地址就是:http://localhost:3000 ,初始管理员账号:admin@admin.com,密码:ymfe.org(可在个人中心修改)。   具体的操作和使用请查看官方文档:YApi-教程,包括Mock测试、自动化测试、数据导入导出等都有详细的介绍。 相关链接YApi - 官网YApi - GitHub","categories":[{"name":"开源","slug":"开源","permalink":"https://blog.mariojd.cn/categories/开源/"},{"name":"YApi","slug":"开源/YApi","permalink":"https://blog.mariojd.cn/categories/开源/YApi/"},{"name":"Swagger","slug":"开源/YApi/Swagger","permalink":"https://blog.mariojd.cn/categories/开源/YApi/Swagger/"}],"tags":[{"name":"swagger","slug":"swagger","permalink":"https://blog.mariojd.cn/tags/swagger/"},{"name":"yapi","slug":"yapi","permalink":"https://blog.mariojd.cn/tags/yapi/"}]},{"title":"Spring Boot 集成 Swagger 简易教程","slug":"Spring Boot 集成 Swagger 简易教程","date":"2018-06-25","updated":"2019-06-02","comments":true,"path":"spring-boot-integrated-swagger-simple-tutorial.html","link":"","permalink":"https://blog.mariojd.cn/spring-boot-integrated-swagger-simple-tutorial.html","excerpt":"","keywords":"","text":"Swagger   Swagger号称是史上最流行的、最好用的API接口文档构建工具,它支持多种语言包括Java在内,本文仅关注如何使用Spring Boot来集成Swagger,更多关于Swagger的介绍可以查看以下几个链接。 Swagger - 官网Swagger - Github SpringFox  SpringFox最初叫Swagger-SpringMVC,从字面意义上简单来理解是使用了SpringMVC来集成Swagger,后来演变成SpringFox这么一个项目(或组织),SpringFox官网有这么一句:Automated JSON API documentation for API’s built with Spring(针对Spring构建的API的自动化JSON API文档)。好了,下来我们只需用SpringFox提供的三方库来快速集成一下Spring Boot和Swagger。 SpringFoxSpringFox - Documentation 1. 添加Maven依赖<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>${latest version}</version> </dependency> <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>${latest version}</version> </dependency> 2. 开启Swagger  在Spring Boot启动类上添加@EnableSwagger2即可。 @SpringBootApplication@EnableSwagger2 //开启Swaggerpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }} 3. 配置Swagger   @Configurationpublic class SwaggerConfig { @Bean public Docket docket() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() // 这里是全局扫描有@Api注解得类,还可以扫描任意位置,指定包以及针对方法上的指定注解 .apis(RequestHandlerSelectors.withClassAnnotation(Api.class)) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title(\"Title\") .description(\"Description\") .termsOfServiceUrl(\"\") .contact(new Contact(\"\", \"\", \"\")) .license(\"\") .licenseUrl(\"\") .version(\" xxx \") .build(); }} 4. 运行效果  启动Spring Boot后,可以点击查看(更改为你的本地地址) http://localhost:8080/swagger-ui.html#/ ,效果如下: 5. 常用注解  Swagger的所有注解定义在io.swagger.annotations包下,下面列一些经常用到的,未列举出来的可以另行查阅说明: Swagger注解 简单说明 @Api(tags = “xxx模块说明”) 作用在模块类上 @ApiOperation(“xxx接口说明”) 作用在接口方法上 @ApiModel(“xxxPOJO说明”) 作用在模型类上:如VO、BO @ApiModelProperty(value = “xxx属性说明”,hidden = true) 作用在类方法和属性上,hidden设置为true可以隐藏该属性 @ApiParam(“xxx参数说明”) 作用在参数、方法和字段上,类似@ApiModelProperty 6. 使用Swagger  完全以上几小步配置后,再次打开swagger-ui界面就可以进行测试了,相较于传统的Postman或Curl方式测试接口,使用swagger简直就是傻瓜式操作,不需要额外说明文档(写得好本身就是文档)而且更不容易出错,只需要录入数据然后点击Execute,如果再配合自动化框架,可以说基本就不需要人为操作了。 End  Swagger是个优秀的工具,现在国内已经有很多的中小型互联网公司都在使用它,相较于传统的要先出Word接口文档再测试的方式,显然这样也更符合现在的快速迭代开发行情。当然了,提醒下大家在正式环境要记得关闭Swagger,一来出于安全考虑二来也可以节省运存。之前看到过一篇深入Swagger原理的文章,最后分享出来给大家:API管理工具Swagger介绍及Springfox原理分析。","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"},{"name":"Spring Boot","slug":"Java/Spring-Boot","permalink":"https://blog.mariojd.cn/categories/Java/Spring-Boot/"},{"name":"Swagger","slug":"Java/Spring-Boot/Swagger","permalink":"https://blog.mariojd.cn/categories/Java/Spring-Boot/Swagger/"}],"tags":[{"name":"spring boot","slug":"spring-boot","permalink":"https://blog.mariojd.cn/tags/spring-boot/"},{"name":"swagger","slug":"swagger","permalink":"https://blog.mariojd.cn/tags/swagger/"}]},{"title":"Docker + MySQL 主从环境搭建","slug":"Docker + MySQL 主从环境搭建","date":"2018-06-22","updated":"2018-09-29","comments":true,"path":"mysql-master-and-slave-environment-construction-based-docker.html","link":"","permalink":"https://blog.mariojd.cn/mysql-master-and-slave-environment-construction-based-docker.html","excerpt":"","keywords":"","text":"环境说明 Docker Ubuntu/CentOS MySQL v8.0.11 1. 配置my.cnf  my.cnf(部分老版本可能是my.ini)是MySQL核心配置文件。首先,在任意挂载目录下新建*.cnf文件(这里的*代表可以是任意的文件名称)。如果你的mysql是下载安装的,请找到my.cnf并参考如下配置:[mysqld]log-bin=mysql-bin //启用二进制日志server_id=xxx //xxx代表唯一ID,默认是1。特别注意,当前版本这里是server_id而不是server-id,有些版本可能会不一样 实战示例 在/root/mysql/conf/master/新建master.cnf,配置如下: [mysqld]log-bin=mysql-binserver_id=100 在/root/mysql/conf/slave/新建slave.cnf,添加如下: [mysqld]log-bin=mysql-binserver_id=200 2. 运行MySQL2.1 master(主库)docker run --name mysql-master -v /root/mysql/conf/master:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=root -p 10000:3306 -d mysql --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci 2.2 slave(从库)docker run --name mysql-slave -v /root/mysql/conf/slave:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=root -p 10001:3306 -d mysql --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci   注意,当前mysql版本是8.0.11,不同的版本之间可能存在差别,docker可以拉取指定版本的mysql Image,命令:docker pull mysql:8.0.11 3. 新建账号3.1 在 master 上创建复制账号// 1.进入主库容器docker exec -it mysql-master bash// 2.登录mysqlmysql -uroot -proot// 3.rep表示复制账号;<salve_ip>可修改为%,代表任意的主机;`IDENTIFIED BY`后面代表rep用户的认证密码# CREATE USER 'rep'@'<salve_ip>' IDENTIFIED WITH mysql_native_password BY 'reppassword';CREATE USER 'rep'@'<salve_ip>' IDENTIFIED BY 'reppassword';GRANT REPLICATION SLAVE ON *.* TO 'rep'@'<salve_ip>'; 3.2 获取并记录File和Position信息  执行show master status;即可 4. 配置连接 配置 slave 连接 master// 1.进入从库容器docker exec -it mysql-slave bash// 2.登录mysqlmysql -uroot -proot// 3.配置主库认证信息,<File>和<Position>同上CHANGE MASTER TO MASTER_HOST='<master_ip>',MASTER_PORT=10000,MASTER_USER='rep',MASTER_PASSWORD='reppassword',MASTER_LOG_FILE='<File>',MASTER_LOG_POS=<Position>; 5. 开启同步 开启同步   在 slave 上启动线程:start slave; 查看状态   在 slave 上执行命令:show slave status\\G; 6. 相关命令 设置从库只读:set global read_only=1;。1表示只读,0是读写,但对拥有super权限的账号是不生效的,所以在授权账号的时候应尽量避免添加super权限 查看读写情况:show global variables like "%read_only%"; 查看server_id:show variables like '%server_id%'; 7. 踩坑记录  执行show slave status\\G;,发现从库并没有连接上主库,留意到错误信息大致为:“主从库server_id不能一致”,随即查看新增的master.cnf和slave.cnf,但并没有发现问题,随即用docker logs mysql-master查看启动日志,有一行警告大概是这样的:mysql: [Warning] World-writable config file '/etc/mysql/conf.d/master.cnf' is ignored.,这说明配置文件并没有挂载成功,搜索后得知当文件权限全局可写时,mysql会担心这种文件被其他用户恶意修改,所以会忽略掉这个配置文件,修改文件权限chmod 644 *.cnf后,再将以上流程重新跑了一遍,问题没有复现。","categories":[{"name":"Docker","slug":"Docker","permalink":"https://blog.mariojd.cn/categories/Docker/"},{"name":"MySQL","slug":"Docker/MySQL","permalink":"https://blog.mariojd.cn/categories/Docker/MySQL/"}],"tags":[{"name":"docker","slug":"docker","permalink":"https://blog.mariojd.cn/tags/docker/"},{"name":"mysql","slug":"mysql","permalink":"https://blog.mariojd.cn/tags/mysql/"},{"name":"主从搭建","slug":"主从搭建","permalink":"https://blog.mariojd.cn/tags/主从搭建/"}]},{"title":"Docker + PostgreSQL 主从环境搭建","slug":"Docker + PostgreSQL 主从环境搭建","date":"2018-06-21","updated":"2019-03-11","comments":true,"path":"postgresql-master-and-slave-environment-construction-based-docker.html","link":"","permalink":"https://blog.mariojd.cn/postgresql-master-and-slave-environment-construction-based-docker.html","excerpt":"","keywords":"","text":"环境说明 Docker Ubuntu/CentOS PostgreSQL v10.1 1. 运行PostgreSQL1.1 主库docker run --name pgsmaster -p 5500:5432 -e POSTGRES_PASSWORD=pgsmaster -v $(pwd)/pgsmaster:/var/lib/postgresql/data -d postgres 1.2 从库docker run --name pgsslave -p 5501:5432 -e POSTGRES_PASSWORD=pgsslave -v $(pwd)/pgsslave:/var/lib/postgresql/data -d postgres   进入以上主、从库对应的实际挂载目录执行下面的操作 2. 配置master(主库)2.1 编辑pg_hba.conf,在最下面添加如下:// replication_username: 复制账号; slave_ip: 从库所在的服务器iphost replication <replication_username> <slave_ip>/32 md5 2.2 编辑postgresql.conf(亲测,非必须),更改如下:synchronous_standby_names = '*' 2.3 进入容器,登录PostgreSQL,创建复制账号并验证:# 1.进入容器 docker exec -it pgsmaster bash# 2.连接PostgreSQL psql -U postgres# 3.创建用户 set synchronous_commit =off; // replication_username: 对应上面设置的复制账号; replication_username_password: 认证密码 create role <replication_username> login replication encrypted password '<replication_username_password>'; # 4.验证用户 \\du 3. 配置Slave(从库)3.1 编辑postgresql.conf,更改如下:hot_standby = onhot_standby_feedback = on 3.2 新建recovery.conf,添加如下内容:standby_mode = 'on'// replication_username: 复制账号(同主库); master_ip: 主库所在的服务器ip; master_port: 主库端口; replication_username_password: 认证密码primary_conninfo = 'host=<master_ip> port=<master_port> user=<replication_username> password=<replication_username_password>' 4. 同步主从库数据及测试4.1 停止PostgreSQLdocker stop pgsmaster docker stop pgsslave 4.2 同步主从库数据(必须) 方法1:rsync // 1.1 已ssh认证,请将$(pwd)更改为实际的路径rsync -cva --inplace --exclude=*pg_xlog* $(pwd)/pgsmaster/ <slave_ip>:$(pwd)/pgsslave/// 1.2 无ssh认证,请将$(pwd)更改为实际的路径rsync -cva --inplace --exclude=*pg_xlog* $(pwd)/pgsmaster/ ssh root@<slave_ip>:$(pwd)/pgsslave/ 方法2:pg_basebackup(自行谷歌) 4.3 先后启动主库、从库服务docker start pgsmaster docker start pgsslave 4.4 连接测试// 进入主库容器docker exec -it pgsmaster bash// 查看复制状态psql -U postgres -x -c "select * from pg_stat_replication;"","categories":[{"name":"Docker","slug":"Docker","permalink":"https://blog.mariojd.cn/categories/Docker/"},{"name":"PostgreSQL","slug":"Docker/PostgreSQL","permalink":"https://blog.mariojd.cn/categories/Docker/PostgreSQL/"}],"tags":[{"name":"docker","slug":"docker","permalink":"https://blog.mariojd.cn/tags/docker/"},{"name":"主从搭建","slug":"主从搭建","permalink":"https://blog.mariojd.cn/tags/主从搭建/"},{"name":"postgresql","slug":"postgresql","permalink":"https://blog.mariojd.cn/tags/postgresql/"}]},{"title":"Java 开发人员常用的服务配置(Nginx、Tomcat、JVM、Mysql、Redis)","slug":"Java 开发人员常用的服务配置(Nginx、Tomcat、JVM、Mysql、Redis)","date":"2018-06-19","updated":"2019-06-02","comments":true,"path":"service-configuration-commonly-used-by-java-developers.html","link":"","permalink":"https://blog.mariojd.cn/service-configuration-commonly-used-by-java-developers.html","excerpt":"","keywords":"","text":"Nginx Nginx是一款由C语言编写的高性能、轻量级的HTTP和反向代理服务器,同时也是一款IMAP/POP3/SMTP服务器。 nginx.conf:Nginx核心配置文件,linux下默认安装在/etc/nginx/ # Nginx所用用户和组,window下不指定 user www-data; # 工作的子进程数量(通常等于CPU数量或者2倍于CPU) worker_processes auto; # pid存放文件 pid /run/nginx.pid; # 简化调试 此指令不得用于生产环境 #master_process off;# 简化调试 此指令可以用到生产环境#daemon off; # 最大文件描述符worker_rlimit_nofile 51200;events { # 使用网络IO模型linux建议epoll,FreeBSD建议采用kqueue,window下不指定。 #use epoll; # 允许最大连接数 worker_connections 1024; # 此指令的作用是立即接受所有连接放到监听队列中,使得Nginx Worker能够在获得新连接通知时尽可能多的接受连接 #multi_accept on;} # load modules compiled as Dynamic Shared Object (DSO)##dso {# load ngx_http_fastcgi_module.so;# load ngx_http_rewrite_module.so;#} http { # 反向代理相关配置(自行新建),参考下面的proxy.conf #include /etc/nginx/conf.d/proxy.conf;; # GZIP压缩相关配置(自行新建),参考下面的gzip.conf #include /etc/nginx/conf.d//gzip.conf; ## # Basic Settings ## sendfile on; # 在一个数据包里发送所有头文件,而不是一个接一个的发送 tcp_nopush on; # 不缓存数据,而是一段一段的发送 tcp_nodelay on; # 设置客户端keep-alive超时时间 keepalive_timeout 65; types_hash_max_size 2048; client_header_timeout 10; client_body_timeout 10; send_timeout 10; client_header_buffer_size 1k; large_client_header_buffers 4 4k; # 允许客户端请求的最大单文件字节数 client_max_body_size 10m; client_body_in_single_buffer on; # 缓冲区代理缓冲用户端请求的最大字节数 client_body_buffer_size 128k; # 关闭Nginx的版本号和系统发行版本显示,默认是on # server_tokens off; # server_names_hash_bucket_size 64; # server_name_in_redirect off; # 设置文件使用的默认的MIME-type include /etc/nginx/mime.types; default_type application/octet-stream; ## # SSL Settings ## ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE ssl_prefer_server_ciphers on; ## # Logging Settings ## # 自定义access_log日志格式和级别 #log_format main '$remote_addr - $remote_user [$time_local] $request "$status" $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for"'; # 关闭日志可配置off # 访问和错误日志存放路径,常见日志级别有[ debug | info | notice | warn | error | crit | alert | emerg ],级别越高记录的信息越少。 access_log /var/log/nginx/access.log main; error_log /var/log/nginx/error.log notice; # 负载均衡配置 upstream upstream_test{ # 负载策略,常见有轮询(默认)、指定权重(weight 默认为1.weight越大,负载的权重就越大)、IP绑定(ip_hash)、fair(第三方)、url_hash(第三方) #ip_hash; server 127.0.0.1:9090 down; (down 表示当前server暂时不参与负载) server 127.0.0.1:8080 weight=10; server 127.0.0.1:8081 weight=5; server 127.0.0.1:7070 backup; (其它所有的非backup机器down掉或者忙碌时候,请求backup机器) ## Tengine config #check interval=300 rise=10 fall=10 timeout=100 type=http port=80; #check_http_send "GET / HTTP/1.0\\r\\n\\r\\n"; #check_http_expect_alive http_2xx http_3xx; ## Tengine config #session_sticky cookie=cookieTest mode=insert; } # server配置,同时支持监听80和443端口 server { listen 80; # https配置 listen 443 ssl http2; #default_server; listen [::]:443 ssl http2; #default_server; ssl_certificate "/etc/nginx/ssh_cert/1_domain.cn_bundle.crt"; ssl_certificate_key "/etc/nginx/ssh_cert/2_domain.cn.key"; ssl_session_cache shared:SSL:1m; ssl_session_timeout 10m; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; # 可使用通配符形式配置,如*.domain.com; server_name domain.com www.domain.com;; # 如果访问时没有加www,则跳转至www.domain.com if ($host !~* www.domain.com) { rewrite ^(.*)$ http://www.domain.com/$1 permanent; } location / { # 指定目录位置 root /etc/nginx/; # 开启目录浏览 autoindex on; # 默认为on,显示出文件的确切大小,单位是bytes。改为off后,显示出文件的大概大小,单位是kB或者MB或者GB autoindex_exact_size off; # 默认为off,显示的文件时间为GMT时间。改为on后,显示的文件时间为文件的服务器时间 autoindex_localtime on; # 10m之后下载速度为10k #limit_rate_after 10m; #limit_rate 10k; } # 根据User-Agent过滤网络爬虫 location /spider { if ($http_user_agent ~* "python|curl|java|wget|httpclient|okhttp") { return 503; } } location /proxy { proxy_pass http://upstream_test; # 配置跨域访问 add_header 'Access-Control-Allow-Headers' 'Content-Type'; add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET'; add_header 'Access-Control-Expose-Headers' 'Access-Control-Allow-Origin,Access-Control-Allow-Credentials'; } # static resources config location ~* ^/static/.*\\.(jpg|jpeg|gif|png|html|htm|swf|js|css)$ { root /etc/nginx/static/; access_log off; expires 30d; } # 当页面发生异常时,可以指定跳转到location中,也可以跳转到指定URL error_page 404 http://www.error404.com/; error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } } gzip.conf ## # Gzip Settings ## gzip on;# IE6或更低版本禁用压缩 gzip_disable "msie6";# 对数据启用压缩的最少字节数,建议最少大于1000字节 gzip_min_length 1k; gzip_vary on;# 允许或者禁止压缩基于请求和响应的响应流,设置为any意味着将会压缩所有的请求 gzip_proxied any;# 设置数据压缩的等级,范围是1-9之间的任意数值,9是最慢但压缩比最大 gzip_comp_level 6; gzip_buffers 16 8k; gzip_http_version 1.1;#指定压缩的文件类型 gzip_types application/font-woff text/plain application/javascript application/json text/css application/xml text/javascript image/jpg image/jpeg image/png image/gif image/x-icon; proxy.conf #后端的Web服务器可以通过X-Forwarded-For获取用户真实IPproxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;#nginx跟后端服务器连接超时时间(代理连接超时)proxy_connect_timeout 90;#连接成功后,后端服务器响应时间(代理接收超时)proxy_read_timeout 90;#nginx发送数据到后端服务器超时时间(代理发送超时)proxy_send_timeout 600; #设置代理服务器(nginx)保存用户头信息的缓冲区大小proxy_buffer_size 4k;#proxy_buffers缓冲区,网页平均在32k以下的话,这样设置proxy_buffers 4 32k;#高负荷下缓冲大小(proxy_buffers*2)proxy_busy_buffers_size 64k; #设定缓存文件夹大小,大于这个值,将从upstream服务器传proxy_temp_file_write_size 64k;#为了支持新的upstream keepalive选项proxy_http_version 1.1;proxy_set_header Connection ""; Tomcat Java EE开发中常用的一款Web应用服务器,也是一个免费开源的Servlet容器,类似的还有Jetty、Undertow、Netty等。 server.xml<Connector port="8080" protocol="org.apache.coyote.http11.Http11AprProtocol" connectionTimeout="5000" maxThreads="500" minSpareThreads="20" acceptCount="50" maxConnections="8192" redirectPort="8443" /> JVM关于JVM参数配置和调优可以参考: 一只懂JVM参数的狐狸 Redis Redis是一款使用C语言编写的高性能Key-Value开源数据库,支持存储的value类型包括有string(字符串)、list(链表)、set(集合)、zset(sorted set , 有序集合)和hash(哈希类型)。 redis.conf: redis核心配置文件 # By default Redis does not run as a daemon. Use 'yes' if you need it.# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.# 设置此选项使得redis以守护进程方式运行daemonize yes# When running daemonized, Redis writes a pid file in /var/run/redis.pid by# default. You can specify a custom pid file location here.# 以守护进程运行时,pid的存放路径pidfile /var/run/redis.pid# Accept connections on the specified port, default is 6379.# If port 0 is specified Redis will not listen on a TCP socket.# 端口port 6379# If you want you can bind a single interface, if the bind option is not# specified all the interfaces will listen for incoming connections.# 指定Redis可接收请求的IP地址,不设置将处理所有请求,建议生产环境中设置# bind 127.0.0.1# Close the connection after a client is idle for N seconds (0 to disable)# 客户端连接的超时时间,单位为秒,超时后会关闭连接timeout 0# Specify the log file name. Also 'stdout' can be used to force# Redis to log on the standard output. Note that if you use standard# output for logging but daemonize, logs will be sent to /dev/null# 配置 log 文件地址,默认打印在命令行终端的窗口上logfile stdout# Set the number of databases. The default database is DB 0, you can select# a different one on a per-connection basis using SELECT <dbid> where# dbid is a number between 0 and 'databases'-1# 设置数据库的个数,可以使用 SELECT <dbid>命令来切换数据库。默认使用的数据库是 0databases 16## Save the DB on disk:## save <seconds> <changes>## Will save the DB if both the given number of seconds and the given# number of write operations against the DB occurred.## In the example below the behaviour will be to save:# after 900 sec (15 min) if at least 1 key changed# after 300 sec (5 min) if at least 10 keys changed# after 60 sec if at least 10000 keys changed## Note: you can disable saving at all commenting all the "save" lines.# RDB备份的频率。# 900秒之内有1个keys发生变化时# 30秒之内有10个keys发生变化时# 60秒之内有10000个keys发生变化时save 900 1save 300 10save 60 10000# Compress string objects using LZF when dump .rdb databases?# For default that's set to 'yes' as it's almost always a win.# If you want to save some CPU in the saving child set it to 'no' but# the dataset will likely be bigger if you have compressible values or keys.# 在进行镜像备份时,是否进行压缩rdbcompression yes# The filename where to dump the DB# 镜像备份文件的文件名dbfilename dump.rdb# The working directory.## The DB will be written inside this directory, with the filename specified# above using the 'dbfilename' configuration directive.# # Also the Append Only File will be created inside this directory.# # Note that you must specify a directory here, not a file name.# 数据库镜像备份的文件放置的路径。这里的路径跟文件名要分开配置是因为 Redis 在进行备份时,先会将当前数据库的状态写入到一个临时文件中,等备份完成时,再把该该临时文件替换为上面所指定的文件,# 而这里的临时文件和上面所配置的备份文件都会放在这个指定的路径当中dir ./# Master-Slave replication. Use slaveof to make a Redis instance a copy of# another Redis server. Note that the configuration is local to the slave# so for example it is possible to configure the slave to save the DB with a# different interval, or to listen to another port, and so on.# 设置该数据库为其他数据库的从数据库# slaveof <masterip> <masterport># If the master is password protected (using the "requirepass" configuration# directive below) it is possible to tell the slave to authenticate before# starting the replication synchronization process, otherwise the master will# refuse the slave request.# 指定与主数据库连接时需要的密码验证# masterauth <master-password># Require clients to issue AUTH <PASSWORD> before processing any other# commands. This might be useful in environments in which you do not trust# others with access to the host running redis-server.## This should stay commented out for backward compatibility and because most# people do not need auth (e.g. they run their own servers).# # Warning: since Redis is pretty fast an outside user can try up to# 150k passwords per second against a good box. This means that you should# use a very strong password otherwise it will be very easy to break.# 设置客户端连接后进行任何其他指定前需要使用的密码。警告:redis速度相当快,一个外部的用户可以在一秒钟进行150K次的密码尝试,你需要指定非常非常强大的密码来防止暴力破解。requirepass foobared# Set the max number of connected clients at the same time. By default there# is no limit, and it's up to the number of file descriptors the Redis process# is able to open. The special value '0' means no limits.# Once the limit is reached Redis will close all the new connections sending# an error 'max number of clients reached'.# 限制同时连接的客户数量。当连接数超过这个值时,redis 将不再接收其他连接请求,客户端尝试连接时将收到 error 信息# maxclients 128# Don't use more memory than the specified amount of bytes.# When the memory limit is reached Redis will try to remove keys# accordingly to the eviction policy selected (see maxmemmory-policy).## If Redis can't remove keys according to the policy, or if the policy is# set to 'noeviction', Redis will start to reply with errors to commands# that would use more memory, like SET, LPUSH, and so on, and will continue# to reply to read-only commands like GET.## This option is usually useful when using Redis as an LRU cache, or to set# an hard memory limit for an instance (using the 'noeviction' policy).# 当内存达到设置上限时,内存的淘汰策略# maxmemmory-policy [volatile-lru or volatile-tt or volatile-randowm or allkeys-lru or allkeys-random] # WARNING: If you have slaves attached to an instance with maxmemory on,# the size of the output buffers needed to feed the slaves are subtracted# from the used memory count, so that network problems / resyncs will# not trigger a loop where keys are evicted, and in turn the output# buffer of slaves is full with DELs of keys evicted triggering the deletion# of more keys, and so forth until the database is completely emptied.## In short… if you have slaves attached it is suggested that you set a lower# limit for maxmemory so that there is some free RAM on the system for slave# output buffers (but this is not needed if the policy is 'noeviction').# 设置redis能够使用的最大内存。当内存满了的时候,如果还接收到set命令,redis将先尝试剔除设置过expire信息的key,而不管该key的过期时间还没有到达。# 在删除时,将按照过期时间进行删除,最早将要被过期的key将最先被删除。如果带有expire信息的key都删光了,那么将返回错误。# 这样,redis将不再接收写请求,只接收get请求。maxmemory的设置比较适合于把redis当作于类似memcached 的缓存来使用# maxmemory <bytes># By default Redis asynchronously dumps the dataset on disk. If you can live# with the idea that the latest records will be lost if something like a crash# happens this is the preferred way to run Redis. If instead you care a lot# about your data and don't want to that a single record can get lost you should# enable the append only mode: when this mode is enabled Redis will append# every write operation received in the file appendonly.aof. This file will# be read on startup in order to rebuild the full dataset in memory.## Note that you can have both the async dumps and the append only file if you# like (you have to comment the "save" statements above to disable the dumps).# Still if append only mode is enabled Redis will load the data from the# log file at startup ignoring the dump.rdb file.## IMPORTANT: Check the BGREWRITEAOF to check how to rewrite the append# log file in background when it gets too big.# 默认情况下,redis 会在后台异步的把数据库镜像备份到磁盘,但是该备份是非常耗时的,而且备份也不能很频繁,如果发生诸如拉闸限电、拔插头等状况,那么将造成比较大范围的数据丢失。# 所以redis提供了另外一种更加高效的数据库备份及灾难恢复方式。# 开启appendonly 模式之后,redis 会把所接收到的每一次写操作请求都追加到appendonly.aof 文件中,当redis重新启动时,会从该文件恢复出之前的状态。# 但是这样会造成 appendonly.aof 文件过大,所以redis还支持了BGREWRITEAOF 指令,对appendonly.aof进行重新整理appendonly no# The fsync() call tells the Operating System to actually write data on disk# instead to wait for more data in the output buffer. Some OS will really flush # data on disk, some other OS will just try to do it ASAP.## Redis supports three different modes:## no: don't fsync, just let the OS flush the data when it wants. Faster.# always: fsync after every write to the append only log . Slow, Safest.# everysec: fsync only if one second passed since the last fsync. Compromise.## The default is "everysec" that's usually the right compromise between# speed and data safety. It's up to you to understand if you can relax this to# "no" that will will let the operating system flush the output buffer when# it wants, for better performances (but if you can live with the idea of# some data loss consider the default persistence mode that's snapshotting),# or on the contrary, use "always" that's very slow but a bit safer than# everysec.## If unsure, use "everysec".# 设置对 appendonly.aof 文件进行同步的频率。always 表示每次有写操作都进行同步,everysec 表示对写操作进行累积,每秒同步一次。# appendfsync alwaysappendfsync everysec# appendfsync no# Virtual Memory allows Redis to work with datasets bigger than the actual# amount of RAM needed to hold the whole dataset in memory.# In order to do so very used keys are taken in memory while the other keys# are swapped into a swap file, similarly to what operating systems do# with memory pages.## To enable VM just set 'vm-enabled' to yes, and set the following three# VM parameters accordingly to your needs.# 是否开启虚拟内存支持。因为 redis 是一个内存数据库,而且当内存满的时候,无法接收新的写请求,所以在redis2.0中,提供了虚拟内存的支持。# 但是需要注意的是,redis中,所有的key都会放在内存中,在内存不够时,只会把value 值放入交换区。# 这样保证了虽然使用虚拟内存,但性能基本不受影响,同时,你需要注意的是你要把vm-max-memory设置到足够来放下你的所有的keyvm-enabled no# vm-enabled yes# This is the path of the Redis swap file. As you can guess, swap files# can't be shared by different Redis instances, so make sure to use a swap# file for every redis process you are running. Redis will complain if the# swap file is already in use.## The best kind of storage for the Redis swap file (that's accessed at random) # is a Solid State Disk (SSD).## *** WARNING *** if you are using a shared hosting the default of putting# the swap file under /tmp is not secure. Create a dir with access granted# only to Redis user and configure Redis to create the swap file there.# 设置虚拟内存的交换文件路径vm-swap-file /tmp/redis.swap# vm-max-memory configures the VM to use at max the specified amount of# RAM. Everything that deos not fit will be swapped on disk *if* possible, that# is, if there is still enough contiguous space in the swap file.## With vm-max-memory 0 the system will swap everything it can. Not a good# default, just specify the max amount of RAM you can in bytes, but it's# better to leave some margin. For instance specify an amount of RAM# that's more or less between 60 and 80% of your free RAM.# 这里设置开启虚拟内存之后,redis将使用的最大物理内存的大小。默认为0,redis将把他所有的能放到交换文件的都放到交换文件中,以尽量少的使用物理内存。# 在生产环境下,需要根据实际情况设置该值,最好不要使用默认的 0vm-max-memory 0# Redis swap files is split into pages. An object can be saved using multiple# contiguous pages, but pages can't be shared between different objects.# So if your page is too big, small objects swapped out on disk will waste# a lot of space. If you page is too small, there is less space in the swap# file (assuming you configured the same number of total swap file pages).## If you use a lot of small objects, use a page size of 64 or 32 bytes.# If you use a lot of big objects, use a bigger page size.# If unsure, use the default # 设置虚拟内存的页大小,如果你的 value 值比较大,比如说你要在 value 中放置博客、新闻之类的所有文章内容,就设大一点,如果要放置的都是很小的内容,那就设小一点vm-page-size 32# Number of total memory pages in the swap file.# Given that the page table (a bitmap of free/used pages) is taken in memory,# every 8 pages on disk will consume 1 byte of RAM.## The total swap size is vm-page-size * vm-pages## With the default of 32-bytes memory pages and 134217728 pages Redis will# use a 4 GB swap file, that will use 16 MB of RAM for the page table.## It's better to use the smallest acceptable value for your application,# but the default is large in order to work in most conditions.# 设置交换文件的总的 page 数量,需要注意的是,page table信息会放在物理内存中,每8个page 就会占据RAM中的 1 个 byte。# 总的虚拟内存大小 = vm-page-size * vm-pagesvm-pages 134217728# Max number of VM I/O threads running at the same time.# This threads are used to read/write data from/to swap file, since they# also encode and decode objects from disk to memory or the reverse, a bigger# number of threads can help with big objects even if they can't help with# I/O itself as the physical device may not be able to couple with many# reads/writes operations at the same time.## The special value of 0 turn off threaded I/O and enables the blocking# Virtual Memory implementation.# 设置 VM IO 同时使用的线程数量。vm-max-threads 4# Hashes are encoded in a special way (much more memory efficient) when they# have at max a given numer of elements, and the biggest element does not# exceed a given threshold. You can configure this limits with the following# configuration directives.# redis 2.0 中引入了 hash 数据结构。 # hash 中包含超过指定元素个数并且最大的元素当没有超过临界时,hash 将以zipmap(又称为 small hash大大减少内存使用)来存储,这里可以设置这两个临界值hash-max-zipmap-entries 512(hash-max-ziplist-entries for Redis >= 2.6)hash-max-zipmap-value 64(hash-max-ziplist-value for Redis >= 2.6)# Active rehashing uses 1 millisecond every 100 milliseconds of CPU time in# order to help rehashing the main Redis hash table (the one mapping top-level# keys to values). The hash table implementation redis uses (see dict.c)# performs a lazy rehashing: the more operation you run into an hash table# that is rhashing, the more rehashing "steps" are performed, so if the# server is idle the rehashing is never complete and some more memory is used# by the hash table.# # The default is to use this millisecond 10 times every second in order to# active rehashing the main dictionaries, freeing memory when possible.## If unsure:# use "activerehashing no" if you have hard latency requirements and it is# not a good thing in your environment that Redis can reply form time to time# to queries with 2 milliseconds delay.## use "activerehashing yes" if you don't have such hard requirements but# want to free memory asap when possible.# 开启之后,redis 将在每 100 毫秒时使用 1 毫秒的 CPU 时间来对 redis 的 hash 表进行重新 hash,可以降低内存的使用。# 当你的使用场景中,有非常严格的实时性需要,不能够接受 Redis 时不时的对请求有 2 毫秒的延迟的话,把这项配置为 no。# 如果没有这么严格的实时性要求,可以设置为 yes,以便能够尽可能快的释放内存activerehashing yes Mysqlmy.cnf: mysql核心配置文件 [client]port = 3306socket = /var/lib/mysql/mysql.sock [mysqld]datadir=/var/lib/mysqlsocket=/var/lib/mysql/mysql.sockuser = mysqlport = 3306pid-file = /var/lib/mysql/mysql.pid#配置此项可以追踪sql执行记录log=存放日志的路径/mysql-sql.log##以下为开启主从的必要配置server-id = 1binlog-do-db=db_nameA #指定对db_nameA记录二进制日志 binlog-ignore-db=db_namB #指定不对db_namB记录二进制日志binlog_format = MIXED #binlog格式指定character-set-server = utf8mb4 #指定数据默认编码log_bin = /var/lib/mysql/log/mysql-bin.logexpire_logs_days = 30character-set-server = utf8mb4default-storage-engine = InnoDB#thread connectionmax_connections = 1024max_connect_errors = 1024# Try number of CPU's*2 for thread_concurrencythread_concurrency = 8thread_cache_size = 256#*networkskip-name-resolve #跳过dns查询max_allowed_packet = 1M#buffer&amp;cachetable_open_cache = 4096sort_buffer_size = 256Kjoin_buffer_size = 256K#query cachequery_cache_limit = 4Mquery_cache_size = 4Mquery_cache_type = 1#temptabletmp_table_size = 64Mmax_heap_table_size = 64M#自增主键配置auto_increment_offset=1auto_increment_increment=2#MyISAMkey_buffer_size = 8Mread_buffer_size = 1Mread_rnd_buffer_size = 256K#Innodbinnodb_log_file_size = 256Minnodb_log_files_in_group = 2innodb_status_file = 1innodb_additional_mem_pool_size = 32Minnodb_buffer_pool_size = 5Ginnodb_data_file_path = ibdata1:1G:autoextendinnodb_file_per_table = 1innodb_additional_mem_pool_size = 32Minnodb_buffer_pool_size = 5Ginnodb_data_file_path = ibdata1:1G:autoextendinnodb_file_per_table = 1innodb_force_recovery = 0#innodb_table_locksinnodb_thread_concurrency = 8innodb_flush_log_at_trx_commit = 2innodb_force_recovery = 0#innodb_table_locksinnodb_thread_concurrency = 8innodb_flush_log_at_trx_commit = 2#slow logslow_query_log=1long_query_time=1slow_query_log_file=/var/lib/mysql/log/slow.log[mysqld_safe]#error loglog-error = /var/log/mysqld.logpid-file = /var/lib/mysql/mysql.pidopen-files-limit = 40960[mysqldump]quickmax_allowed_packet = 48M[mysql]no-auto-rehash# Remove the next comment character if you are not familiar with SQL#safe-updatesdefault-character-set=utf8[isamchk]key_buffer = 128Msort_buffer_size = 128Mread_buffer = 2Mwrite_buffer = 2M[myisamchk]key_buffer = 128Msort_buffer_size = 128Mread_buffer = 2Mwrite_buffer = 2M 相关链接 NginxApache TomcatRedisMysql","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"mysql","slug":"mysql","permalink":"https://blog.mariojd.cn/tags/mysql/"},{"name":"redis","slug":"redis","permalink":"https://blog.mariojd.cn/tags/redis/"},{"name":"nginx","slug":"nginx","permalink":"https://blog.mariojd.cn/tags/nginx/"},{"name":"tomcat","slug":"tomcat","permalink":"https://blog.mariojd.cn/tags/tomcat/"},{"name":"jvm","slug":"jvm","permalink":"https://blog.mariojd.cn/tags/jvm/"}]},{"title":"Intellij IDEA 神器那些让人爱不释手的小技巧","slug":"Intellij IDEA 神器那些让人爱不释手的小技巧","date":"2018-06-14","updated":"2018-10-29","comments":true,"path":"those-little-tricks-that-people-love-to-put-up-with-in-intellij-idea.html","link":"","permalink":"https://blog.mariojd.cn/those-little-tricks-that-people-love-to-put-up-with-in-intellij-idea.html","excerpt":"","keywords":"","text":"原文链接:https://blog.csdn.net/linsongbin1/article/details/80560332 概述 在2018年5月6日写了一篇介绍IntellIJ IDEA的文章,Intellij IDEA神器居然还有这些小技巧,主要是列出一些平时大家可能没用过或者没怎么用,但是又非常好用的IntellIJ IDEA小技巧。由于篇幅原因,只是列出了一小部分,那么接下来的这篇文章,会继续补充一些IntellIJ IDEA的小技巧。 别轻易自定义快捷键 有蛮多操作,IntellIJ IDEA并没有给我们设置默认快捷键,需要使用者自己去定义快捷键。比如说: Rebuild Project Compare With Branch 为了能在IntellIJ IDEA里进行无鼠标编程,很多程序员都会自定义快捷键,但是有三个地方你可能需要注意一下。 经常会出现快捷键与其他应用的快捷键冲突的情况; 自定义太多快捷键了,你也不太好记住; 使用其他同事的IDEA时(比如说帮忙定位问题),你自定义的快捷键没法用。 其实在IntellIJ IDEA里的每个操作,都可以看出一个action。我们可以使用ctrl+shift+a来输入我们要使用的操作。比如说,上面提到的Rebuild Project。你可以先使用ctrl+shift+a快捷键,然后输入Rebuild Project并回车,即可执行你要的操作。 对我自己来说,除了基础的快捷键,ctrl+shift+a是我用最频繁的快捷键了。 使用ctrl+alt+h要小心 ctrl+alt+h非常好用,但是有个坑,当同一个方法里,调用某个方法多次的时候,比如说下面的代码: public class TestService { public void test1() { System.out.println(\"aa\"); } public void test2() { test1(); } public void test3() { test1(); //无数业务操作后,再次电影test1()方法 test1(); }} 如果我们想知道有哪些地方调用了test1()方法,使用ctrl+alt+h无法正确列出来的。因为ctrl+alt+h只能告诉你调用的层次。 ctrl+alt+h只是会在某个隐蔽的地方,告诉你,test3()方法调用了test1()方法两次。这样就很容易坑到开发者,因为大部分人可能不太注意后面的调用次数,导致改bug的时候,以为全部都改了呢? 如果你想精确的列出到底哪些地方调用了test1()方法,你需要使用alt+f7这个快捷键。 尤其是我们在阅读极其复杂的业务代码时,使用alt+f7就非常合适。 当然alt+f7也可以作用在变量上,列出某个类里,哪些地方使用了该变量。 ctrl+alt+h被问的最多的两个问题 经常有同事和网友问我。 Sam哥,使用ctrl+alt+h怎么跳转到源代码,又如何重新回到ctrl+alt+h对应的视图里面。 调转到源代码 其实很简单,当你使用ctrl+alt+h后,使用向下或者向上箭头,选择某个调用,然后按下f4即可跳转到源代码。 如何回到ctrl+alt+h视图这个真心被问了好几百遍,其实很简单,当你使用f4跳转到源代码后,直接使用alt+8就可以跳回去了。就又可以继续看下一个调用的地方了。 快速找到Controller方法 如果你的项目里有非常多的controller,里面有非常多的http或者resful方法。如何快速找到这些方法呢?这个时候,ctrl+alt+shift+n就可以派上用场了。 比如说,你依稀记得入账单相关的接口,都有个bill的url路径,那么使用ctrl+alt+shift+n后,直接输入/bill即可。 当你在成千上万的Controller里寻找方法时,这一招就可以大大提高效率。 了解项目关键业务流程方法的利器-bookmark 在一些创业公司里,很多核心的模块都是放置在同一个项目里的。比如说,订单相关的接口,支付相关的接口,商品相关的接口。这个时候,你可以将这些关键业务方法,使用bookmark统一放置到某个地方,方便你阅读。 那么如何使用快捷键来达到上面的效果呢? public class TestService { public void test1() { System.out.println(\"aa\"); } public void test2() { test1(); } public void test3() { test1(); test1(); }} 比如像上面的方法,我想将test1()方法放置到bookmark里,可以通过如下操作来完成: 1、使用ctrl+f12,列出该类的所有方法,然后输入test1,将光标定位在test1上; 2、按下f11,将test1()加入到bookmark; 3、按下shift+f11,将bookmark列表弹出来; 4、按下ctrl+enter修改bookmark名字。 只留下一个tab 这个是我目前正在用的,就是整个工程里面,只有一个代码tab。也即是说,无论你打开多少个文件,都是在同一个tab里面显示。如果这样设置了,有些网友可能会问,我想看看我最近操作哪些类了,怎么看? 可以直接使用ctrl+e来显示最近操作的文件。 我是比较推荐只是保留一个代码tab的,非常简洁。如果每打开一个文件,就是一个新的tab,很快你就会乱掉,而且还得关闭部分tab。 可以通过下面的方式来设置成用一个tab显示代码。按下ctrl+shif+a,然后输入Editor Tabs,然后回车进入编辑页面。 然后在Placement那里,选择None 如何阅读又长又臭的代码 由于历史原因,项目里总会存在那种无法理解的,又长又臭的业务代码。阅读这种代码,简直就是一种煎熬。但是在IntellIJ IDEA里,只要使用5个小技巧,便可大大提高阅读质量和速度。 创建任意代码折叠块 像上面的for循环,我想直接将其折叠起来,因为代码太长的时候,使用折叠块,可以帮助你快速理清代码的主脉络。 可以将光标定位在for循环的左大括号里,然后使用ctrl+shift+. 即可。 如果你想让这个折叠快消失,直接使用ctrl 加上一个+即可。 大括号匹配 这个也非常有用,因为代码太长,某个for循环,可能已经撑满整个屏幕了。这个时候,找到某个大括号对应的另外一边就很费劲。你可以将光标定位在某个大括号一边,然后使用ctrl+]或者ctrl+[来回定位即可。 ctrl+shift+f7结合f3 ctrl+shift+f7可以高亮某个变量,而且随着鼠标的移动,这个高亮是不会消失的(这个很重要)。然后使用f3找到下一个使用该变量的地方。 使用ctrl+shift+i 这个也是阅读长段代码的法宝,当你阅读的代码很长的时候,突然想看代码里某个类的定义,那么直接使用ctrl+shift+i,就可以在当前类里再弹出一个窗口出来。比如说: 在这个代码块里,你想看看TestTemp类的定义,那么将光标定位在TestTemp上,然后直接使用ctrl+shift+i,就会弹出如下的窗口。 按下esc,可以关闭这个窗口。 使用alt+f7 这个我在上面已经介绍过了。可以列出变量在哪些地方被使用了。 结合这5个技巧,相信可以大大提高长段代码的阅读效率。 跳到父类接口 我们经常会定义一个service 接口,比如说UserService,然后使用一个UserServiceImpl类去实现UserService里面的接口。 public interface UserService { void test1();} public class UserServiceImpl implements UserService { @Override public void test1() { }} 那么在UserServiceImpl里的test1()方法上,如何跳转到UserService的test1(),直接使用ctrl+u即可。 后悔药 如果修改了部分代码,突然觉得不合适,使用ctrl+z回滚掉后。突然又觉得刚才的修改是可以的。那你可以使用ctr+shift+z再次恢复你刚才修改的内容。 切换皮肤最快的方式 可以直接使用ctrl,然后加上一个`,就可以立刻弹出如下界面: 选择Color Scheme,然后回车,就可以弹出修改皮肤的窗口。","categories":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/categories/IDEA/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/tags/Java/"}]},{"title":"Intellij IDEA 神器居然还有这些小技巧","slug":"Intellij IDEA 神器居然还有这些小技巧","date":"2018-06-14","updated":"2018-10-29","comments":true,"path":"these-little-tricks-in-intellij-idea.html","link":"","permalink":"https://blog.mariojd.cn/these-little-tricks-in-intellij-idea.html","excerpt":"","keywords":"","text":"原文链接:https://blog.csdn.net/linsongbin1/article/details/80211919 概述 Intellij IDEA真是越用越觉得它强大,它总是在我们写代码的时候,不时给我们来个小惊喜。出于对Intellij IDEA的喜爱,我决定写一个与其相关的专栏或者系列,把一些好用的Intellij IDEA技巧分享给大家。本文是这个系列的第一篇,主要介绍一些你可能不知道的但是又实用的小技巧。 我最爱的【演出模式】 我们可以使用【Presentation Mode】,将IDEA弄到最大,可以让你只关注一个类里面的代码,进行毫无干扰的coding。 可以使用Alt+V快捷键,弹出View视图,然后选择Enter Presentation Mode。效果如下: 这个模式的好处就是,可以让你更加专注,因为你只能看到特定某个类的代码。可能读者会问,进入这个模式后,我想看其他类的代码怎么办?这个时候,就要考验你快捷键的熟练程度了。你可以使用CTRL+E弹出最近使用的文件。又或者使用CTRL+N和CTRL+SHIFT+N定位文件。 如何退出这个模式呢?很简单,使用ALT+V弹出view视图,然后选择Exit Presentation Mode 即可。但是我强烈建议你不要这么做,因为你是可以在Enter Presentation Mode模式下在IDEA里面做任何事情的。当然前提是,你对IDEA足够熟练。 神奇的Inject language 如果你使用IDEA在编写JSON字符串的时候,然后要一个一个\\去转义双引号的话,就实在太不应该了,又烦又容易出错。在IDEA可以使用Inject language帮我们自动转义双引号。 先将焦点定位到双引号里面,使用alt+enter快捷键弹出inject language视图,并选中Inject language or reference。 选择后,切记,要直接按下enter回车键,才能弹出inject language列表。在列表中选择 json组件。 选择完后。鼠标焦点自动会定位在双引号里面,这个时候你再次使用alt+enter就可以看到 选中Edit JSON Fragment并回车,就可以看到编辑JSON文件的视图了。 可以看到IDEA确实帮我们自动转义双引号了。如果要退出编辑JSON信息的视图,只需要使用ctrl+F4快捷键即可。 Inject language可以支持的语言和操作多到你难以想象,读者可以自行研究。 使用快捷键移动分割线 假设有下面的场景,某个类的名字在project视图里被挡住了某一部分。 你想完整的看到类的名字,该怎么做。一般都是使用鼠标来移动分割线,但是这样子效率太低了。可以使用alt+1把鼠标焦点定位到project视图里,然后直接使用ctrl+shift+左右箭头来移动分割线。 ctrl+shift+enter不只是用来行尾加分号的 ctrl+shift+enter其实是表示为您收尾的意思,不只是用来给代码加分号的。比如说: 这段代码,我们还需要为if语句加上大括号才能编译通过,这个时候你直接输入ctrl+shift+enter,IDEA会自动帮你收尾,加上大括号的。 不要动不动就使用IDEA的重构功能 IDEA的重构功能非常强大,但是也有时候,在单个类里面,如果只是想批量修改某个文本,大可不必使用到重构的功能。比如说: 上面的代码中,有5个地方用到了rabbitTemplate文本,如何批量修改呢? 首先是使用ctrl+w选中rabbitTemplate这个文本,然后依次使用5次alt+j快捷键,逐个选中,这样五个文本就都被选中并且高亮起来了,这个时候就可以直接批量修改了。 去掉导航栏 去掉导航栏,因为平时用的不多。 可以把红色的导航栏去掉,让IDEA显得更加干净整洁一些。使用alt+v,然后去掉Navigation bar即可。去掉这个导航栏后,如果你偶尔还是要用的,直接用alt+home就可以临时把导航栏显示出来。 如果想让这个临时的导航栏消失的话,直接使用esc快捷键即可。 把鼠标定位到project视图里 当工程里的包和类非常多的时候,有时候我们想知道当前类在project视图里是处在哪个位置。 上面图中的DemoIDEA里,你如何知道它是在spring-cloud-config工程里的哪个位置呢? 可以先使用alt+F1,弹出Select in视图,然后选择Project View中的Project,回车,就可以立刻定位到类的位置了。 那如何从project跳回代码里呢?可以直接使用esc退出project视图,或者直接使用F4,跳到代码里。 强大的symbol 如果你依稀记得某个方法名字几个字母,想在IDEA里面找出来,可以怎么做呢?直接使用ctrl+shift+alt+n,使用symbol来查找即可。比如说: 你想找到checkUser方法。直接输入user即可。 如果你记得某个业务类里面有某个方法,那也可以使用首字母找到类,然后加个.,再输入方法名字也是可以的。 如何找目录 使用ctrl+shift+n后,使用/,然后输入目录名字即可. 自动生成not null判断语句 自动生成not null这种if判断,在IDEA里有很多种办法,其中一种办法你可能没想到。 当我们使用rabbitTemplate. 后,直接输入notnull并回车,IDEA就好自动生成if判断了。 按照模板找内容 这个也是我非常喜欢的一个功能,可以根据模板来找到与模板匹配的代码块。比如说: 想在整个工程里面找到所有的try catch语句,但是catch语句里面没有做异常处理的。 catch语句里没有处理异常,是极其危险的。我们可以IDEA里面方便找到所有这样的代码。 首先使用ctrl+shift+A快捷键弹出action框,然后输入Search Struct 选择Search Structurally后,回车,跳转到模板视图。 点击Existing Templates按钮,选择try模板。为了能找出catch里面没有处理异常的代码块,我们需要配置一下CatchStatement的Maximum count的值,将其设置为1。 点击Edit Variables按钮,在界面修改Maximum count的值。 最后点击find按钮,就可以找出catch里面没有处理异常的代码了。","categories":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/categories/IDEA/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/tags/Java/"}]},{"title":"Python 中的 is 和 == 以及字符串驻留机制","slug":"Python 中的 is 和 == 以及字符串驻留机制","date":"2018-06-14","updated":"2019-06-02","comments":true,"path":"equal-compare-and-string-resident-mechanism-in-python.html","link":"","permalink":"https://blog.mariojd.cn/equal-compare-and-string-resident-mechanism-in-python.html","excerpt":"","keywords":"","text":"is 和 ==  先了解下官方文档中关于 is 和 == 的概念。is 表示的是对象标示符(object identity),而 == 表示的是相等(equality);is 的作用是用来检查对象的标示符是否一致,也就是比较两个对象在内存中的地址是否一样(相当于检查 id(a) == id(b)),而 == 是用来检查两个对象引用的值是否相等(相当于检查 a.eq(b));这点和Java有点类似,只不过Java中是用 == 来比较两个对象在内存中的地址,用 equals() 来检查两者之间的值是否相等。 is == 概念 对象标示符 相等 作用 比较对象在内存中的地址 检查两个对象引用的值 示例 id(a) == id(b) a.eq(b) 字符串驻留机制  Python中的字符串采用了intern机制,当需要值相同的字符串的时候(比如标识符),可以直接从字符串池里拿来使用,避免频繁的创建和销毁,提升效率和节约内存,因此拼接和修改字符串是会比较影响性能的。因为是不可变的,所以字符串的操作都不是replace,而是新建对象,这也是为什么拼接多字符串的时候不建议用+而用join(),join()是先计算出所有字符串的长度,然后再拷贝,只new一次对象。需要注意的是,并不是所有的字符串都会采用intern机制,当且仅当只包含下划线、数字、字母的字符串才会被intern。 相关示例示例一a = \"hello\"b = \"hello\"print(a is b) # 输出 True print(a == b) # 输出 True 值相同的简单字符串对象在字符串池里只会保存一份,这决定了字符串必须是不可变对象,所以a和b是同一个对象 示例二a = \"hello world\"b = \"hello world\"print(a is b) # 输出 Falseprint(a == b) # 输出 True a和b中都有空格,所以不会被intern(空格不是python标识符),故a和b不是同一个对象。注意,这仅仅是在交互式命令行中执行,而在PyCharm或者保存为文件执行,结果是不一样的,主要是因为解释器做了一部分优化 示例三a = 'ab' + 'c' is 'abc'print(a) # 输出 Trueab = 'ab'b = ab + 'c' is 'abc'print(b) # 输出 False 第一个’ab’+’c’是在compile-time(编译期)求值的,被替换成了’abc’,所以输出为True;第二个示例,ab+’c’是在run-time(运行期)拼接的,导致没有被自动intern 示例四a = [1, 2, 3]b = [1, 2, 3]print(a is b) # 输出 Falseprint(a == b) # 输出 True a和b是列表,不是同一个对象 示例五a = [1, 2, 3]b = aprint(a is b) # 输出 True print(a == b) # 输出 True 把a的引用复制给b(引用赋值),在内存中其实是指向同一个对象 示例六a = [\"I\", \"love\", \"Python\"]b = a[:]print(a is b) # 输出 Falseprint(a == b) # 输出 Trueprint(a[0] is b[0]) # 输出 Trueprint(a[0] == b[0]) # 输出 True b通过切片操作重新分配了对象(切片赋值),但值和a相同。由于切片拷贝是浅拷贝,这说明列表中的元素并未重新创建,因此a[0] is b[0]输出为True 示例七a = 1b = 1print(a is b) # 输出 Trueprint(a == b) # 输出 True Python会对比较小的整数对象进行缓存,下次用的时候直接从缓存中获取 示例八a = 320b = 320print(a is b) # 输出 Falseprint(a == b) # 输出 True Python仅仅对比较小的整数对象进行缓存(范围为范围[-5, 256]),而并非是所有整数对象。注意,这仅仅是在交互式命令行中执行,而在PyCharm或者保存为文件执行,结果是不一样的,主要是因为解释器做了一部分优化 is 与 == 对比  is 与 == 相比计算速度会更快,因为它不能重载,不用进行特殊的函数调用,通过直接比较两个整数 id,减少了函数调用的开销。而 a == b 则是等同于a.eq(b),继承自 object 的 eq 方法原本也是比较两个对象的id,结果与 is 一样,但大多数Python对象会覆盖重写object的 eq 方法,而定义内容的相关比较,所以比较的是对象属性的值。  在变量和单例值之间比较时,应该使用 is。目前,最常使用 is 的地方是当判断对象是不是 None,下面是推荐的写法: xxx is None;判断不是None的推荐写法是: xxx is not None 参考文章 Python面试之 is 和 == 的区别什么是string interning(字符串驻留)以及python中字符串的intern机制Python 中的比较:is 与 ==Python中的浅拷贝与深拷贝","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.mariojd.cn/categories/Python/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.mariojd.cn/tags/python/"},{"name":"字符串机制","slug":"字符串机制","permalink":"https://blog.mariojd.cn/tags/字符串机制/"}]},{"title":"用 Python 统计你的简书数据","slug":"用 Python 统计你的简书数据","date":"2018-05-20","updated":"2019-06-02","comments":true,"path":"use-python-to-count-your-jianshu-data.html","link":"","permalink":"https://blog.mariojd.cn/use-python-to-count-your-jianshu-data.html","excerpt":"","keywords":"","text":"写在前面  说来也巧,之前有一次无意间留意到简书好像没有做文章总阅读量的统计(准确的说法应该叫展示),刚好最近有时间,趁这个机会就用Python写了这么个功能,既是学习也是练手。 展示效果  再继续往下之前,先贴两张图看看最终的效果。第一张图片展示的是个人简书爬取后的数据,第二张可是大名鼎鼎简叔的简书数据。   两图一对比,果然这数量和质量都不是一个等级的,不过后生会继续努力的。写了两个多月的博客了,没想到也有一千多的阅读量了,这是算喜还是算忧呢?哈哈,总之感觉还是蛮欣慰的。 环境说明 python v3.6.4 webpy v0.40-dev1 要求有一定的Python基础 开发搭建  项目中用到的第三方module主要包括Requests、BeautifulSoup和Numpy,因为另外两个安装都比较顺利,所以这里只记录一下安装BeautifulSoup和遇到的问题。 pip安装pip install beautifulsoup4 # 安装Beautifulsouppip install beautifulsoup4 upgrade # 升级Beautifulsouppip uninstall beautifulsoup4 # 卸载Beautifulsoup 直接下载  地址:https://www.crummy.com/software/BeautifulSoup/bs4/download/ 。下载好后把解压文件夹下的bs4文件夹直接拷贝到python安装目录的Lib下即可。(如果此方法无效,请尝试重新进入解压文件夹下,使用命令python setup.py build和python setup.py install,然后再拷贝复制bs4文件夹) 问题还原   本以为安装好Beautifulsoup后就可以了,没想到在实际运行的时候出现了下面这个问题,如果你在使用的过程中也出现了同样的问题,可以参考如下解决过程。    You are trying to run the Python 2 version of Beautiful Soup under Python 3. This will not work.’<>’You need to convert the code, either by installing it (python setup.py install) or by running 2to3 (2to3 -w bs4).   说明: bs4需要通过python自带的工具2to3.py转化为python3下的文件,这个工具在python安装目录的Tools\\scripts中(PS:其他库出现这种情况应该也可以这样解决)。具体命令:python D:\\python36\\Tools\\scripts\\2to3.py -w bs4,如果该命令出现执行错误的情况,可以尝试进入python安装目录下的Lib\\bs4中再执行。 其它问题  1.使用webpy过程中,当渲染的模板 ( 大多是html ) 中带有中文的时候,出现了如下错误:   File “D:\\python36\\lib\\site-packages\\web\\template.py”, line 1052, in _load_template return Template(open(path).read(), filename=path, **self._keywords)  UnicodeDecodeError: ‘gbk’ codec can’t decode byte 0x80 in position 298: illegal multibyte sequence   解决办法:直接点进去这个template.py,然后修改代码return Template(open(path).read(), filename=path, **self._keywords)为return Template(open(path, encoding='utf-8').read(), filename=path, **self._keywords),也就是添加encoding='utf-8',详细可以查看这个GitHub PR。   2.页面请求后显示IndentationError,错误信息如下: Traceback (most recent call last): File \"D:\\python36\\lib\\site-packages\\web\\application.py\", line 257, in process return self.handle() File \"D:\\python36\\lib\\site-packages\\web\\application.py\", line 248, in handle return self._delegate(fn, self.fvars, args) File \"D:\\python36\\lib\\site-packages\\web\\application.py\", line 488, in _delegate return handle_class(cls) File \"D:\\python36\\lib\\site-packages\\web\\application.py\", line 466, in handle_class return tocall(*args) File \"D:/PyCharmProjects/jianshu\\webCount.py\", line 16, in GET return render.data(read_count) File \"D:\\python36\\lib\\site-packages\\web\\template.py\", line 1070, in __getattr__ t = self._template(name) File \"D:\\python36\\lib\\site-packages\\web\\template.py\", line 1067, in _template return self._load_template(name) File \"D:\\python36\\lib\\site-packages\\web\\template.py\", line 1052, in _load_template return Template(open(path, encoding='utf-8').read(), filename=path, **self._keywords) File \"D:\\python36\\lib\\site-packages\\web\\template.py\", line 903, in __init__ code = self.compile_template(text, filename) File \"D:\\python36\\lib\\site-packages\\web\\template.py\", line 970, in compile_template compiled_code = compile(code, filename, 'exec') File \"templates\\data.html\", line 32 extend_(['<header class=\"text-white text-center\" style=\"padding-top: 5rem;\">\\n']) ^IndentationError: expected an indented blockTemplate traceback: File 'templates\\\\data.html', line 32 None   说明:模板渲染中,如果使用了webpy内置的python语法函数,那就有一定的格式要求,这种错误信息就是提示要注意缩进,语法定义和html之间保持缩进即可,参考如下: $if read_count.exit: # 这两行之间的缩进是必需的 <header class=\"text-white text-center\" style=\"padding-top: 5rem;\"> </header>$else: <b>uid: $read_count.uid is not exit !</b>   3.页面请求后显示SyntaxError,错误信息如下: Traceback (most recent call last): File \"D:\\python36\\lib\\site-packages\\web\\application.py\", line 257, in process return self.handle() File \"D:\\python36\\lib\\site-packages\\web\\application.py\", line 248, in handle return self._delegate(fn, self.fvars, args) File \"D:\\python36\\lib\\site-packages\\web\\application.py\", line 488, in _delegate return handle_class(cls) File \"D:\\python36\\lib\\site-packages\\web\\application.py\", line 466, in handle_class return tocall(*args) File \"D:/PyCharmProjects/jianshu\\webCount.py\", line 16, in GET return render.data(read_count) File \"D:\\python36\\lib\\site-packages\\web\\template.py\", line 1070, in __getattr__ t = self._template(name) File \"D:\\python36\\lib\\site-packages\\web\\template.py\", line 1067, in _template return self._load_template(name) File \"D:\\python36\\lib\\site-packages\\web\\template.py\", line 1052, in _load_template return Template(open(path, encoding='utf-8').read(), filename=path, **self._keywords) File \"D:\\python36\\lib\\site-packages\\web\\template.py\", line 903, in __init__ code = self.compile_template(text, filename) File \"D:\\python36\\lib\\site-packages\\web\\template.py\", line 970, in compile_template compiled_code = compile(code, filename, 'exec') File \"templates\\data.html\", line 31 def with (read_count) ^SyntaxError: invalid syntaxTemplate traceback: File 'templates\\\\data.html', line 31 None   说明:碰到这种错误提示,需要检查是否将渲染参数(对象)的定义放到了模板的最顶部,参考如下: $def with (read_count)# 上面这段定义要在最顶部<html lang=\"zh-CN\"><head># 省略这部分信息</head><body># 省略这部分信息</body></html> 程序代码  一些必要的说明在编码的时候我就写在注释里了,大家还是直接看代码吧。 readCount.py:核心功能是抓取简书用户的数据 import mathimport timeimport numpyimport requestsimport multiprocessingfrom bs4 import BeautifulSoup# 简书用户的文章阅读总量统计class ReadCount(object): # 数据初始化 def __init__(self, uid): self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) ' 'Chrome/66.0.3359.139 Safari/537.36' } # uid self.uid = uid # 昵称 self.nickname = '' # 头像 self.avatar = '' # 总发表文章数 self.articles = 0 # 文章总阅读数 self.total_reading = 0 # 关注人数 self.watch = 0 # 粉丝人数 self.fans = 0 # 写作字数 self.words = 0 # 收货喜欢数 self.likes = 0 # 查询总耗时 self.time = 0 # 用户是否存在的标志 self.exit = True # 判断用户是否存在。存在则抓取并统计数据,否则修改exit标志 def count(self): start = time.time() url = 'https://www.jianshu.com/u/' + self.uid # print(url) resp = requests.get(url, headers=self.headers) if resp.status_code == 200: bs = BeautifulSoup(resp.content, 'html.parser', from_encoding='UTF-8') # 头像 avatar = bs.find(class_='avatar') self.avatar = 'https:' + avatar.img['src'] # 昵称 nickname = bs.find(class_='name') self.nickname = nickname.text meta_block = bs.find_all(class_='meta-block') # 关注数 self.watch = int(meta_block[0].a.p.text) # 粉丝数 self.fans = int(meta_block[1].a.p.text) # 总发表文章数 self.articles = int(meta_block[2].a.p.text) # 写作字数 self.words = int(meta_block[3].p.text) # 收获喜欢数 self.likes = int(meta_block[4].p.text) if self.articles != 0: # print(self.articles) meta = bs.find_all(class_='meta') # 每页展示文章数 page_articles = len(meta) # print(page_articles) # 文章展示总页数 pages = int(math.ceil(self.articles / page_articles)) + 1 # 用多线程统计 cpu_count = multiprocessing.cpu_count() # print(cpu_count) pool = multiprocessing.Pool(cpu_count) # 从第一页开始 page = range(1, pages) # 包含每页阅读量的列表 page_reading_list = pool.map(self.page_count, page) # print(page_reading_list) self.total_reading = numpy.sum(page_reading_list) # print('用户:%s 总发表文章数为:%d , 文章总阅读量为: %s' % (input_uid, self.articles, self.total_reading)) else: self.exit = False # print('用户:%s 不存在' % input_uid) end = time.time() self.time = int(end - start) # 每页的阅读量统计 def page_count(self, page): url = 'https://www.jianshu.com/u/' + self.uid + '?page=' + str(page) # print(url) resp = requests.get(url, headers=self.headers) bs = BeautifulSoup(resp.content, 'html.parser', from_encoding='UTF-8') divs = bs.find_all(class_='meta') page_reading = 0 for div in divs: page_reading += int(div.a.text) return page_reading webCount.py:web支持和入口类,包括调用readCount并进行数据渲染 import webimport readCount# 第一个是映射规则,第二个是具体匹配的类urls = ('/(.*)', 'Hello')# 指定模板所在的位置render = web.template.render('templates/')class Hello: @staticmethod def GET(uid): if not uid: uid = '000a530f461c' read_count = readCount.ReadCount(uid) read_count.count() # data是渲染模板的名称 return render.data(read_count)if __name__ == \"__main__\": app = web.application(urls, globals()) app.run() data.html:处理数据和渲染展示 $def with (rc)<html lang=\"zh-CN\"><head> <meta charset=\"UTF-8\"> <meta name=\"viewport\" content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\"> <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"> <title>简书数据统计</title> <!-- Bootstrap --> <link href=\"https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css\" rel=\"stylesheet\"> <!-- Custom styles for this template --> <link href=\"https://blackrockdigital.github.io/startbootstrap-freelancer/css/freelancer.min.css\" rel=\"stylesheet\"> <style> .text { color: rgba(255, 254, 21, 0.96); } .text2 { color: rgba(255, 58, 2, 0.96); } a { color: rgba(255, 254, 21, 0.96); font-size: 2.1rem; text-underline: none; } </style> <!-- HTML5 shim 和 Respond.js 是为了让 IE8 支持 HTML5 元素和媒体查询(media queries)功能 --> <!-- 警告:通过 file:// 协议(就是直接将 html 页面拖拽到浏览器中)访问页面时 Respond.js 不起作用 --> <!--[if lt IE 9]> <script src=\"https://cdn.bootcss.com/html5shiv/3.7.3/html5shiv.min.js\"></script> <script src=\"https://cdn.bootcss.com/respond.js/1.4.2/respond.min.js\"></script> <![endif]--></head><body class=\"bg-primary\"><header class=\"text-white text-center\" style=\"padding-top: 5rem;\">$if rc.exit: <div class=\"container\"> <img class=\"img-fluid mb-5 d-block mx-auto img-circle\" src=\"$rc.avatar\" alt=\"avatar\"> <h2 class=\"mb-0\">$rc.nickname</h2> <hr class=\"star-light\"> <h4 class=\"font-weight-light mb-0\"> 关注 : <span class=\"text\">$rc.watch</span> 人 - 粉丝 : <span class=\"text\">$rc.fans</span> 人 - 文章 : <span class=\"text\">$rc.articles</span> 篇 <p></p> 写作 : <span class=\"text2\">$rc.words</span> 字 - 收获喜欢 : <span class=\"text2\">$rc.likes</span> 次 - 总阅读量 : <span class=\"text2\">$rc.total_reading</span> 次 - 查询总耗时 :<span class=\"text2\">$rc.time</span> 秒 </h4> </div>$else: <h1 class=\"mb-0\">404</h1> <h4>您可以尝试下搜下面这些人</h4><br> <h4 class=\"font-weight-light mb-0\"> <p><a href=\"/y3Dbcz\">简叔</a></p> <p><a href=\"/d9edcb44e2f2\">简黛玉</a></p> <p><a href=\"/1441f4ae075d\">彭小六</a></p> <p><a href=\"/000a530f461c\">happyJared</a></p> </h4></header></body></html> 启动测试  webpy启动的命令是:python webCount.py {port}。其中,端口不是必须的,默认是运行在8080。以上程序已经跑在个人的服务器上,测试地址是:http://jianshu.mariojd.cn/{uid} ,这里uid是用户的唯一标志(非必填有默认值),你也可以通过在个人主页的地址栏中获取自己的。 结尾总结  不得不感慨,Python能做的领域确实很广,关键是代码量又少。像写个自动化脚本,It can;写个小爬虫,It can;写个web应用,It can;…这也是为什么一直想把Python当做我的第二门编程语言。这次写这个简书小爬虫也是一波三折,来来回回也折腾了差不多一天时间,还是基础不够扎实,代码不够熟练。写完代码后也有仔细想过,觉得有机会有时间的话还可以做得更细一点,就像大数据分析一样,同样也是一个个用户数据慢慢堆起来的,所以目前来看还可以考虑扩展以下几点: 获得用户加入简书的时间(假设以第一篇文章发表时间为参考) 通过用户发表的总文章数,获取用户平均每年、每月发表多少文章数 最高阅读量、打赏数、喜欢数、留言量的文章 统计用户获得的总打赏笔数 当前用户发表文章最活跃的时间段 至今为止加入简书多少天 最后一次发表文章的时间 评论总数 ……   如果真完成了以上的扩展,像我的话肯定会趁机再扩展学习下Python的图表库,像matplotlib;或者使用ECharts、HighCharts、D3这样的前端报表库,相信这样会让这些数据显得更加的生动。大概也就这么多吧,本人目前也已经辞职近一个月了(时间过得真TM快,转眼就毕业了),刚好最近也在着手准备找下一份工作,按计划会先写一个招聘平台(主要是拉钩和Boss)的关键字筛选小爬虫,也算是帮助筛选一部分的求职公司,我瞄准的关键字是:“健身房”,就是不知道真的有没有这么幸运,感兴趣的敬请期待。 注:以上所有程序代码已经发布到我的GitHub仓库","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.mariojd.cn/categories/Python/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.mariojd.cn/tags/python/"},{"name":"spider","slug":"spider","permalink":"https://blog.mariojd.cn/tags/spider/"}]},{"title":"Python + Selenium 自动发布文章(四):加入 bat 脚本","slug":"Python + Selenium 自动发布文章(四):加入 bat 脚本","date":"2018-05-19","updated":"2018-09-29","comments":true,"path":"python-and-selenium-automatically-publish-articles-about-add-bat-script.html","link":"","permalink":"https://blog.mariojd.cn/python-and-selenium-automatically-publish-articles-about-add-bat-script.html","excerpt":"","keywords":"","text":"写在前面  这是本系列的第四篇文章,同时也是最后一篇。有关于Bat脚本和自动发布博客的内容,不太了解的可以先看看之前写的文章。这篇文章是介绍如何整合Bat脚本来一键自动发博客,包括我的Hexo博客、简书、开源中国和CSDN。好了,在那之前我们先稍微调整一下代码。 代码改造  这里只需要调整一下main.py即可,引入sys这个moudle,到时我们通过Bat命令占位符的形式进行传参,参数是Markdown文件名称(含文件后缀.md),参考代码如下: import reimport sysimport csdnimport jianshuimport oschinaclass Main(object): # init def __init__(self, file): self.title = '' self.content = '' self.category = '' self.tags = '' # OsChina的系统分类, 设个默认值 self.osChina_sys_category = '编程语言' # CSDN的文章分类, 设个默认值 self.csdn_article_category = '原创' # CSDN的博客分类, 设个默认值 self.csdn_blog_category = '后端' self.read_file(file) # 读取MD中的title, content, self_category, self_tags, osChina_sys_category, csdn_article_category, csdn_blog_category def read_file(self, markdown_file): with open(markdown_file, 'r', encoding='UTF-8') as f: self.content = f.read().split('-->\\n')[1] # 重置文件指针偏移量 f.seek(0) for line in f.readlines(): if self.judge('title: ', line): self.title = line.split('title: ')[1].strip('\\n') elif self.judge('self_category: ', line): self.category = line.split('self_category: ')[1].strip('\\n') elif self.judge('self_tags: ', line): self.tags = line.split('self_tags: ')[1].strip('\\n') elif self.judge('osChina_sys_category: ', line): self.osChina_sys_category = line.split('osChina_sys_category: ')[1].strip('\\n') elif self.judge('csdn_article_category: ', line): self.csdn_article_category = line.split('csdn_article_category: ')[1].strip('\\n') elif self.judge('csdn_blog_category: ', line): self.csdn_blog_category = line.split('csdn_blog_category: ')[1].strip('\\n') # 正则匹配判断, 获取的信息需保证其标志的唯一性 @staticmethod def judge(rule, line): if re.search(rule, line) is None: return False if 'self.' in line: return False return Trueif __name__ == '__main__': # sys.argv[0] = 当前文件名,这里是main.py md_file = sys.argv[1] # md_file = 'auto.md' print(\"Markdown File is \", md_file) timeout = 10 main = Main(md_file) # 开源中国 osChina = oschina.OsChina() osChina.post(main, timeout) # 简书 jian_shu = jianshu.JianShu() jian_shu.post(main, timeout) # CSDN csdn = csdn.CSDN() csdn.post(main, timeout) Bat 脚本  和以往一样,Bat脚本还是放在hexo的安装目录下,命名deploy.bat。Python + Selenium系列的所有代码已经上传到了我的GitHub仓库,同时为了代码的同步和方便,这里也直接在source\\_posts目录下克隆了该仓库。Okay,关于这个一键部署的脚本参考如下: :: 拉取最新的部署代码cd source/_posts/auto-postgit pull origin master:: 拉取最新的Markdown文章cd ../git pull origin master:: 输入文件名(最好用双引号括起来)才执行一键部署set /p fileName=if defined fileName (start python auto-post/main.py %fileName%):: 这里是部署hexocd ../../hexo g -d 运行效果 可优化点  单从上面的效果图来看,代码就有很大的优化空间了。下面列出去的几点是目前发现已知的可优化点,计划等有时间再来慢慢改进: 部分页面加载等待时间过长:在大多数情况下,其实需要处理元素已经完全加载显示出来了,但实际页面还在等待css、js等资源的完全加载;(可以设置最长加载等待时间) 录入内容可能会很慢:当正文内容很长的时候,用selenium的send_keys方法其实是比较慢的,毕竟这种方式还是一个个字符的输入;(可以利用剪贴板的复制粘贴功能) 可选第三方授权登录方式有限:如代码所示,目前仅有QQ授权的方式;(可以扩展新浪微博、GitHub、微信等渠道进行授权登录) 发布文章的操作是串行的:目前deploy.bat脚本只是整合了这几个平台的发布文章操作,执行顺序是按代码顺序来的;(可以使用多线程技术同步完成多平台的发布操作) 自定义分类的局限:目前只能输入已经自定义好的,其实应该先判断,当分类不存在时,就新增分类,然后再进行选择。 系列小结  小结作个简单说明,本系列所演示的代码目前仅支持发布(单篇)文章,不支持文章的更新和多篇文章同时发布,只能说代码是死的,人才是活的,有时间有兴趣的可以自己捣腾一下。最后,结合这几个平台,附上Markdown中注释部分参数的说明和可选参数值,本系列的所有代码在GitHub仓库中。 参数名 说明/可选参数值 self_category 自定义的分类名称。在OSChina和CSDN叫个人分类,简书里叫文集 self_tags 文章的标签。OSChina和CSDN里用到,多个标签以,(中文逗号)分隔 osChina_sys_category OSChina的系统分类。可选参数值有:移动开发、前端开发、人工智能、服务端开发/管理、游戏开发、编程语言(默认值)、数据库、企业开发、图像/多媒体、系统运维、软件工程、大数据、云计算、开源硬件、区块链、其他类型 csdn_article_category CSDN的文章类型。可选参数值有:原创(默认值)、转载、翻译 csdn_blog_category CSDN的博客分类。可选参数值有:人工智能、移动开发、物联网、架构、云计算/大数据、游戏开发、运维、数据库、前端、后端、编程语言(默认值)、研发管理、安全、程序人生、区块链、音视频开发、资讯、计算机理论与基础","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.mariojd.cn/categories/Python/"},{"name":"Selenium","slug":"Python/Selenium","permalink":"https://blog.mariojd.cn/categories/Python/Selenium/"},{"name":"Windows Batch","slug":"Python/Selenium/Windows-Batch","permalink":"https://blog.mariojd.cn/categories/Python/Selenium/Windows-Batch/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.mariojd.cn/tags/python/"},{"name":"selenium","slug":"selenium","permalink":"https://blog.mariojd.cn/tags/selenium/"},{"name":"bat","slug":"bat","permalink":"https://blog.mariojd.cn/tags/bat/"}]},{"title":"Python + Selenium 自动发布文章(三):CSDN","slug":"Python + Selenium 自动发布文章(三):CSDN","date":"2018-05-18","updated":"2018-09-29","comments":true,"path":"python-and-selenium-automatically-publishes-articles-in-csdn.html","link":"","permalink":"https://blog.mariojd.cn/python-and-selenium-automatically-publishes-articles-in-csdn.html","excerpt":"","keywords":"","text":"写在开始  这是本系列的第三篇文章,主要介绍如何用Python+Selenium 自动发布CSDN博客,一些必要的条件在之前的文章里面已经提到过,这里也不再重复。 使用说明  同样的,还是需要先分析下CSDN写博客的界面(记得设置默认编辑器为Markdown)。   从上面两张图可以看到,在CSDN平台写一篇博客,依次需要填入标题和内容信息。如果是发布博客操作,还需要选择文章类型、博客分类、个人分类(可选)以及填写文章标签(可选)等信息。  我们结合auto.md的内容进行分析,标题定义在title处;正文内容通过匹配-->\\n获取;剩下文章类型、博客分类、文章标签和个人分类,按规则已经提前定义在注释中,分别对应csdn_article_category、csdn_blog_category、self_tags和self_category。 代码说明  main.py:程序入口类,主要负责正则匹配解析Markdown和调用post发布文章 import reimport csdnimport linecacheclass Main(object): # init def __init__(self, file): self.title = '' self.content = '' self.category = '' self.tags = '' # OsChina的系统分类, 设个默认值 self.osChina_sys_category = '编程语言' # CSDN的文章分类, 设个默认值 self.csdn_article_category = '原创' # CSDN的博客分类, 设个默认值 self.csdn_blog_category = '后端' self.read_file(file) # 读取MD中的title, content, self_category, self_tags, osChina_sys_category, csdn_article_category, csdn_blog_category def read_file(self, markdown_file): self.title = linecache.getline(markdown_file, 2).split('title: ')[1].strip('\\n') with open(markdown_file, 'r', encoding='UTF-8') as f: self.content = f.read().split('-->\\n')[1] # 重置文件指针偏移量 f.seek(0) for line in f.readlines(): if re.search('self_category: ', line) is not None: self.category = line.split('self_category: ')[1].strip('\\n') elif re.search('self_tags: ', line) is not None: self.tags = line.split('self_tags: ')[1].strip('\\n') elif re.search('osChina_sys_category: ', line) is not None: self.osChina_sys_category = line.split('osChina_sys_category: ')[1].strip('\\n') elif re.search('csdn_article_category: ', line) is not None: self.csdn_article_category = line.split('csdn_article_category: ')[1].strip('\\n') elif re.search('csdn_blog_category: ', line) is not None: self.csdn_blog_category = line.split('csdn_blog_category: ')[1].strip('\\n')if __name__ == '__main__': md_file = 'auto.md' print(\"Markdown File is \", md_file) timeout = 10 main = Main(md_file) # CSDN csdn = csdn.CSDN() csdn.post(main, timeout)   authorize.py:目前仅实现了用qq进行授权登录的方法 from selenium.webdriver.support.wait import WebDriverWait# QQ授权登录, 使用前提是QQ客户端在线def qq(driver, timeout): # 切换到最新打开的窗口 window_handles = driver.window_handles driver.switch_to.window(window_handles[-1]) print('qq authorize title is ', driver.title) # 切换iframe iframe = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_id('ptlogin_iframe')) driver.switch_to.frame(iframe) # 点击头像进行授权登录 login = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_xpath('//*[@id=\"qlogin_list\"]/a[1]')) login.click()   csdn.py:这个是CSDN自动写(发)博客的核心类 import timeimport authorizefrom selenium import webdriverfrom selenium.webdriver.support.ui import Selectfrom selenium.webdriver.support.wait import WebDriverWaitfrom selenium.webdriver.common.action_chains import ActionChains# CSDNclass CSDN(object): @staticmethod def post(main, timeout, self_timeout=5): # 1.账号密码 account = 'xxx' password = 'xxx' # 2.跳转登陆 login = 'https://passport.csdn.net/account/login' driver = webdriver.Chrome() driver.get(login) # 3.窗口最大化 driver.maximize_window() # 4.使用账号密码登陆 # login_by_account = WebDriverWait(driver, timeout).until( # lambda d: d.find_element_by_xpath('/html/body/div[3]/div/div/div[2]/div/h3/a')) # login_by_account.click() # time.sleep(self_timeout) # driver.find_element_by_id('username').send_keys(account) # driver.find_element_by_id('password').send_keys(password) # driver.find_element_by_xpath('//*[@id=\"fm1\"]/input[8]').click() # 4.使用QQ授权登录 driver.find_element_by_id('qqAuthorizationUrl').click() driver.close() authorize.qq(driver, timeout) # 5.点击\"写博客\" write_blog = WebDriverWait(driver, timeout).until( lambda d: d.find_element_by_xpath('/html/body/div[1]/div/div/ul/li[3]/a')) write_blog.click() driver.close() window_handles = driver.window_handles driver.switch_to.window(window_handles[-1]) # 6.点击\"开始写作\" start = WebDriverWait(driver, timeout).until( lambda d: d.find_element_by_xpath('//*[@id=\"btnStart\"]')) start.click() # 7.填写标题, 内容 time.sleep(self_timeout) title = driver.find_element_by_xpath('//*[@id=\"txtTitle\"]') title.clear() title.send_keys(main.title) # PS:下面这行代码很重要,卡了好久才解决┭┮﹏┭┮,不信可以试试注释掉这句 ActionChains(driver).click(title).perform() content = driver.find_element_by_xpath('//*[@id=\"wmd-input\"]/div[1]') content.clear() content.send_keys(main.content) # 8.保存草稿 # driver.find_element_by_xpath('//*[@id=\"editorBox\"]/div[2]/div/button[2]').click() # 8.发布文章 driver.find_element_by_xpath('//*[@id=\"editorBox\"]/div[2]/div/button[1]').click() # 9.若第8步选择\"发布文章\", 往下需依次填写标签,个人分类,文章类型,博客分类 tags = main.tags.split(',') add_tag = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_id('addTag')) for i, tag in enumerate(tags): add_tag.click() tag_input = WebDriverWait(driver, timeout).until( lambda d: d.find_element_by_xpath('//*[@id=\"tagBox\"]/div[' + str(i + 1) + ']/span')) tag_input.send_keys(tag) classify = driver.find_elements_by_class_name('form-check-label') for c in classify: html = c.get_attribute('innerHTML') if main.category in html: c.click() select = Select(driver.find_element_by_id('selType')) select.select_by_visible_text(main.csdn_article_category) select = Select(driver.find_element_by_id('radChl')) select.select_by_visible_text(main.csdn_blog_category) # 10.保存草稿 driver.find_element_by_xpath('//*[@id=\"meditor_box\"]/div[3]/div/div[6]/input[2]').click() # 10.发布文章 # driver.find_element_by_xpath('//*[@id=\"meditor_box\"]/div[3]/div/div[6]/input[3]').click() time.sleep(self_timeout)   CSDN支持账号密码登录,也可以用qq授权的方式,后期只需要扩展authorize.py的功能,就可以支持更多的第三方平台进行授权登录。 运行效果  还是来看看运行效果图吧,这里仅测试保存草稿。 写在最后  在CSDN平台自动写文章的流程大概也就这样,同样这不是唯一的办法,也不敢保证程序可以一直正常运行下去。总而言之,这个花的时间是最多,因为一直卡在了某一点上,不过还好最后还是解决了。本系列还有最后一篇,将介绍如何结合bat脚本在多个平台同时发布文章,以及对系列做一个简单的总结,敬请期待。","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.mariojd.cn/categories/Python/"},{"name":"Selenium","slug":"Python/Selenium","permalink":"https://blog.mariojd.cn/categories/Python/Selenium/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.mariojd.cn/tags/python/"},{"name":"selenium","slug":"selenium","permalink":"https://blog.mariojd.cn/tags/selenium/"},{"name":"csdn","slug":"csdn","permalink":"https://blog.mariojd.cn/tags/csdn/"}]},{"title":"Python + Selenium 自动发布文章(二):简书","slug":"Python + Selenium 自动发布文章(二):简书","date":"2018-05-17","updated":"2018-09-29","comments":true,"path":"python-and-selenium-automatic-publishing-article-in-jianshu.html","link":"","permalink":"https://blog.mariojd.cn/python-and-selenium-automatic-publishing-article-in-jianshu.html","excerpt":"","keywords":"","text":"写在开始  本篇介绍用Python+Selenium 自动发布简书文章,一些必要的前置准备说明在上篇文章里面有提到,这里就不再重复了。 使用说明  同样的,还是需要先分析下简书写博客的界面(记得设置默认编辑器为Markdown)。   从上图可以看到,在简书写一篇博客,需要依次选择分类(也就是文集),新建文章,然后填入标题和内容。  结合auto.md的内容进行分析,标题有了,定义在title处;正文内容同样通过匹配-->\\n获取。剩下分类,按规则已经定义在注释里了(self_category)。 代码说明  main.py:程序入口类,主要负责正则匹配解析Markdown和调用post发布文章 import reimport jianshuimport linecacheclass Main(object): # init def __init__(self, file): self.title = '' self.content = '' self.category = '' self.tags = '' # OsChina的系统分类, 设个默认值 self.osChina_sys_category = '编程语言' # CSDN的文章分类, 设个默认值 self.csdn_article_category = '原创' # CSDN的博客分类, 设个默认值 self.csdn_blog_category = '后端' self.read_file(file) # 读取MD中的title, content, self_category, self_tags, osChina_sys_category, csdn_article_category, csdn_blog_category def read_file(self, markdown_file): self.title = linecache.getline(markdown_file, 2).split('title: ')[1].strip('\\n') with open(markdown_file, 'r', encoding='UTF-8') as f: self.content = f.read().split('-->\\n')[1] # 重置文件指针偏移量 f.seek(0) for line in f.readlines(): if re.search('self_category: ', line) is not None: self.category = line.split('self_category: ')[1].strip('\\n') elif re.search('self_tags: ', line) is not None: self.tags = line.split('self_tags: ')[1].strip('\\n') elif re.search('osChina_sys_category: ', line) is not None: self.osChina_sys_category = line.split('osChina_sys_category: ')[1].strip('\\n') elif re.search('csdn_article_category: ', line) is not None: self.csdn_article_category = line.split('csdn_article_category: ')[1].strip('\\n') elif re.search('csdn_blog_category: ', line) is not None: self.csdn_blog_category = line.split('csdn_blog_category: ')[1].strip('\\n')if __name__ == '__main__': md_file = 'auto.md' print(\"Markdown File is \", md_file) timeout = 10 main = Main(md_file) # 简书 jian_shu = jianshu.JianShu() jian_shu.post(main, timeout)   authorize.py:目前仅实现了用qq进行授权登录的方法 from selenium.webdriver.support.wait import WebDriverWait# QQ授权登录, 使用前提是QQ客户端在线def qq(driver, timeout): # 切换到最新打开的窗口 window_handles = driver.window_handles driver.switch_to.window(window_handles[-1]) print('qq authorize title is ', driver.title) # 切换iframe iframe = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_id('ptlogin_iframe')) driver.switch_to.frame(iframe) # 点击头像进行授权登录 login = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_xpath('//*[@id=\"qlogin_list\"]/a[1]')) login.click()   jianshu.py:这个是简书自动写(发)博客的核心类 import timeimport authorizefrom selenium import webdriverfrom selenium.webdriver.support.wait import WebDriverWait# 简书class JianShu(object): @staticmethod def post(main, timeout, self_timeout=3): # 1.跳转登陆 login = 'https://www.jianshu.com/sign_in' driver = webdriver.Chrome() driver.get(login) # 2.窗口最大化 driver.maximize_window() # 3.使用QQ授权登录 driver.find_element_by_xpath('/html/body/div[1]/div[2]/div/div/ul/li[3]/a/i').click() driver.close() authorize.qq(driver, timeout) # 4.点击\"写文章\" write_blog = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_xpath('/html/body/nav/div/a[2]')) write_blog.click() driver.close() window_handles = driver.window_handles driver.switch_to.window(window_handles[-1]) # 5.点击指定分类 classify = WebDriverWait(driver, timeout).until(lambda d: d.find_elements_by_class_name('_3DM7w')) for c in classify: html = c.get_attribute('innerHTML') if main.category in html: c.click() else: # TODO 如果分类不存在,还可以直接新建分类 pass # 6.点击'新建文章' time.sleep(self_timeout) new_article = WebDriverWait(driver, timeout).until( lambda d: d.find_element_by_xpath('//*[@id=\"root\"]/div/div[2]/div[1]/div/div/div/div[1]/i')) new_article.click() article = WebDriverWait(driver, timeout).until( lambda d: d.find_element_by_xpath('//*[@id=\"root\"]/div/div[2]/div[1]/div/div/div/ul/li[1]')) article.click() # 7.填写标题, 内容 time.sleep(self_timeout) title = driver.find_element_by_class_name('_24i7u') title.clear() title.send_keys(main.title) content = driver.find_element_by_id('arthur-editor') content.clear() content.send_keys(main.content) # 8.保存草稿 driver.find_element_by_xpath('//*[@id=\"root\"]/div/div[2]/div[2]/div/div/div/div/ul/li[8]/a').click() # 8.发布文章 # driver.find_element_by_xpath('//*[@id=\"root\"]/div/div[2]/div[2]/div/div/div/div/ul/li[1]/a').click()   其实简书也是支持账号密码登录的,但无奈这种方式登录还有文字验证层,感觉比较棘手,目前也没研究怎么解决,所以先用qq授权的方式登录吧。 运行效果  还是来看看运行效果图吧,这里测试的是保存草稿。 写在最后  在简书自动写文章的思路大概就这样,同样这也不是唯一的办法,根据代码自己做调整即可,网页的结构也可能会改变,故不保证程序可以一直正常运行。最后,下一篇继续介绍如何在CSDN自动写(发)文章。","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.mariojd.cn/categories/Python/"},{"name":"Selenium","slug":"Python/Selenium","permalink":"https://blog.mariojd.cn/categories/Python/Selenium/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.mariojd.cn/tags/python/"},{"name":"selenium","slug":"selenium","permalink":"https://blog.mariojd.cn/tags/selenium/"},{"name":"简书","slug":"简书","permalink":"https://blog.mariojd.cn/tags/简书/"}]},{"title":"Python + Selenium 自动发布文章(一):开源中国","slug":"Python + Selenium 自动发布文章(一):开源中国","date":"2018-05-16","updated":"2018-09-29","comments":true,"path":"python-and-selenium-automatically-publish-articles-in-oschina.html","link":"","permalink":"https://blog.mariojd.cn/python-and-selenium-automatically-publish-articles-in-oschina.html","excerpt":"","keywords":"","text":"写在开始  还是说说出这个系列的起因吧。之前写完或是修改了Markdown文章,我还分别需要在多个平台进行发布或是更新维护这些内容,这些平台目前包括我的博客、简书、开源中国和CSDN,其实早就想过用比较自动化的形式来解决,无奈有技术、时间、精力等各方面原因的限制。废话不多说吧,直奔今天的主题,本文主要介绍如何用Python和Selenium写(发)开源中国的博客。 准备说明 一定的Python基础知识 一定的Selenium相关知识 开发环境说明:Python v3.6.4,Selenium v3.8.1 PS:Selenium操纵浏览器是依赖于浏览器驱动程序的,下面贴出的是谷歌和火狐浏览器驱动程序的下载地址。 Chrome ( chromedriver ) Firefox ( geckodriver ) 官方下载 官方下载 淘宝镜像 淘宝镜像 备用下载 备用下载 使用说明  下面是示例代码中用到的auto.md文件内容,自动发布文章也还是需要遵循一定的规则,所以以下有几点是必须说明的:  1. [//]: # ()是Markdown注释的一种写法,注释内容写在小括号内;  1.< !-- -->是HTML注释的一种写法,由于Markdown写法的注释有兼容性问题,所以在此调整一下(注意<和!之间实际上是没有空格的,又是为了兼容某些平台的Markdown识别,好想o(╥﹏╥)o);  2. auto.md中间注释部分的内容,用于匹配获得这几个平台的分类和标签等信息;  3. -->\\n仅用于划分并匹配获取正文部分内容。 ---title: 自动发布测试文章date: 2018-05-16categories: - 测试author: Jared Qiutags: - 标签cover_picture: https://images.unsplash.com/photo-1520095972714-909e91b038e5?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=1110ecf3ce9e4184d4676c54dec0032d&auto=format&fit=crop&w=500&q=60top: 1---<!-- self_category: 开源 self_tags: 博客,docker osChina_sys_category: 其他类型 csdn_article_category: 原创 csdn_blog_category: 编程语言-->### 自动发布&emsp;&emsp;自动发布文章。。### 参考地址> [happyJared - 博客](https://blog.mariojd.cn/)   下面的截图是开源中国撰写博客的界面(记得设置默认编辑器为Markdown)。   从上图可以看到,在开源中国写一篇博客,需要依次录入标题、摘要(可选)、内容、标签(可选)和选择分类(自定义的)、系统分类等信息。  结合auto.md的内容进行分析,相信用过hexo的朋友都比较清楚,标题一般定义在title处;摘要因为是可选的,所以这里先忽略不处理;正文内容我们通过匹配-->\\n就可以获取。剩下标签,自定义分类和系统分类,按规则需要提前定义在注释里,分别对应self_tags,self_category和osChina_sys_category。 代码说明  main.py:程序入口类,主要负责正则匹配解析Markdown和调用post发布文章 import reimport oschinaimport linecacheclass Main(object): # init def __init__(self, file): self.title = '' self.content = '' self.category = '' self.tags = '' # OsChina的系统分类, 设个默认值 self.osChina_sys_category = '编程语言' # CSDN的文章分类, 设个默认值 self.csdn_article_category = '原创' # CSDN的博客分类, 设个默认值 self.csdn_blog_category = '后端' self.read_file(file) # 读取MD中的title, content, self_category, self_tags, osChina_sys_category, csdn_article_category, csdn_blog_category def read_file(self, markdown_file): self.title = linecache.getline(markdown_file, 2).split('title: ')[1].strip('\\n') with open(markdown_file, 'r', encoding='UTF-8') as f: self.content = f.read().split('-->\\n')[1] # 重置文件指针偏移量 f.seek(0) for line in f.readlines(): if re.search('self_category: ', line) is not None: self.category = line.split('self_category: ')[1].strip('\\n') elif re.search('self_tags: ', line) is not None: self.tags = line.split('self_tags: ')[1].strip('\\n') elif re.search('osChina_sys_category: ', line) is not None: self.osChina_sys_category = line.split('osChina_sys_category: ')[1].strip('\\n') elif re.search('csdn_article_category: ', line) is not None: self.csdn_article_category = line.split('csdn_article_category: ')[1].strip('\\n') elif re.search('csdn_blog_category: ', line) is not None: self.csdn_blog_category = line.split('csdn_blog_category: ')[1].strip('\\n')if __name__ == '__main__': md_file = 'auto.md' print(\"Markdown File is \", md_file) timeout = 10 main = Main(md_file) # 开源中国 osChina = oschina.OsChina() osChina.post(main, timeout)   authorize.py:目前仅实现了用qq进行授权登录的方法 from selenium.webdriver.support.wait import WebDriverWait# QQ授权登录, 使用前提是QQ客户端在线def qq(driver, timeout): # 切换到最新打开的窗口 window_handles = driver.window_handles driver.switch_to.window(window_handles[-1]) print('qq authorize title is ', driver.title) # 切换iframe iframe = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_id('ptlogin_iframe')) driver.switch_to.frame(iframe) # 点击头像进行授权登录 login = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_xpath('//*[@id=\"qlogin_list\"]/a[1]')) login.click()   oschina.py:这个是开源中国自动写(发)博客的核心类 import authorizefrom selenium import webdriverfrom selenium.webdriver.support.wait import WebDriverWaitfrom selenium.webdriver.common.action_chains import ActionChains# 开源中国class OsChina(object): @staticmethod def post(main, timeout): # 1.账号密码 account = 'xxx' password = 'xxx' # 2.跳转登陆 login = 'https://www.oschina.net/home/login' driver = webdriver.Chrome() driver.get(login) # 3.窗口最大化 driver.maximize_window() # 4.使用QQ授权登录 driver.find_element_by_xpath('/html/body/section/div/div[2]/div[2]/div/div[2]/a[4]').click() authorize.qq(driver, timeout) # 4.使用账号密码登陆 # driver.find_element_by_id('userMail').send_keys(account) # driver.find_element_by_id('userPassword').send_keys(password) # driver.find_element_by_xpath('//*[@id=\"account_login\"]/form/div/div[5]/button').click() # 5.移到\"我的空间\", 点击\"我的博客\" my_space = WebDriverWait(driver, timeout).until(lambda d: d.find_element_by_xpath('//*[@id=\"MySpace\"]')) ActionChains(driver).move_to_element(my_space).perform() driver.find_element_by_xpath('/html/body/header/div/div[2]/div/div[2]/div/ul/li[4]/a').click() # 6.点击\"写博客\" write_blog = WebDriverWait(driver, timeout).until( lambda d: d.find_element_by_xpath('/html/body/div/div/div/div/div[1]/div[1]/div[4]/a')) write_blog.click() # 7.选择自定义分类, 系统分类 classify = WebDriverWait(driver, timeout).until(lambda d: d.find_elements_by_class_name('select-opt')) for c in classify: html = c.get_attribute('innerHTML') if main.category in html: if 'span' in html: # 自定义分类 data_value = c.get_attribute('data-value') js = 'document.getElementById(\"self_sort\").value=' + data_value driver.execute_script(js) else: if main.osChina_sys_category == html: # 系统分类 data_value = c.get_attribute('data-value') js = 'document.getElementById(\"sys_sort\").value=' + data_value driver.execute_script(js) # 8.填写标题, 内容和标签 title = driver.find_element_by_xpath('//*[@id=\"title\"]') title.clear() title.send_keys(main.title) content = driver.find_element_by_id('mdeditor') content.clear() content.send_keys(main.content) tags = driver.find_element_by_xpath('//*[@id=\"blog-form\"]/div[2]/div/div[3]/div[1]/div[2]/div[2]/input') tags.clear() tags.send_keys(main.tags) # 9.保存草稿 driver.find_element_by_xpath('//*[@id=\"blog-form\"]/div[3]/div/button[1]').click() # 9.发布文章 # driver.find_element_by_xpath('//*[@id=\"blog-form\"]/div[3]/div/button[2]').click()   从代码注释可以看到,目前支持账号密码和QQ授权两种方式登录,支持保存草稿或发布文章操作。 运行效果  多说无益,来看看运行效果图吧,测试一下保存草稿。 写在最后  总之,在开源中国自动写文章的思路大概就这样,不过这也绝对不是唯一的办法,大家完全可以根据代码自己做调整,而且网页的结构可能会发生改变,这里也不敢保证程序可以一直正常运行下去。好了,下一篇介绍如何在简书自动写(发)文章。","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.mariojd.cn/categories/Python/"},{"name":"Selenium","slug":"Python/Selenium","permalink":"https://blog.mariojd.cn/categories/Python/Selenium/"}],"tags":[{"name":"python","slug":"python","permalink":"https://blog.mariojd.cn/tags/python/"},{"name":"selenium","slug":"selenium","permalink":"https://blog.mariojd.cn/tags/selenium/"},{"name":"oschina","slug":"oschina","permalink":"https://blog.mariojd.cn/tags/oschina/"}]},{"title":"Chocolatey,Windows 下的包管理器","slug":"Chocolatey,Windows 下的包管理器","date":"2018-05-14","updated":"2019-06-02","comments":true,"path":"package-manager-chocolatey-under-windows.html","link":"","permalink":"https://blog.mariojd.cn/package-manager-chocolatey-under-windows.html","excerpt":"","keywords":"","text":"关于Chocolatey  Chocolatey(中文译:巧克力味)是Windows平台下的一款包管理工具,类似于Linux平台的apt-get和yum。第一次接触到Chocolatey的起因是因为在Git官网下载Git的时候,发现这玩意下载的速度超级慢,而且根本没办法一次性顺利下载下来。废话不多说,下面我们来看看怎么安装和使用Chocolatey。 安装Chocolatey  Chocolatey目前不支持类似于.msi这种类型文件的安装包安装,官网提供的是另外两种命令行的方式,一种是CMD,另外一种是PowerShell。Chocolatey的安装可以参考官网的chocolatey安装,但前提是要满足以下几点安装环境要求。 Windows 7+ / Windows Server 2003+ PowerShell v2+ .NET Framework 4+ (the installation will attempt to install .NET 4.0 if you do not have it installed)   PowerShell是cmd的超集。这么理解,cmd能做的事情,PowerShell都能做;cms做不了的事情,PowerShell也都能做。关于PowerShell,点击Windows Power Shell可以了解更多   提示:Chocolatey的两种安装办法都需要超级管理员权限,不然安装的时候会报错,提示权限不够(默认Chocolatey的安装路径是在C盘下,权限不够会导致某些安装文件无法写入) CMD安装  cmd一般安装在C:\\Windows\\System32下,找到后选择鼠标右键,以管理员身份运行。使用cmd安装Chocolatey的命令如下: @\"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\" -NoProfile -InputFormat None -ExecutionPolicy Bypass -Command \"iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))\" && SET \"PATH=%PATH%;%ALLUSERSPROFILE%\\chocolatey\\bin\" PowerShell安装  powershell一般安装在C:\\Windows\\System32\\WindowsPowerShell\\v1.0,找到后同样选择鼠标右键,然后以管理员身份运行。使用powershell安装Chocolatey的步骤如下: 运行Get-ExecutionPolicy。如果返回Restricted,则运行Set-ExecutionPolicy AllSigned或Set-ExecutionPolicy Bypass -Scope Process; 运行如下命令 iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))   提示:如果用PowerShell形式安装,最好确认当前PowerShell的版本大于v1.0,否则更推荐大家使用CMD的形式来进行安装。 测试Chocolatey  输入choco,如果出现以下情况,说明安装已经成功。   提示:如果遇到下面这种错误提示,只需要执行提示内的命令choco feature enable -n useFipsCompliantChecksums即可。 使用Chocolatey  正如你所看到的,Chocolatey是用命令choco来下载、管理、安装和升级包的。下面举几个示例,更多的用法可以查看chocolatey官网。 升级Chocolatey:choco upgrade chocolatey 查看包列表:choco list 安装Git:choco install git 升级Git:choco upgrade git 写在最后  工具的好坏都是因人而异的,所以现在很多东西大家都是褒贬不一的。就拿Chocolatey来说,这个工具对本人来说还是挺好用的,起码解决了升级Git版本的烦恼! 参考链接 Windows 系统下使用包管理器Chocolateychocolatey 安装报错失败解决办法","categories":[{"name":"Chocolatey","slug":"Chocolatey","permalink":"https://blog.mariojd.cn/categories/Chocolatey/"}],"tags":[{"name":"Chocolatey","slug":"Chocolatey","permalink":"https://blog.mariojd.cn/tags/Chocolatey/"},{"name":"Windows","slug":"Windows","permalink":"https://blog.mariojd.cn/tags/Windows/"},{"name":"开源","slug":"开源","permalink":"https://blog.mariojd.cn/tags/开源/"}]},{"title":"Hexo,使用 bat 脚本部署文章","slug":"Hexo,使用 bat 脚本部署文章","date":"2018-05-14","updated":"2019-06-02","comments":true,"path":"deploying-the-article-using-the-bat-script-for-hexo.html","link":"","permalink":"https://blog.mariojd.cn/deploying-the-article-using-the-bat-script-for-hexo.html","excerpt":"","keywords":"","text":"写在前面  熟悉Hexo的朋友都知道,写好的Markdown文章应该放到hexo安装目录的source\\_posts文件夹下,然后使用命令hexo g -d或者是hexo d -g进行部署。我在使用Hexo的时候,_post目录其实是关联了我的一个远程Github仓库,我习惯于用小书匠这款编辑器来写MD文章(主要是因为小书匠可以关联使用多个平台的图床服务),写好后也可以直接保存到这个远程仓库中。按以前那种方式,我首先需要在_post目录中pull最新的文章,然后再使用hexo g -d命令来部署。老实说,我承认自己是个懒人,之前也捣腾过用Travis CI来解决hexo自动化部署的问题,但就是一直卡在某一步,然后现在也就搁置了,下来如果弄成功了我会再写一篇文章。今天这篇文章是主角是使用bat脚本来部署文章,在那之前我们先了解一下bat。 关于bat  bat是Windows平台下的一种脚本语言,类似于Linux平台下是shell。下面只是举几个简单的例子,点到为止就好,更多关于bat脚本的语法可以查看这里。 语法 解释 echo 表示打印该命令后的字符,如echo hello执行后会打印“hello” echo off 表示在此语句后所有运行的命令都不显示命令本身 ,但本身会显示 @ 与echo off相象,但它是加在每个命令行的最前面,表示运行时不显示这一行的命令行(只能影响当前行) @echo off 组合上两个语法,表示不显示后续执行命令及当前命令 dir c:*.* >a.txt 将c盘文件列表写入a.txt call 用于调用另一个批处理命令或文件(如果不用call而直接调用别的批处理文件,那么执行完那个批处理文件后将无法返回当前文件并执行当前文件的后续命令) call c:\\ucdos\\ucdos.bat 调用ucdos.bat脚本 pause 暂停批处理的执行并在屏幕上显示Press any key to continue...的提示,等待用户按任意键后继续 rem 用于注释,也可以用 ::代替 start 调起另外一个窗口执行当前行命令 实战部署  下面贴出的这段代码,也是我用来部署hexo文章的bat脚本(在hexo安装目录下新建deploy.bat,拷贝代码,每次双击运行即可),用Sublime看会比较清晰一点。 :: 左边的两个冒号和rem的效果是类似的,都是注释的作用:: 下面这段代码已经注释,如果去掉rem,那么包含echo off本身和后面的call命令都不会再打印rem @echo off :: call表示在当前窗口执行后面的命令或调用其它脚本call cd source/_postsgit pull origin mastercd ../../hexo g -d 写在最后  所谓好的工具都是为了提高效率而生的,后续本人可能要维护多个平台的文章发表,所以计划用Python写一些自动化发布文章的脚本,然后再结合bat脚本进行调用,感兴趣的小伙伴欢迎继续关注! 参考文章 Windows Batch ScriptingBAT脚本编写教程Windows .bat 脚本简单用法介绍","categories":[{"name":"hexo","slug":"hexo","permalink":"https://blog.mariojd.cn/categories/hexo/"}],"tags":[{"name":"hexo","slug":"hexo","permalink":"https://blog.mariojd.cn/tags/hexo/"},{"name":"bat脚本","slug":"bat脚本","permalink":"https://blog.mariojd.cn/tags/bat脚本/"},{"name":"开源博客","slug":"开源博客","permalink":"https://blog.mariojd.cn/tags/开源博客/"}]},{"title":"Hexo,自定义博客主题","slug":"Hexo,自定义博客主题","date":"2018-05-04","updated":"2018-09-29","comments":true,"path":"custom-blog-theme-for-hexo.html","link":"","permalink":"https://blog.mariojd.cn/custom-blog-theme-for-hexo.html","excerpt":"","keywords":"","text":"  hexo搭建好后,默认的主题叫 landscape。可是,如果你不想自己的博客就这么干巴巴的,那就去Hexo Themes里面挑一个喜欢的吧,然后换掉它。   从上图可以看到,Hexo Themes 目前已经提供了超过200个主题供大家选择,这些主题呢都是世界各地热爱开源的小伙伴分享出来的(下一个可能就是你了)。心动了吧,那么改个主题很难吗?答案当然是否定的,只需要下面简单几步,我们就可以拥有一个好看又有个性又有逼格的博客。 挑一个你喜欢的 blog theme,点进去(多数是贡献者的博客网站) 找到当前主题的Github仓库入口(通常情况在博客的正下方),进入Github 克隆当前仓库,放在hexo所在目录的themes文件夹下 修改hexo根目录下的_config.yml文件,找到theme配置项,修改为克隆的主题名称 阅读当前Theme Repository的README.md,参考说明并修改相应配置即可(一般来说每个主题都有自己的_config.yml配置文件,我们只需要关注主题的配置文件即可) PS: 支持安装多个主题,但一次只能使用一个,在根目录下的_config.yml文件中修改切换主题即可。安装和使用主题中遇到的任何问题,可以通过查看主题所在Github仓库的Issues来排查和提问,或者直接联系主题贡献者协助解决吧。   hexo博客换装,大概就以上这么几步。下来给大家介绍一下我的博客换装过程,有兴趣的可以接着往下看。   我这个博客用的主题叫MiHo,然后这套主题贡献者博客大概长下面这个样子。   是不是觉得有点不太一样,这是因为默认的主题还是不太符合我的个性,所以我是有做一点点改造。如果大家接触过前端或者干脆就是做前端开发的,那这些应该都不是难事。好了,下面正式介绍下我是怎么换装的。 Hexo themes,搜索“miho”,点进去 在贡献者博客的正下方,找到了MiHo主题所在的Github Repository 克隆仓库,修改相关配置,主要参考README.md和主题贡献者发布的MiHo-主题安装和配置详情 遇到问题,第一时间查看GitHub Issues和上面那篇文章底部的留言,或者直接联系作者,这总是能快速的帮助我解决 后期个性化调整,主要修改了miho\\source目录下的部分样式文件,以及miho\\layout目录下的部分结构文件 神奇吗?用Hexo,自定义博客主题就这么简单,你还在犹豫什么呢?","categories":[{"name":"hexo","slug":"hexo","permalink":"https://blog.mariojd.cn/categories/hexo/"}],"tags":[{"name":"hexo","slug":"hexo","permalink":"https://blog.mariojd.cn/tags/hexo/"},{"name":"开源博客","slug":"开源博客","permalink":"https://blog.mariojd.cn/tags/开源博客/"},{"name":"hexo themes","slug":"hexo-themes","permalink":"https://blog.mariojd.cn/tags/hexo-themes/"}]},{"title":"Hexo,自定义域名 http 升级 https","slug":"Hexo,自定义域名 http 升级 https","date":"2018-05-03","updated":"2019-06-02","comments":true,"path":"custom-domain-name-http-upgrade-https-for-hexo.html","link":"","permalink":"https://blog.mariojd.cn/custom-domain-name-http-upgrade-https-for-hexo.html","excerpt":"","keywords":"","text":"  数数手指头,我这基于 Hexo + GitHub Page 搭起来的个人博客也有两个月时间了,之前就想过把 http 升级为 https,无奈因为各种原因也就被搁置下来了。今天,我这博客终于升级到 https 了。好东西第一时间是写出来分享,何况这操作真的很简单。 http 升级 https 超简易教程 进入GitHub Page所在的Repository,点击 Settings 在 Options(默认)选项下方找到 GitHub Pages 一栏,如下图所示,勾选 Enforce HTTPS设置即可   就两步操作,够简单吧!如果你也和我一样,尝试使用过第三方 Cloudflare 域名商的服务,估计你也有跟我一样抓狂过,但 Cloudflare 能为我们自定义的域名提供SSL证书,如果不想这么简单草率,那就留意文章的后续更新吧。 参考链接 GitHub Pages上的自定义域获得对HTTPS的支持Securing your GitHub Pages site with HTTPS低成本将你的网站切换为 HTTPS","categories":[{"name":"hexo","slug":"hexo","permalink":"https://blog.mariojd.cn/categories/hexo/"}],"tags":[{"name":"hexo","slug":"hexo","permalink":"https://blog.mariojd.cn/tags/hexo/"},{"name":"http","slug":"http","permalink":"https://blog.mariojd.cn/tags/http/"},{"name":"https","slug":"https","permalink":"https://blog.mariojd.cn/tags/https/"}]},{"title":"Spring Boot 几种启动问题的解决方案","slug":"Spring Boot 几种启动问题的解决方案","date":"2018-05-02","updated":"2019-06-02","comments":true,"path":"solutions-to-several-startup-problems-of-spring-boot.html","link":"","permalink":"https://blog.mariojd.cn/solutions-to-several-startup-problems-of-spring-boot.html","excerpt":"","keywords":"","text":"  使用Spring Boot以来,遇到和解决过好几次不同的项目启动问题,大多数事故起于错误的配置和依赖。因此,本文用于汇总这些问题,以及提供相应的解决方案,帮助大家更快的定位和排除故障。 1. Unregistering JMX-exposed beans on shutdown  项目中没有添加spring-boot-starter-web模块依赖,在启动 Application 运行过程中会出现这个错误。 . ____ _ __ _ _ /\\\\ / ___'_ __ _ _(_)_ __ __ _ \\ \\ \\ \\( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\ \\\\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v1.5.11.RELEASE)2018-05-02 18:32:49.445 INFO 33160 --- [ main] cn.mariojd.demo.DemoApplication : Starting DemoApplication on Mario with PID 33160 (started by jd in D:\\IntelliJ IDEA\\projects\\test)2018-05-02 18:32:49.451 INFO 33160 --- [ main] cn.mariojd.demo.DemoApplication : No active profile set, falling back to default profiles: default2018-05-02 18:32:49.542 INFO 33160 --- [ main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@17211155: startup date [Wed May 02 18:32:49 CST 2018]; root of context hierarchy2018-05-02 18:32:50.115 INFO 33160 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup2018-05-02 18:32:50.128 INFO 33160 --- [ main] cn.mariojd.demo.DemoApplication : Started DemoApplication in 1.01 seconds (JVM running for 1.83)... end SpringApplication.run()2018-05-02 18:32:50.129 INFO 33160 --- [ Thread-2] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@17211155: startup date [Wed May 02 18:32:49 CST 2018]; root of context hierarchy2018-05-02 18:32:50.130 INFO 33160 --- [ Thread-2] o.s.j.e.a.AnnotationMBeanExporter : Unregistering JMX-exposed beans on shutdown   解决方案,引入spring-boot-starter-web模块 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>   网上大多数的解决方案是通过添加spring-boot-starter-tomcat依赖来解决,但实测证明此方法不可行。 2. Cannot determine embedded database driver class for database type NONE  项目中添加了spring-boot-starter-data-jpa模块依赖,而且没有配置数据源连接信息的情况下,启动 Application 过程中会出现该错误,原因是Spring Boot在启动时会自动注入数据源和配置JPA。 . ____ _ __ _ _ /\\\\ / ___'_ __ _ _(_)_ __ __ _ \\ \\ \\ \\( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\ \\\\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v1.5.11.RELEASE)2018-05-02 19:49:13.640 INFO 37652 --- [ main] cn.mariojd.demo.DemoApplication : Starting DemoApplication on Mario with PID 37652 (started by jd in D:\\IntelliJ IDEA\\projects\\test)2018-05-02 19:49:13.643 INFO 37652 --- [ main] cn.mariojd.demo.DemoApplication : No active profile set, falling back to default profiles: default2018-05-02 19:49:13.692 INFO 37652 --- [ main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@62fdb4a6: startup date [Wed May 02 19:49:13 CST 2018]; root of context hierarchy2018-05-02 19:49:15.150 INFO 37652 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$4ad697b] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)2018-05-02 19:49:15.433 INFO 37652 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)2018-05-02 19:49:15.460 INFO 37652 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]2018-05-02 19:49:15.461 INFO 37652 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.292018-05-02 19:49:15.564 INFO 37652 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext2018-05-02 19:49:15.564 INFO 37652 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1876 ms2018-05-02 19:49:15.679 INFO 37652 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/]2018-05-02 19:49:15.682 INFO 37652 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*]2018-05-02 19:49:15.682 INFO 37652 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]2018-05-02 19:49:15.682 INFO 37652 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*]2018-05-02 19:49:15.683 INFO 37652 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*]2018-05-02 19:49:15.717 WARN 37652 --- [ main] ationConfigEmbeddedWebApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Tomcat.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.apache.tomcat.jdbc.pool.DataSource]: Factory method 'dataSource' threw exception; nested exception is org.springframework.boot.autoconfigure.jdbc.DataSourceProperties$DataSourceBeanCreationException: Cannot determine embedded database driver class for database type NONE. If you want an embedded database please put a supported one on the classpath. If you have database settings to be loaded from a particular profile you may need to active it (no profiles are currently active).2018-05-02 19:49:15.719 INFO 37652 --- [ main] o.apache.catalina.core.StandardService : Stopping service [Tomcat]2018-05-02 19:49:15.765 INFO 37652 --- [ main] utoConfigurationReportLoggingInitializer : Error starting ApplicationContext. To display the auto-configuration report re-run your application with 'debug' enabled.2018-05-02 19:49:15.791 ERROR 37652 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter : ***************************APPLICATION FAILED TO START***************************Description:Cannot determine embedded database driver class for database type NONEAction:If you want an embedded database please put a supported one on the classpath. If you have database settings to be loaded from a particular profile you may need to active it (no profiles are currently active).Process finished with exit code 1 解决方案1,移除spring-boot-starter-data-jpa模块依赖; 解决方案2,将启动类注解@SpringBootApplication修改如下; @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,DataSourceTransactionManagerAutoConfiguration.class, HibernateJpaAutoConfiguration.class}) 解决方案3,在配置文件中添加数据库连接信息。spring: datasource: url: xxx username: xxx password: xxx","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"https://blog.mariojd.cn/tags/Spring-Boot/"},{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/tags/Java/"}]},{"title":"Spring Boot 中初始化资源的几种方式","slug":"Spring Boot 中初始化资源的几种方式","date":"2018-05-02","updated":"2019-06-02","comments":true,"path":"several-ways-of-initializing-resources-in-spring-boot.html","link":"","permalink":"https://blog.mariojd.cn/several-ways-of-initializing-resources-in-spring-boot.html","excerpt":"","keywords":"","text":"  假设有这么一个需求,要求在项目启动过程中,完成线程池的初始化,加密证书加载等功能,你会怎么做?如果没想好答案,请接着往下看。今天介绍几种在Spring Boot中进行资源初始化的方式,帮助大家解决和回答这个问题。 CommandLineRunner 定义初始化类 MyCommandLineRunner 实现 CommandLineRunner 接口,并实现它的 run() 方法,在该方法中编写初始化逻辑 注册成Bean,添加 @Component注解即可 示例代码如下: @Componentpublic class MyCommandLineRunner implements CommandLineRunner { @Override public void run(String... args) throws Exception { System.out.println(\"...init resources by implements CommandLineRunner\"); } }   实现了 CommandLineRunner 接口的 Component 会在所有 Spring Beans 初始化完成之后, 在 SpringApplication.run() 执行之前完成。下面通过加两行打印来验证我们的测试。 @SpringBootApplicationpublic class DemoApplication { public static void main(String[] args) { System.out.println(\"... start SpringApplication.run()\"); SpringApplication.run(DemoApplication.class, args); System.out.println(\"... end SpringApplication.run()\"); } }   控制台打印结果如下。 ... start SpringApplication.run() . ____ _ __ _ _ /\\\\ / ___'_ __ _ _(_)_ __ __ _ \\ \\ \\ \\( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\ \\\\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v1.5.11.RELEASE)。。。。。。(此处省略一堆打印信息)2018-05-02 17:01:19.700 INFO 21236 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)...init resources by implements CommandLineRunner2018-05-02 17:01:19.708 INFO 21236 --- [ main] cn.mariojd.demo.DemoApplication : Started DemoApplication in 2.282 seconds (JVM running for 3.125)... end SpringApplication.run() ApplicationRunner 定义初始化类 MyApplicationRunner 实现 ApplicationRunner 接口,并实现它的 run() 方法,在该方法中编写初始化逻辑 注册成Bean,添加 @Component注解即可 示例代码如下: @Componentpublic class MyApplicationRunner implements ApplicationRunner { @Override public void run(ApplicationArguments applicationArguments) throws Exception { System.out.println(\"...init resources by implements ApplicationRunner\"); }}   可以看到,通过实现 ApplicationRunner 接口,和通过实现 CommandLineRunner 接口都可以完成项目的初始化操作,实现相同的效果。两者之间唯一的区别是 run() 方法中自带的形参不相同,在 CommandLineRunner 中只是简单的String... args形参,而 ApplicationRunner 则是包含了 ApplicationArguments 对象,可以帮助获得更丰富的项目信息。 @Order  如果项目中既有实现了 ApplicationRunner 接口的初始化类,又有实现了 CommandLineRunner 接口的初始化类,那么会是哪一个先执行呢?测试告诉我们,答案是实现了 ApplicationRunner 接口的初始化类先执行,我想这点倒是不需要大家过分去关注为什么。但如果需要改变两个初始化类之间的默认执行顺序,那么使用 @Order 注解就可以帮助我们解决这个问题。 @Component@Order(1)public class MyCommandLineRunner implements CommandLineRunner { @Override public void run(String... args) throws Exception { System.out.println(\"...init resources by implements CommandLineRunner\"); }} @Component@Order(2)public class MyApplicationRunner implements ApplicationRunner { @Override public void run(ApplicationArguments applicationArguments) throws Exception { System.out.println(\"...init resources by implements ApplicationRunner\"); }}   最终,控制台中打印如下。通过控制台输出我们发现, @Order 注解值越小,该初始化类也就越早执行。 。。。。。。(此处省略一堆打印信息)2018-05-02 17:27:31.450 INFO 28304 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)...init resources by implements CommandLineRunner...init resources by implements ApplicationRunner2018-05-02 17:27:31.453 INFO 28304 --- [ main] cn.mariojd.demo.DemoApplication : Started DemoApplication in 2.086 seconds (JVM running for 2.977) @PostConstruct  使用 @PostConstruct 注解同样可以帮助我们完成资源的初始化操作,前提是这些初始化操作不需要依赖于其它Spring beans的初始化工作。   可以看到 @PostConstruct 注解是用在方法上的,写一个方法测试一下吧。 @PostConstructpublic void postConstruct() { System.out.println(\"... PostConstruct\");}   启动项目,控制台中最终打印如下。 ... start SpringApplication.run() . ____ _ __ _ _ /\\\\ / ___'_ __ _ _(_)_ __ __ _ \\ \\ \\ \\( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\ \\\\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v1.5.11.RELEASE)。。。。。。(此处省略一堆打印信息)... PostConstruct。。。。。。(此处省略一堆打印信息)2018-05-02 17:40:22.300 INFO 29796 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)...init resources by implements CommandLineRunner...init resources by implements ApplicationRunner2018-05-02 17:40:22.303 INFO 29796 --- [ main] cn.mariojd.demo.DemoApplication : Started DemoApplication in 2.387 seconds (JVM running for 3.267)... end SpringApplication.run() 文末小结  综上,使用 @PostConstruct 注解进行初始化操作的顺序是最快的,前提是这些操作不能依赖于其它Bean的初始化完成。通过添加 @Order 注解,我们可以改变同层级之间不同Bean的加载顺序。","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"https://blog.mariojd.cn/tags/Spring-Boot/"},{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/tags/Java/"}]},{"title":"Docker + anoyi-blog 打造专属个人简书","slug":"Docker + anoyi-blog 打造专属个人简书","date":"2018-04-27","updated":"2018-09-29","comments":true,"path":"anoyi-blog-to-build-an-exclusive-personal-book-based-docker.html","link":"","permalink":"https://blog.mariojd.cn/anoyi-blog-to-build-an-exclusive-personal-book-based-docker.html","excerpt":"","keywords":"","text":"写在前面  anoyi-blog,一款有趣的开源个人博客建站工具,简单梳理下分享给大家。该工具基于 Docker, 可快速搭建和一键生成个人博客,用于实时解析简书文章、作者信息,专为个性化而生! Docker命令docker run -d --name myblog \\-e JIANSHU_ID=000a530f461c \\-e WEB_NAME=happyJared \\-e GITHUB=\"https://github.com/happyjared\" \\-e GITLAB=\"https://github.com/happyjared\" \\-e QQ=12345678 \\-e ICP=粤ICP备12345678号 \\-e REWARD_IMAGES=\"https://upload.jianshu.io/users/qrcodes/9709135/myweixin.png?imageMogr2/auto-orient/strip|imageView2/1/w/84/h/84\" \\-e REWARD_DESC=\"赞赏支持\" \\-e BLOG_TITLE=\"博客标题\" \\-e BLOG_DESC=\"博客描述\" \\-p 20184:8080 registry.cn-hangzhou.aliyuncs.com/anoy/blog   docker run之后,访问 http://localhost:20184/ 可查看生成的博客,映射其他端口可配置: -p {port}:8080 配置说明 变量 说明 示例 JIANSHU_ID 简书ID https://www.jianshu.com/u/000a530f461c, 点击“简书”->“我的主页”,这里只需要取000a530f461c即可 WEB_NAME 网站名称 happyJared GITHUB Github 地址 https://github.com/happyjared GITLAB Gitlab 地址 https://github.com/happyjared QQ QQ 号(用于发起聊天沟通) 12345678 ICP ICP 备案号 粤ICP备12345678号 REWARD_IMAGES 打赏二维码图片链接地址 多个图片可用 , 分隔 REWARD_DESC 打赏提示语 赞赏支持 BLOG_TITLE 博客标题 博客标题 BLOG_DESC 博客描述 博客描述 DING_TALK 钉钉机器人 API 地址 参考钉钉自定义机器人 示例:-e DING_TALK=”https://oapi.dingtalk.com/robot/send?access_token=b1586fba8caf2c98bf6f1174b4ec57c75941553a15a75c437422f55fc1b76cd1“ 简单分析   查看启动日志,通过运行命令docker logs myblog,可以看到 anoyi-blog 是基于 Spring Bootv2.0.0.RELEASE开发的,使用了 Undertow 作为Web服务器   运行命令docker inspect anoyi-blog,可以看到完整的启动脚本 java -jar -server /app.jar --jianshu.user-id=$JIANSHU_ID --custom.web-name=$WEB_NAME --custom.github-url=$GITHUB --custom.gitlab-url=$GITLAB --custom.qq=$QQ --custom.icp=$ICP --custom.reward-images=$REWARD_IMAGES --custom.reward-desc=$REWARD_DESC --custom.blog-title=$BLOG_TITLE --custom.blog-desc=$BLOG_DESC --custom.ding-talk=$DING_TALK 写在最后  可以看到,利用 docker + anoyi-blog 来打造个人简书博客还是很方便的。但相比于两大静态博客主流框架:jekyll 和 hexo ,anoyi-blog 在可定制化方面确实显得较为欠缺。 参考地址 开源博客 ANOYI-BLOG 使用指南anoy/blog - 阿里云镜像","categories":[{"name":"开源","slug":"开源","permalink":"https://blog.mariojd.cn/categories/开源/"}],"tags":[{"name":"docker","slug":"docker","permalink":"https://blog.mariojd.cn/tags/docker/"},{"name":"博客","slug":"博客","permalink":"https://blog.mariojd.cn/tags/博客/"}]},{"title":"Jmeter 压测 ws(s)","slug":"Jmeter 压测 ws(s)","date":"2018-04-25","updated":"2019-06-02","comments":true,"path":"jmeter-pressure-test-based-on-websocket.html","link":"","permalink":"https://blog.mariojd.cn/jmeter-pressure-test-based-on-websocket.html","excerpt":"","keywords":"","text":"  上一篇Jmeter压测http(s)介绍了如何使用Jmeter压测常规的web服务接口。本文将继续演示如何使用Jmeter对ws(s)进行压力测试。   有过Socket相关开发经验的人应该都不会陌生,ws协议就是WebSocket协议,而wss对应就是在SSL上运行的WebSocket协议。有关WebSocket的知识,这里不做过多说明,有兴趣可以看看本人写的另外一篇文章:记录一次迁移wss WebSocket的事故。 配置环境  由于JMeter需要添加几个扩展Jar包后方可支持ws测试,所以我们需要配置相关的测试环境,以下是这些jar包的下载地址。 官方下载 备用下载   将下载好的Jar包放到JMeter的lib\\ext目录下即可。 ws测试  启动Jmeter后,右键依次选择 “测试计划” -> “添加” -> “Threads(Users)” -> “线程组” -> “Sampler” -> “WebSocket Sampler”。 选项说明: WebServer Server Name or IP:目标WebSocket服务器所在地址或名称 Port Number:WebSocket 服务监听端口(http和ws一般是80端口,https和wss一般是433端口) Timeout(单位:毫秒) Connection:连接等待完成的最长时间 Response:消息响应的最大等待时间 WebSocket Request Implementation:目前仅支持RFC6455(v13) ,这也是最新版的WebSocket协议标准 Protocol:WebSocket标识,ws或者wss Streaming Connection:用于TCP会话是否需要保持。如果勾上表示连接会一直存在,否则在第一次响应后该连接就会被关闭 Request data:请求要发送的数据 Path:WebSocket端点路径   为了方便,下面以一个在线的WebSocket网站来开展测试。分别在Server Name or IP一栏中输入:echo.websocket.org,在Request data一栏中输入:Send test。启动测试计划,得到如下响应结果。 wss测试  相比于ws测试,wss的测试也仅需修改两处地方。分别将Port Number修改为:443,以及将Protocol修改为:wss。启动测试计划后,我们同样可以得到系统的正常响应。 写在最后  结合“函数助手”或者“CVS数据文件”的方式,我们同样可以模拟实现不同的用户,发送不同的请求参数。有关于JMeter的内容到这就告一段落了,后续如果还有其它的补充,我会及时的更新上去。","categories":[{"name":"Jmeter","slug":"Jmeter","permalink":"https://blog.mariojd.cn/categories/Jmeter/"}],"tags":[{"name":"Jmeter","slug":"Jmeter","permalink":"https://blog.mariojd.cn/tags/Jmeter/"},{"name":"压测工具","slug":"压测工具","permalink":"https://blog.mariojd.cn/tags/压测工具/"},{"name":"ws(s)","slug":"ws-s","permalink":"https://blog.mariojd.cn/tags/ws-s/"}]},{"title":"Jmeter 压测 http(s)","slug":"Jmeter 压测 http(s)","date":"2018-04-24","updated":"2019-06-02","comments":true,"path":"jmeter-pressure-test-based-on-http.html","link":"","permalink":"https://blog.mariojd.cn/jmeter-pressure-test-based-on-http.html","excerpt":"","keywords":"","text":"  上一篇文章关于Jmeter介绍了Jmeter入门相关的知识。本文是实战篇,讲讲如何使用Jmeter对Http(s)进行压力测试。 Http测试测试一  首先,添加 “线程组”,选择 “添加” -> “Sampler” -> “HTTP请求”。其次,分别添加 “查看结果树”,“聚合报告”,“图形结果”等,用于查看测试结果。   配置 “HTTP请求”,对应上图中的 “jmeter测试1”。这里配置Get请求,测试本地8080端口下的jmeter/test1接口地址,具体的Http配置及测试代码请参考如下。 @RequestMapping(\"jmeter\")@RestControllerpublic class HttpController { private AtomicInteger atomicInteger = new AtomicInteger(0); @GetMapping(\"test1\") public Map<String, Object> test1() { Map<String, Object> map = new HashMap<>(1); int decrementAndGet = atomicInteger.incrementAndGet(); System.out.println(\"decrementAndGet = \" + decrementAndGet); map.put(\"atomicInteger\", decrementAndGet); return map; } }   配置好后,点击绿色三角图标即可启动线程组,线程组的配置如下。   启动测试请求后,响应结果如下,这是模拟1s内发起200次的请求。 测试二  一般而言,测试都需要尽可能的模拟真实用户。因此,通过传递不同的请求参数,来模拟不同的用户进行请求是必不可少的。接下来使用 “函数助手的” 方式,告诉大家如何模拟不同的用户进行压力测试。   案例场景:模拟10个用户发起POST请求,请求地址jmeter/test2,要求在Http请求头中自定义请求头 Authorization ,不同用户对应的自定义请求头参数值不同。 1. 使用函数助手添加请求头参数 CSV,即Comma Separate Values,这种文件格式经常用来作为不同程序之间的数据交互格式。具体文件格式:每条记录占一行,以逗号为分隔符,逗号前后的空格会被忽略。字段中包含有逗号,该字段必须用双引号括起来;字段中包含有换行符,该字段必须用双引号括起来;字段前后包含有空格,该字段必须用双引号括起来;字段中的双引号用两个双引号表示;字段中如果有双引号,该字段必须用双引号括起来。   在本地创建test2.cvs(如:G:\\TEST\\test2.cvs),添加10个用户对应的Authorization值(假设是:1 ~ 10),使用函数助手进行添加。 2. 配置 “HTTP信息头管理器”   如上图所示,分别设置 Content-Type:application/json和Authorization:${__CSVRead(G:\\TEST\\test2.cvs,0)} 3. 模拟用户请求进行测试@RequestMapping(\"jmeter\")@RestControllerpublic class HttpController { @PostMapping(\"test2\") public Map<String, Object> test2(HttpServletRequest request) { Map<String, Object> map = new HashMap<>(1); String authorization = request.getHeader(\"Authorization\"); map.put(\"authorization\", authorization); return map; }}   启动测试后,响应结果如下。 测试三  案例场景:使用配置元件 “CSV数据文件” 的方式,模拟10个用户发起PUT请求,请求地址jmeter/test3,要求不同用户对应请求参数reqId的值不同。 1. 使用“CSV数据文件”添加请求参数  在本地创建test2.cvs(如:G:\\TEST\\test2.cvs),添加10个用户对应的reqId值(假设是:1 ~ 10)。 2. 配置 “HTTP信息头管理器”   如上图所示,通过在请求路径使用占位符${}来读取“CSV数据文件”中对应的配置参数和值。注意,占位符内的参数名称要保持与配置一致。 3. 模拟用户请求进行测试@RequestMapping(\"jmeter\")@RestControllerpublic class HttpController { @PutMapping(\"test3\") public Map<String, Object> test3(@RequestParam int reqId) { Map<String, Object> map = new HashMap<>(1); System.out.println(\"id = [\" + reqId + \"]\"); map.put(\"id\", reqId); return map; }}   启动测试后,响应结果如下。 Https测试  为了方便,关于https的测试将选取网上站点进行,如我的简书主页,参考如下。 写在最后  关于http(s)这块的测试到这就介绍完了,本文重点部分就是学习两种实现参数化的方式。JMeter作为一款压测工具,我们只需要熟悉之后再多加练习,便可以基本掌握。下来我将继续梳理如何使用JMeter进行ws(s)测试,欢迎关注。","categories":[{"name":"Jmeter","slug":"Jmeter","permalink":"https://blog.mariojd.cn/categories/Jmeter/"}],"tags":[{"name":"Jmeter","slug":"Jmeter","permalink":"https://blog.mariojd.cn/tags/Jmeter/"},{"name":"http(s)","slug":"http-s","permalink":"https://blog.mariojd.cn/tags/http-s/"},{"name":"压测工具","slug":"压测工具","permalink":"https://blog.mariojd.cn/tags/压测工具/"}]},{"title":"关于 Jmeter","slug":"关于 Jmeter","date":"2018-04-23","updated":"2019-06-02","comments":true,"path":"about-jmeter.html","link":"","permalink":"https://blog.mariojd.cn/about-jmeter.html","excerpt":"","keywords":"","text":"Apache JMeter The Apache JMeter™ application is open source software, a 100% pure Java application designed to load test functional behavior and measure performance. It was originally designed for testing Web Applications but has since expanded to other test functions.    关于JMeter,这里不做过多说明,更多介绍可以查看官网。我们只需要知道这是一款用Java开发的压力测试工具,可以模拟对服务器的请求来测试它们的负载强度,分析不同压力类型下的整体性能。 1. 下载安装 Download Apache JMeter    注意: JMeter运行依赖于Java环境,所以请提前装好这些环境,并配置全局的环境变量。从上图我们可以得知,JMeter4.0+更是需要Java8或者Java9环境。如果不需要了解源码,这里我们只需要选择下载“Binaries”一栏中的.tgz或.zip包即可,然后解压即可。 2. 运行JMeter4.0   进入bin目录下,Windows平台双击 “ApacheJMeter.jar” 或 “jmeter.bat” 即可,Linux下请启动 “jmeter.sh” 脚本。   这是JMeter4.0的主界面,可以看到这是用Java Swing写的GUI。 3. JMeter4.0简单介绍   顶部导航栏中。最左边的 File 选项,可用于新建、打开(最近)和保存测试计划。重点关注第四个选项 Run,一般编写完测试计划后,我们可以在此下拉选择启动(或中断),或者可以点击第二栏中间部位的“绿色右三角标”进行启动。再往右边一点,可以看到有两个带有“扫帚”的图案,这两个是用来清除测试结果的。 在Test plan(测试计划)处,右键选择 Add,然后在 Threads(Users)中选择 Thread Group(添加线程组),接下来简单介绍一下线程组中的部分核心功能。 线程属性 线程数:模拟多少并发用户,就设置多大的数值 Ramp-Up Period (in seconds):并发访问的时间范围大小 循环次数:执行多少次循环(勾选“永远”后线程组将一直执行) 示例:线程数(1000),Ramp-Up Period(3),循环次数(2)解释:在3秒内模拟共1000次的用户并发请求,并循环执行2次 调度器配置(勾选“调度器”选项开启) 持续时间(秒):线程组执行的总时长 启动延迟(秒):执行线程组后,延迟真正开始请求的时间,默认启动后立刻执行 Http请求:用于配置Http请求的信息 配置元件 CVS数据文件设置:通过导入CVS文件,模拟不同的请求参数进行接口压测(PS:顶部导航栏 “选项” -> “函数助手” 也可以实现类似的功能,后续实战将进行一一介绍) Http信息头管理:设置Http请求头参数等 Http Cookie 管理器:设置Cookie信息 察看结果树:用于查看Http请求响应结果信息 聚合报告:用于查看报告分析等信息 写在最后   注意,线程组的配置似乎是需要按顺序进行的,否则可能会出现某些配置不生效的情况。下来我会进行实战压测演示,模拟对Http(s)和ws(s)接口的请求,欢迎关注。最后,附上示例的JMeter配置,下载后(无法自动下载,请点击右键,选择另存为即可),在JMeter中选择导入即可。 demo.jmx","categories":[{"name":"Jmeter","slug":"Jmeter","permalink":"https://blog.mariojd.cn/categories/Jmeter/"}],"tags":[{"name":"Jmeter","slug":"Jmeter","permalink":"https://blog.mariojd.cn/tags/Jmeter/"},{"name":"压测工具","slug":"压测工具","permalink":"https://blog.mariojd.cn/tags/压测工具/"}]},{"title":"记录一次迁移 wss WebSocket 的事故","slug":"记录一次迁移 wss WebSocket 的事故","date":"2018-04-21","updated":"2019-06-02","comments":true,"path":"record-an-accident-of-a-migration-of-wss-websocket.html","link":"","permalink":"https://blog.mariojd.cn/record-an-accident-of-a-migration-of-wss-websocket.html","excerpt":"","keywords":"","text":"  今天是2018年04月21日。   过去的这一个多月里,我的工(开)作(发)任务转战回了游戏。短短的一个月里,催着输出两款h5游戏,再加上对接、联调,想想真是够辛(ku)苦(bi)的。本人负责后端,也就是服务端这块的游戏主流程输出。去年下半年,在前任大佬的带领下,做过一两款棋牌类的手游,虽然目前的运营状况不太乐观。不过好在,过去学的那点皮毛也还没丢光,所以这次写h5后端总体还算顺畅。至于怎么用Java来写游戏,下来如果有时间会整理下这块的思路和知识。 关于WebSocket,维基百科是这样介绍的:    以前,很多网站为了实现实时推送技术,所用的技术都是轮询。轮询是在特定的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端。这种传统的模式带来的缺点很明显,即浏览器需要不断的向服务器发出请求,然而HTTP请求包含较多的请求头信息,而其中真正有效的数据只是很小的一部分,显然这样会浪费很多的带宽等资源。在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。   WebSocket是一种在单个TCP连接上进行全双工通讯的协议,使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。   WebSocket 协议在2008年诞生,2011年成为国际标准,现在几乎所有浏览器都已经支持了。它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。   简单来说,WebSocket减少了客户端与服务器端建立连接的次数,减轻了服务器资源的开销,只需要完成一次HTTP握手。整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的非状态性,服务端会一直与客户端保持连接,直到双方发起关闭请求,同时由原本的客户端主动询问,转换为服务器有信息的时候推送。所以,它能做实时通信(聊天室、直播间等),其他特点还包括: 建立在 TCP 协议之上,服务器端的实现比较容易 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器 数据格式比较轻量,性能开销小,通信高效 可以发送文本,也可以发送二进制数据 没有同源限制,客户端可以与任意服务器通信 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL   差点就跑题了。这不,由于业务需求,上头要求新出的h5游戏要配上Https。无奈,公司小,没有专业的运维人员,所以只能由我们这些开发“猿”顶上了,以为会很顺畅,但一连串的问题没想到也才刚刚开始。因此本文,就是用来记录这些踩过的“坑”,希望可以让后人少走点弯路。 1. 申领证书   公有云服务器上,一般大家都习惯使用Nginx来做反向代理。首先,配置Https,需要我们到专业的CA机构去申领证书,这个证书大多数情况下都是要钱的,但其实也有免费的(有效期1年),例如利用国内的阿里云或者腾讯云就可以很方便的申请这证书。    - 阿里云 - Https证书申请   - 腾讯云 - Https证书申请 阿里云Https证书申请   PS: 通过阿里云申领免费版SSL证书有点套路,藏得有点深。点击以上链接进入后,如果在“证书类型”一栏中没找到“免费型DV SSL”,那么请依次点击第三栏的“选择品牌”中的“Symantec”,然后回到第一栏的“证书类型”,点击出现的第三个选项“增强型OV SSL”,之后就会在“证书类型”中出现我们需要的第二项:“免费型DV SSL”。 腾讯云Https证书申请   确认申领、购买之后,下来还需要绑定我们的域名(注意:免费型的SSL证书一般仅支持绑定一个一级域名或者子域名,通配符的证书一般是需要花钱的),以及进行域名身份验证等操作。等这两步都完成之后,只需要等待CA机构扫描认证之后,我们就可以拿到真正的证书了。 2. 配置Https  下载好证书压缩包并解压之后,一般里面有IIS、Apache和Nginx三款主流服务器的ssl证书,这里我们也仅需要Nginx的证书。首先,将证书里Nginx文件夹下的1_{域名}bundle.crt 和2{域名}.key复制到我们服务器上的指定位置(假设在/root/ssl/下面)。基于Nginx的Https配置还是比较简单的,参考如下。 server {# listen 80; #如果需要同时支持http和https listen 443 ssl http2; listen [::]:443 ssl http2; ssl_certificate "/root/ssl/1_{域名}_bundle.crt"; ssl_certificate_key "/root/ssl/2_{域名}.key"; ssl_session_cache shared:SSL:1m; ssl_session_timeout 10m; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; server_name {域名}; location / { proxy_pass http://localhost:{代理端口}; } }   附:下面是开启Nginx的Gzip压缩的配置,有需要的也可以参考。 http { gzip on; gzip_disable "msie6"; gzip_min_length 1k; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_buffers 16 8k; gzip_http_version 1.1; gzip_types application/font-woff text/plain application/javascript application/json text/css application/xml text/javascript image/jpg image/jpeg image/png image/gif image/x-icon; server { # 这里是server相关的配置 }} 3. 事故现场  完成以上步骤后,按道理来说,h5游戏确实可以通过https的形式来打开了,简单测试后的确没啥问题,然后大家也就这样愉快的下班了。不过正如“墨菲定律”所说的:“凡事只要有可能出错,那就一定会出错”。果不其然,一段时间后,测试就在群里反馈,某段时间后h5游戏就无法加载正常进行下去了,一看时间,正是配完Https之后开始出现的问题。没办法,于是连忙打开电脑,开始排查解决问题,直觉告诉我要先打开浏览器的控制面板,果不其然,立刻发现了问题。 Mixed Content: The page at ‘https://{域名}.com/‘ was loaded over HTTPS, but attempted to connect to the insecure WebSocket endpoint ‘ws://{ip}:{port}/‘. This request has been blocked; this endpoint must be available over WSS.Uncaught DOMException: Failed to construct ‘WebSocket’: An insecure WebSocket connection may not be initiated from a page loaded over HTTPS.   好家伙,这种情况,毫无疑问我们就需要使用 wss:// 安全协议了,于是立即联系h5客户端,把连接服务端webscoket的形式由ws:// 改为 wss:// 。本以为这样就解决了,没想到一段时间后下一个问题又来了。 扩展:关于 ws 和 wss WebSocket可以使用 ws 或 wss 来作为统一资源标志符,类似于 HTTP 或 HTTPS。其中 ,wss 表示在 TLS 之上的 WebSocket,相当于 HTTPS。默认情况下,WebSocket的 ws 协议基于Http的 80 端口;当运行在TLS之上时,wss 协议默认是基于Http的 443 端口。说白了,wss 就是 ws 基于 SSL 的安全传输,与 HTTPS 一样样的道理。所以,如果你的网站是 HTTPS 协议的,那你就不能使用 ws:// 了,浏览器会 block 掉连接,和 HTTPS 下不允许 HTTP 请求一样。   h5客户端改成wss连接后,测试发现还是无法正常游戏。无奈,再次打开浏览器面板,果然,又看到一个新的问题。 WebSocket connection to ‘wss://{ip}:{port}/‘ failed: Error in connection establishment: net::ERR_SSL_PROTOCOL_ERROR   之前在Http的情况下,客户端一直是用ip+port的形式来连接服务端,当然了也不会出现什么问题。很明显,在更改成Https后,若还是以这种方式连接服务端,浏览器就会报 SSL 协议错误,这很明显就是证书的问题。如果这时候还用 IP + 端口号 的方式连接 WebSocket ,是根本就没有证书存在的(即使我们在Nginx配置了SSL证书,但这种方式其实是不会走Nginx代理的),所以在生产环境下,更推荐大家用域名的方式来连接。于是,立刻又联系前端,再一次做更改,修改为 wss://{域名}/ 进行连接。我以为这样就真的解决了,没想到还是too young too simple,没一会下个问题又来了,测试反馈的结果还是不可以,第三次打开浏览器控制面板,果然又是一个新的错误信息。 WebSocket connection to ‘wss://{域名}/‘ failed: Error during WebSocket handshake: Unexpected response code: 400   看到这个错误信息后,确定这是服务端返回的400响应。既然可以请求到服务端,就说明客户端这边是没有问题的,那么问题最可能出在客户端和服务端之间。由于中间层使用了Nginx做转发,所以导致服务端无法知道这是一个合法的WebSocket请求。于是立刻查找了网上资料,在Nginx配置文件加入了以下配置,成功解决了这个问题。 server { location / { proxy_pass http://localhost:{port}; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }}   接着,连忙拿域名进行再次连接测试,终于看到了101 Switching Protocols的响应Status Code。就这样,也算是终于解决完在 HTTPS 下以 wss://{域名}/ 的方式连接 WebSocket的一系列问题。不过,最后这其中还有一个小问(插)题(曲)。 关于Nginx中的WebSocket配置    自1.3 版本开始,Nginx就支持 WebSocket,并且可以为 WebSocket 应用程序做反向代理和负载均衡。WebSocket 和 HTTP 是两种不同的协议,但是 WebSocket 中的握手和 HTTP 中的握手兼容,它使用 HTTP 中的 Upgrade 协议头将连接从 HTTP 升级到 WebSocket,当客户端发过来一个 Connection: Upgrade请求头时,其实Nginx是不知道的。所以,当 Nginx 代理服务器拦截到一个客户端发来的 Upgrade 请求时,需要我们显式的配置Connection、Upgrade头信息,并使用 101(交换协议)返回响应,在客户端、代理服务器和后端应用服务之间建立隧道来支持 WebSocket。   当然,还需要注意一点,此时WebSocket 仍然受到 Nginx 缺省为60秒的 proxy_read_timeout 配置影响。这意味着,如果你有一个程序使用了 WebSocket,但又可能超过60秒不发送任何数据的话,那么需要增大超时时间(配置proxy_read_timeout),要么实现一个Ping、Pong的心跳消息以保持客户端和服务端的联系。使用Ping、Pong的解决方法有额外的好处,如:可以发现连接是否被意外关闭等。   关于最后的这个小问题,主要是在对Nginx配置的时候将location=/的请求都进行了proxy_pass(转发)。由于h5客户端的文件打包成静态文件后,存放在服务器的指定目录下(这里假设在/root/html/static/路径下),这也就导致这种配置的情况下Nginx无法正常代理指定目录下的客户端文件。于是再一次修改配置文件,添加location配置,最终完美解决所有问题。 location /static/ { root /root/html; } 4. 写在最后  事故一波三折,现在回想起当时,也是一把辛酸史,一把辛酸泪(累)啊。所以仅以此文,记录下我的填“坑”过程。","categories":[{"name":"websocket","slug":"websocket","permalink":"https://blog.mariojd.cn/categories/websocket/"}],"tags":[{"name":"websocket","slug":"websocket","permalink":"https://blog.mariojd.cn/tags/websocket/"},{"name":"wss","slug":"wss","permalink":"https://blog.mariojd.cn/tags/wss/"},{"name":"nginx","slug":"nginx","permalink":"https://blog.mariojd.cn/tags/nginx/"}]},{"title":"阿里技术精华干货整理","slug":"阿里技术精华干货整理","date":"2018-04-18","updated":"2018-12-04","comments":true,"path":"dry-goods-finishing-of-ali-tech.html","link":"","permalink":"https://blog.mariojd.cn/dry-goods-finishing-of-ali-tech.html","excerpt":"","keywords":"","text":"  本文用于整理阿里开源出来的技术电子书,更多精彩请搜微信公众号:“阿里技术”。 Java《阿里巴巴Java开发手册》(详尽版) 官方下载 备用下载 《阿里巴巴Java开发手册》(终极版) 官方下载 备用下载 《阿里巴巴Java开发规约》(扫描插件) GitHub仓库 使用指南 Android《阿里巴巴Android开发手册》 官方下载 备用下载 《深入探索Android热修复技术原理》 官方下载 备用下载 其它《阿里技术参考图册》(研发篇) 官方下载 备用下载 《阿里技术参考图册》(算法篇) 官方下载 备用下载 《2017阿里技术年度精选集上》 官方下载 备用下载 《2017阿里技术年度精选集下》 官方下载 备用下载 《九年双11:互联网技术超级工程》 官方下载 备用下载 《强化学习在阿里的技术演进与业务创新》 官方下载 备用下载 《不一样的技术创新——阿里巴巴2016双11背后的技术》 备用下载 《不止代码》 官方下载 备用下载 《阿里机器智能技术精选》 官方下载 备用下载 《不仅仅是流计算:Apache Flink实践》 官方下载 备用下载 欢迎留言补充","categories":[{"name":"学习资源","slug":"学习资源","permalink":"https://blog.mariojd.cn/categories/学习资源/"}],"tags":[{"name":"学习资源","slug":"学习资源","permalink":"https://blog.mariojd.cn/tags/学习资源/"},{"name":"阿里技术","slug":"阿里技术","permalink":"https://blog.mariojd.cn/tags/阿里技术/"}]},{"title":"使用 Spring RestTemplate 访问 Rest 服务","slug":"使用 Spring RestTemplate 访问 Rest 服务","date":"2018-04-04","updated":"2019-06-02","comments":true,"path":"access-to-rest-services-using-spring-resttemplate.html","link":"","permalink":"https://blog.mariojd.cn/access-to-rest-services-using-spring-resttemplate.html","excerpt":"","keywords":"","text":"RestTemplate简介 Spring’s central class for synchronous client-side HTTP access.It simplifies communication with HTTP servers, and enforces RESTful principles.It handles HTTP connections, leaving application code to provide URLs(with possible template variables) and extract results.   上面这段是RestTemplate类中的简单介绍,RestTemplate是Spring3.0后开始提供的用于访问 Rest 服务的轻量级客户端,相较于传统的HttpURLConnection、Apache HttpClient、OkHttp等框架,RestTemplate大大简化了发起HTTP请求以及处理响应的过程。本文关注RestTemplate是如何使用的,暂不涉及内部的实现原理。   RestTemplate支持多种的请求方式,具体参考下表: HTTP method RestTemplate methods GET getForObject、getForEntity POST postForObject、postForEntity、postForLocation PUT put DELETE delete HEAD headForHeaders OPTIONS optionsForAllow PATCH patchForObject any exchange、execute 引入RestTemplate 方式一,使用无参构造器直接new一个对象 private RestTemplate restTemplate = new RestTemplate(); 方式二,先注册成Spring的Bean对象,之后使用的时候直接注入@Beanpublic RestTemplate restTemplate(){ return new RestTemplate();} @Autowiredprivate RestTemplate restTemplate; 测试准备  新建User对象,用于下面不同请求方式的测试。 @Datapublic class User { /** * id */ private Long id; /** * 用户名 */ private String username; /** * 年龄 */ private Integer age; } GET请求  GET请求对应两个方法,getForObject()和getForEntity(),每个方法又对应有具体的三个重载方法。这两者的区别在于getForObject()返回的是一个简单的对象,而getForEntity()响应的数据中,还额外包含有与HTTP相关的信息,如响应码、响应头等。 /** * GET资源 (发送一个HTTP GET请求,返回的请求体将映射为一个对象) * <p> * 1. 执行根据URL检索资源的GET请求 * 2. 根据responseType参数匹配为一定的类型 * 3. getForObject()只返回所请求类型的对象信息 */@Testpublic void getForObject() { long id = 0; //URL中的{id}占位符最终将会用方法的id参数来填充 String url = \"http://localhost:9000/user/{id}\"; //重载1:最后一个参数是大小可变的参数列表,每个参数都会按出现顺序插入到指定URL的占位符中 User user = restTemplate.getForObject(url, User.class, id); System.out.println(\"user = \" + user); //重载2:将id参数放到Map中,并以id作为key,然后将这个Map作为最后一个参数 Map<String, String> urlParams = new HashMap<>(1); urlParams.put(\"id\", String.valueOf(id)); User user2 = restTemplate.getForObject(url, User.class, urlParams); System.out.println(\"user2 = \" + user2); //重载3:构造URL对象,要在url上进行字符串拼接,不推荐使用 url = \"http://localhost:9000/user/\" + id; User user3 = restTemplate.getForObject(URI.create(url), User.class); System.out.println(\"user3 = \" + user3);} /** * GET资源 (发送一个HTTP GET请求,返回的ResponseEntity包含了响应体所映射成的对象) * <p> * 1. 执行根据URL检索资源的GET请求 * 2. 根据responseType参数匹配为一定的类型 * 3. getForEntity()方法会返回请求的对象以及响应相关的额外信息 */ @Test public void getForEntity() { long id = 1; //URL中的{id}占位符最终将会用方法的id参数来填充 String url = \"http://localhost:9000/user/{id}\"; //重载1:同getForObject(),只不过返回的类型是ResponseEntity ResponseEntity<User> userResponseEntity = restTemplate.getForEntity(url, User.class, id); User user = userResponseEntity.getBody(); HttpStatus statusCode = userResponseEntity.getStatusCode(); int statusCodeValue = userResponseEntity.getStatusCodeValue(); HttpHeaders headers = userResponseEntity.getHeaders(); System.out.println(\"user = \" + user + \"; statusCode = \" + statusCode + \"; statusCodeValue = \" + statusCodeValue + \"; headers = \" + headers); //重载1:同getForObject(),只不过返回的类型是ResponseEntity Map<String, String> urlParams = new HashMap<>(1); urlParams.put(\"id\", String.valueOf(id)); ResponseEntity<User> userResponseEntity2 = restTemplate.getForEntity(url, User.class, urlParams); System.out.println(\"userResponseEntity2 = \" + userResponseEntity2); //重载3:同getForObject(),只不过返回的类型是ResponseEntity url = \"http://localhost:9000/user/\" + id; ResponseEntity<User> userResponseEntity3 = restTemplate.getForEntity(URI.create(url), User.class); System.out.println(\"userResponseEntity3 = \" + userResponseEntity3); } POST请求  POST请求对应三个方法,postForObject()、postForEntity()和postForLocation(),每个方法同样对应有三个具体的重载方法。postForObject()、postForEntity()类似于getForObject()和postForEntity(),postForLocation()返回的是一个URI对象。 /** * POST资源 (POST数据到一个URL,返回根据响应体匹配形成的对象) */@Testpublic void postForObject() { String url = \"http://localhost:9000/user\"; //重载1 & 重载2 User user1 = new User(); user1.setAge(20); user1.setUsername(\"张三\"); //第4个参数可以是Object... uriVariables 或者 Map<String, ?> uriVariables User u1 = restTemplate.postForObject(url, user1, User.class); System.out.println(\"user1 = \" + u1); //重载3 User user2 = new User(); user2.setAge(30); user2.setUsername(\"李四\"); User u2 = restTemplate.postForObject(URI.create(url), user2, User.class); System.out.println(\"user2 = \" + u2);} /** * POST资源 (POST数据到一个URL,返回包含一个对象的ResponseEntity,这个对象是从响应体中映射得到的) */@Testpublic void postForEntity() { String url = \"http://localhost:9000/user\"; // 重载1 & 重载2 User user3 = new User(); user3.setAge(25); user3.setUsername(\"王五\"); // 第4个参数可以是Object... uriVariables 或者 Map<String, ?> uriVariables ResponseEntity<User> userResponseEntity = restTemplate.postForEntity(url, user3, User.class); User userBody = userResponseEntity.getBody(); HttpStatus statusCode = userResponseEntity.getStatusCode(); int statusCodeValue = userResponseEntity.getStatusCodeValue(); HttpHeaders headers = userResponseEntity.getHeaders(); System.out.println(\"user = \" + userBody + \"; statusCode = \" + statusCode + \"; statusCodeValue = \" + statusCodeValue + \"; headers = \" + headers); // 重载3 User user4 = new User(); user4.setAge(35); user4.setUsername(\"陆六\"); ResponseEntity<User> userResponseEntity2 = restTemplate.postForEntity(URI.create(url), user4, User.class); System.out.println(\"userResponseEntity2 = \" + userResponseEntity2);} /** * POST资源 (POST数据到一个URL) * 如果服务端在响应的Location头信息中返回新资源的URL,接下来postForLocation()会以String的格式返回该URL */@Testpublic void postForLocation() { String url = \"http://localhost:9000/user\"; User user = new User(); user.setAge(28); user.setUsername(\"七七\"); // 重载1 & 重载2 // 第3个参数可以是Object... uriVariables 或者 Map<String, ?> uriVariables URI uri = restTemplate.postForLocation(url, user); if (Objects.nonNull(uri)) { String location = uri.toString(); System.out.println(\"location = \" + location); } // 重载3 URI uri1 = restTemplate.postForLocation(URI.create(url), user); if (Objects.nonNull(uri1)) { String location = uri1.toString(); System.out.println(\"location = \" + location); }} PUT请求  PUT请求只有一个方法:put(),对应三个具体的重载方法,put请求返回值为void。 /** * PUT资源 (PUT资源到特定的URL) */@Testpublic void put() { long id = 1; //URL中的{id}占位符最终将会用方法的id参数来填充 String url = \"http://localhost:9000/user/{id}\"; User user = new User(); user.setId(id); user.setUsername(\"update 张三\"); user.setAge(99); //重载1 restTemplate.put(url, user, id); //重载2 Map<String, String> urlParams = new HashMap<>(1); urlParams.put(\"id\", String.valueOf(id)); restTemplate.put(url, user, urlParams); //重载3 restTemplate.put(URI.create(\"http://localhost:9000/user/\" + id), user);} DELETE请求  DELETE请求同样只有一个方法:delete(),对应有三个具体的重载方法,delete请求返回值为void。 /** * DELETE资源 (在特定的URL上对资源执行HTTP DELETE操作) */@Testpublic void delete() { long id = 1; //URL中的{id}占位符最终将会用方法的id参数来填充 String url = \"http://localhost:9000/user/{id}\"; //重载1 restTemplate.delete(url, id); //重载2 Map<String, String> urlParams = new HashMap<>(1); urlParams.put(\"id\", String.valueOf(id)); restTemplate.delete(url, urlParams); //重载3 restTemplate.delete(URI.create(\"http://localhost:9000/user/\" + id));} HEAD & OPTIONS & PATCH 请求  这几种请求方式比较少见和少用,这里就不再说明了。 any(通用)请求  通用的请求主要是指execute()和exchange()方法,这两个方法又分别对应有三个和八个具体的重载方法。 /** * 交换资源 (在URL上执行特定的HTTP方法,返回包含对象的ResponseEntity,这个对象是从响应体中映射得到的) * 允许在发送给服务端的请求中设置头信息 * 支持GET、POST、PUT、DELETE... */@Testpublic void exchange() { long id = 1; String url = \"http://localhost:9000/user/{id}\"; //GET资源 //参数3是请求头部分;参数4是响应数据要转成对象;最后一个参数用于替换URL中的占位符 ResponseEntity<User> userResponseEntity = restTemplate.exchange(url, HttpMethod.GET, null, User.class, id); System.out.println(\"exchange = \" + userResponseEntity + \"; response body = \" + userResponseEntity.getBody()); //POST资源 String url2 = \"http://localhost:9000/user\"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); String jsonParams = \"{\\\"username\\\":\\\"123\\\",\\\"age\\\":23}\"; HttpEntity<User> httpEntity = new HttpEntity(jsonParams, headers); ResponseEntity<User> responseEntity = restTemplate.exchange(url2, HttpMethod.POST, httpEntity, User.class); System.out.println(\"exchange = \" + responseEntity + \"; response body = \" + responseEntity.getBody()); //PUT and DELETE忽请自行测试}   execute()的操作相对而言会比较麻烦,建议大家多使用exchange(),这里就不再贴代码进行说明了。 补充说明  以上测试代码可以在我的GitHub仓库中找到。","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"}],"tags":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/tags/Java/"},{"name":"RestTemplate","slug":"RestTemplate","permalink":"https://blog.mariojd.cn/tags/RestTemplate/"},{"name":"Spring","slug":"Spring","permalink":"https://blog.mariojd.cn/tags/Spring/"}]},{"title":"程序员神器,IntelliJ IDEA 2018.1 正式发布","slug":"程序员神器,IntelliJ IDEA 2018.1 正式发布","date":"2018-04-03","updated":"2018-10-29","comments":true,"path":"intellij-idea2018.1-officially-released.html","link":"","permalink":"https://blog.mariojd.cn/intellij-idea2018.1-officially-released.html","excerpt":"","keywords":"","text":"工欲善其事必先利其器,如果有一款IDE可以让你更高效地专注于开发以及源码阅读,为什么不试一试? 3月27日,jetbrains正式发布期待已久的IntelliJ IDEA 2018.1,再次让人眼前一亮:什么,还能这么玩? 下面,我们来快速了解一下 IDEA 2018.1 最新版本给我们带来哪些惊喜 1. stream代码自动生成更智能 IDEA对java代码的自动化生成令人惊叹,一个alt + enter或者alt + /可以省去很多敲打键盘的操作。本次更新对stream api的支持更加智能化,如上图,如果对一个list filer掉指定的type之后,在后续的map操作中,自动给你加上一个cast。 2. while循环优化 IDEA智能检测代码逻辑,将原本丑陋的代码自动改写,while优化也是继承自此理念。上图中展示的是,while if break 逻辑,直接修改成do while,代码颜值和易理解程度明显提升。 3. 优化多余的资源关闭操作 使用过IDEA的同学可能会经常看到代码里面有灰色的代码,这就提示你,这段代码是多余了,不可达的代码,可以删掉,代码更干净整洁。上图中展示的是,当你已经使用了try resource的方式来自动关闭资源,没有必要再手动调用一次close,显示成灰色,alt+enter一键自动删除。 4. 字符串数组自动排序 这个功能也是非常贴心,不得不佩服jetbrain对产品细节的打磨。如果你代码里面有一堆字符串常量,想做一个简单的按字母排序,只需要在数组上按下alter+enter,然后sort conetnt,IDEA会自动排序,秒级完成。 5. 拷贝构造函数完整性提示 拷贝构造函数在实际项目开发过程中也是比较常见的,通过同一个类的对象,生成另外一个对象,这个过程中,如果少set了一个属性,在后续的逻辑中很容易出现NPE。上图展示的是,当你实现拷贝构造函数的时候,忘了拷贝 myFileFilter和 myForcedToUseIdeaFileChooser这两个属性,IDEA会给你智能提示,让你修改。 6. postfix支持自定义模板postfix可以说是IDEA里面最有特色的一大功能,和live template并驾齐驱,完成一些非常快速的操作,关于这两个强大的功能可以翻到文末我之前录制的视频课程,这里不过多介绍。在这之前,我一直觉得postfix比不上live template,是因为他不支持自定义模板,一直想不通为什么jetbrain不开发这个功能来提高postfix的地位,没想到2018.1终于实现了自定义模板。 上图中,自定义了 .do这个postfix,我们在敲了一个表达式之后,唤出 do这个postfix,IDEA自动送你一个do while循环,并且随后光标自动跳转到你接下来需要书写逻辑的地方,棒不棒? 7. 自定义类生成文件名前后缀 这个功能是IDEA对自动生成类文件的增强,IDEA的自动生成类文件,举两个简单的例子 你创建好一个接口之后,直接在接口名处按下alt+enter,动一动上下键,一路按回车,可以给你自动生成这个接口的实现,默认后缀为Impl 你要对当前类写个UT,直接在当前类下按下cmd+shift+t,一路按回车,动一动上下键,空格键,可以给你自动生成这个类的UT,默认后缀为Test 之前这个默认值在IDEA中是不能修改的,新版本不仅可以修改这个默认后缀,甚至连默认前缀也可以修改,自由度更高,可以让自动生成的类更加贴合你的代码风格 8. debug异常模拟 IDEA的debug功能可以说让我爱不释手,无论是bug的排查,源码的阅读,超多惊艳的功能几乎每次都能让你瞬间定位关键代码。在之前的版本,IDEA已经实现了表达式(段落)求值,动态值修改,force return等强悍的调试功能,新版增加了Exception模拟功能。debug的时候,你可以在任意一个地方,模拟异常的抛出,这在验证代码的异常完备性方面非常有用。 9. 断点栈轨迹在阅读netty源码的过程中,我最喜欢的功能就是使用IDEA的调用栈迅速了解某段代码执行的上下文环境,调用栈被挂起之后,只需要动一动上下方向键,就可以快速熟悉这段代码的执行路径。 新版本中,如上图,IDEA对breakpoint的功能得到进一步的增强,让你的断掉调试在控制台留下轨迹,每经过一个断点,都会在控制台打印出这个断点的调用栈,下次,可以不用调试你就能知道断点处的代码原始的调用链是什么。 10. 更强大的全局文本替换 IDEA对文本的处理可以说是相当强悍了,无论搜索还是替换,可以自定义范围,文件名,甚至支持预览效果。新版本对全局文本替换功能进行进一步的扩充,上图展示的是,你想把所有 xxController文件替换成 xxProducer,直接上两段正则表达式就可以迅速完成,并且在窗口下方实时预览修改后的效果。 11. SpringBoot整合进一步优化IDEA和Spring的整合可以说是天衣无缝,Spring IOC和AOP的特性在IDEA中得到了充分的可视化体现,无比强大的关联功能让人叹为观止,详细可以翻到文末我录制的视频。 新版本更是对SpringBoot的整合达到了一种登峰造极的程度。上面图展示的是,当你的SpringBoot应用启动之后,在所有对外提供API的方法,你都可以直接点个鼠标就可以发起HTTP调用,发起HTTP调用有两种方式: 一种方式是直接发起调用,不用手动输入url,调用之后结果直接展示在控制台 另外一种调用方式更为强大,IDEA给你自动给你生成一个.http文件,这个文件里面,你可以自定义http请求的参数,url,并且每一次调用的结果都保存为一个文本文件,甚至可以进行调用结果的对比。 12. git部分提交与changelistIDEA对git的可视化支持以及各种快速便捷的操作在代码冲突解除、开源代码历史版本追溯、code review发挥了强大的作用,你根本不需要第二个可视化版本控制工具。在IDEA的版本控制管理的概念里有一个changelist的术语,指的是,你可以把每一处更改扔到不同的变更集合里,提交代码的时候,你可以选择不同的变更集合进行提交,剩下的变更集合,变更还在,但是不会提交到仓库中。这个应用场景是,比如,你遇到一个新项目,想快速了解这个项目,你可以随意做修改,把这些修改扔到一个自定义的changelist中,而在真正完成task的时候(一般刚开始接触新项目,实现的需求都比较简单,几行代码搞定),把需求实现的变更集进行提交即可,之前的实验代码可以留在你的项目中,你可以进一步地做实验。 本次版本的更新,细粒度版本控制得到了进一步的增强,上面这幅图展示的是,你在提交代码的时候,窗口右半侧左边栏,每一处改动都会有一个单选框,如果你在code review的时候,不想提交某一处改动,只需要把前面的单选框取消勾选即可,这样,结果就是,代码还在本地项目中,但是不会提交到版本库。 此外,你还可以把你不想提交的代码扔到一个自定义changelist里,代码还在,但是不会提交,如上图。 13. git历史记录增强在阅读netty源码的过程中,有的时候我会发现注释和代码逻辑牛头不对马嘴,于是,我使用了IDEA的git历史记录功能之后发现,原来这段注释是应用在很久之前的代码上的,老代码注释一直未删除,这也说明了你在阅读开源项目的时候,不要盲目相信权威,要多思考,要相信自己的判断。 本次git的历史记录的增强,指的是,你在阅读某个版本的代码,想了解这个版本中的某一个文件的提交历史,你可以直接在这个文件上右键,然后查看针对当前版本,查看这个文件的提交历史,所有的改动一览无余。 14. 弹出式javadoc 对的,新版本IDEA终于支持弹出式java doc了,当你鼠标放到某个方法,参数,类型上去的时候,自动给你弹出对应的文档的解释,并且给出文档表现形式是经过美化的。 15. Kubernetes支持 IDEA之所以如此优秀,是因为jetbrain一直会关注行业的最新动态,迅速实现对java生态圈中热门技术的整合。我记得当年Docker刚火起来的时候,IDEA就已经开始对Docker的整合了,并且在每个版本里面迅速迭代,目前IDEA对docker的支持已经做得非常出色了。 上图展示的是IDEA对Kubernetes的支持,只敲了几个字母,一个Kubernetes资源文件模板就生成了,然后,一个批量编辑,光标自动定位,一个完整的资源文件就已经写完了,IDEA在2018.1版本中对Kubernetes的支持做了非常多的增强,感兴趣的同学可以看下官网博客的专题介绍。 结束语以上就是我对本次 IntelliJ IDEA 2018.1 版本更新的解读,如果你非常迫切想提升开发效率,提升代码质量,可以跳转到我的IDEA系列课程,免费,看完这个课程之后再来读这篇文章,可能会更容易理解。 原文出处:IntelliJ IDEA 2018.1正式发布微信公众号:netty技术内幕注:以上内容有做部分修改","categories":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/categories/IDEA/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/tags/Java/"}]},{"title":"1万步21天钉钉运动大神赛","slug":"1万步21天钉钉运动大神赛","date":"2018-04-01","updated":"2018-09-29","comments":true,"path":"10-thousand-step-21-day-dingding-sports-competition.html","link":"","permalink":"https://blog.mariojd.cn/10-thousand-step-21-day-dingding-sports-competition.html","excerpt":"","keywords":"","text":"  3月5日,钉钉宣布启动 “ 酷公司运动季 ”,拿出88万元奖金鼓励上班族减肥,运动达标者不但可以成功减掉赘肉,还能一起来瓜分奖金。开启挑战可以选择7天、14天、21天赛程,只要在赛程内按照要求每天的钉钉运动步数达到10000步,即可获得奖励资格。支付2元运动契约金,即可加入“多走半小时,日行一万步“活动。  活动时间:2018年3月5日一4月21日(3月31日23:59:59为最晚参与时间)   目前所在的企业,从入职开始到现在,内部沟通协作的主要软件也就是钉钉。三月初的时候,偶然在钉钉运动一栏中发现了这个活动,当时也没怎么犹豫,果断支付了2元契约金,选的也是最长的21天挑战。没想到,坎坎坷坷这一路下来,发现这个挑战的难度还是蛮大的,不过好在几天前,终于让我拿下了这个21天大神挑战赛。   大概从大二开始的时候,就慢慢在养成跑步的习惯,不过一直都选择在晚上进行,因为如果是其它时间段的话,像早上或者是中午这种时间,南方的太阳这么火辣,一出汗就得换衣服、洗衣服,也是嫌麻烦事多。之前在学校夜跑的话,一般会选择晚课之后这个时间点,每次运动的量也不多,围着操场跑一二公里的样子也就够了,但好在基本每天都有坚持下来,总体大学这四年下来,也算是没丢了身体这个本钱,是蛮幸运的。   后来,也就是去年暑假,出来另外一个城市开始实习了。刚开始那会,也没怎么把线程切换过来,附近一时半会也没找到比较好的运动场所,所以就懈怠了没有去跑步。我更倾向于在环境好一点的地方来运动,像公园、大学这种一般环境都不错,很可惜这附近真的没有,没办法,也不太喜欢健身房这种商业化又比较压抑的场所。   大概实习一个月之后,发现身体实在吃不消了,果断上了某宝,买了一双运动鞋,铁了心要把身体弄好。到现在,这半年多的时间里,经历过和同事一起约跑的夜晚,也经历过一个人去远一点的学校、公园跑步的周末。   也许天生就比较喜欢运动。大学这四年里,每年的体测都能拿个80分+,知足。加上后来,选择了做一名程序员,如果不坚持运动的话,我不知道未来会怎样,是先秃顶呢?还是挺着个啤酒肚?而且这个职业,吐槽的新闻可不少,像去麦当劳约会的,背双肩、穿拖鞋上班的,还有看秃顶猜年龄能力的,总之是不会太轻松的啦。   身处于高压的行业,最大的感受除了经常被产品那群人当“猴”耍之外,还有永远不确定的需求、改不完的Bug以及写不完的代码。列一下程序员最容易得的职业病。 键盘手 鼠标手 颈椎病 腰椎病 眼疲劳、用眼过度 久坐对前列腺的危害以及肥胖问题 饮食、作息不规律导致的胃病等一系列问题   该庆幸自己还年轻,可以及早的做好预防措施。如果你也喜欢某一运动,那就把它当成一辈子的事业去坚持吧!我一直都坚信,身体好才是最重要的正事。看到过这么一句话,如果暂时做不到拼得比别人狠,那就比谁拼得久吧。   没错,钉钉这次举办的21天大神赛不是开始,也不会是结束。喜欢运动的那种感觉,很轻松、释放、无所顾忌。一辈子很长,很多事要用行动去践行,共勉!    4月21日,钉钉21天大神赛正式落下帷幕。最后Show一下战绩。","categories":[{"name":"随笔","slug":"随笔","permalink":"https://blog.mariojd.cn/categories/随笔/"}],"tags":[{"name":"跑步","slug":"跑步","permalink":"https://blog.mariojd.cn/tags/跑步/"},{"name":"钉钉运动","slug":"钉钉运动","permalink":"https://blog.mariojd.cn/tags/钉钉运动/"}]},{"title":"实现 Java 热部署的几种解决方案","slug":"实现 Java 热部署的几种解决方案","date":"2018-03-30","updated":"2019-06-02","comments":true,"path":"several-solutions-to-realize-java-hot-deployment.html","link":"","permalink":"https://blog.mariojd.cn/several-solutions-to-realize-java-hot-deployment.html","excerpt":"","keywords":"","text":"百度百科:热部署,就是应用正在运行的时候就可以升级软件,而不需要重新启动应用。 spring-loaded 依赖 <dependency> <groupId>org.springframework</groupId> <artifactId>springloaded</artifactId> <version>1.2.5.RELEASE</version> </dependency> ps:据说此方法还需要手动编译(Ctrl + Shift + F9),请亲测! spring-boot-devtools 依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional><!-- 表示依赖不会传递,之后依赖该module的项目如需继续使用,则需要再次引入 --></dependency> JRebel 安装插件 激活JRebel 启动运行 若不起效 全局配置 对比 方式 效果 spring-loaded 需手动编译 spring-boot-devtools 会重新加载整个应用 JRebel 只加载修改类(当有错误时不会)。无需手动编译,不会重新加载整个应用 参考文章 SpringBoot初始教程之热部署(五)JRebel无限制版IntelliJ IDEA 热部署插件 JRebel 安装激活及使用intellij idea 热部署 jrebel 详细配置","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"},{"name":"热部署","slug":"Java/热部署","permalink":"https://blog.mariojd.cn/categories/Java/热部署/"}],"tags":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/tags/Java/"},{"name":"热部署","slug":"热部署","permalink":"https://blog.mariojd.cn/tags/热部署/"}]},{"title":"Fiddler 实现微信授权开发调试","slug":"Fiddler 实现微信授权开发调试","date":"2018-03-30","updated":"2019-06-02","comments":true,"path":"the-implementation-of-wechat-authorized-development-and-debugging-in-fiddler.html","link":"","permalink":"https://blog.mariojd.cn/the-implementation-of-wechat-authorized-development-and-debugging-in-fiddler.html","excerpt":"","keywords":"","text":"一、下载、安装Fiddler二、微信授权调试   案发现场: 某天,一名正儿八经的开发”猿”,在疯狂一顿Coding之后,他完成了微信授权登录功能的编码。下来他想先在本地调试一下,然后再部署到线上环境。于是在本地Run起了Project,假设微信回调的地址是:localhost:9002。这时,他就可以利用Fiddler进行代理测试,具体操作实现请参考以下两种方法。    PS: 请先自行登录微信公众平台进行相关配置。 1. Fiddler + 微信web开发者工具 打开微信web开发者工具,选择公众号网页开发: 修改Fiddler中的Hosts配置信息    完成以上配置,即可利用微信web开发者工具在PC本地进行微信授权调试,就这么简单。 2. Fiddler + 手机(需结合方法1的配置操作) 确保手机、电脑在同一个局域网,查看PC的ip地址 Fiddler代理配置 手机代理信息配置    完成以上配置,即可使用手机进行微信授权(可自行构造请求微信授权),微信回调后会走PC运行的项目接口,大概就这么简单。 三、推荐两个小工具 内网映射工具(第三种调试方法,具体请参考在线教程):NATAPP Hosts修改软件:SwitchHosts","categories":[{"name":"抓包工具","slug":"抓包工具","permalink":"https://blog.mariojd.cn/categories/抓包工具/"}],"tags":[{"name":"Fiddler","slug":"Fiddler","permalink":"https://blog.mariojd.cn/tags/Fiddler/"},{"name":"微信授权","slug":"微信授权","permalink":"https://blog.mariojd.cn/tags/微信授权/"},{"name":"代理调试","slug":"代理调试","permalink":"https://blog.mariojd.cn/tags/代理调试/"}]},{"title":"IDEA 快捷键拆解系列(后记)","slug":"IDEA 快捷键拆解系列(后记)","date":"2018-03-28","updated":"2019-06-02","comments":true,"path":"idea-shortcut-key-disassembly-series-postscript.html","link":"","permalink":"https://blog.mariojd.cn/idea-shortcut-key-disassembly-series-postscript.html","excerpt":"","keywords":"","text":"  没错,这是IDEA快捷键拆解系列的最后一篇文章了,也是对应本系列前言的一篇。   对于绝大多数开发者而言,把一款编辑器用熟了之后,再突然切换到其它款的编辑器的话,这个适应阶段一般都比较艰辛和漫长。但博主有理由相信,大神级别的程序员总是会先去熟悉快捷键,然后其它应用的快捷键冲突能改的都改掉,而不是把快捷键都修改成原来的。个人也很推荐这么做,总结了以下几种原因。 团队协作开发的统一; 官方定义的快捷键代表着本编辑器的最佳实践操作; 方便与其它同系列的子产品快捷键做统一。   使用JetBrains的产品有一个很大的好处,这在前言那篇文章里面也提到过,这家公司拥有很多种主流开发语言的编辑器产品。所以一般只要学好了IDEA的快捷键,那么在跨语言学习和开发使用新的编辑器的时候,我们都可以非常轻松、快速的迁移和上手快捷键操作。其实这里IDEA还有一个更为人性化的地方,就是针对从Eclipse、NetBeans或者是从Emacs中迁移过来的开发者,都提供了一整套的原生编辑器快捷键,我们也只需要做一下小小的配置即可(如下图所示),不过还是再一次建议使用IDEA默认的快捷键来做开发。   在这里,介绍一下IDEA强大的Local History功能,这对于经常接触和使用VCS(版本控制系统)的开发者来说是比较好理解的。对于Local History,从字面上的意思来理解,就是IDEA为开发者提供了一个本地的VCS,然后针对我们项目上的任何操作,在本地都做了一个历史的记录。具体查看和操作可以通过右键点击,或者是快捷键Alt + 反引号,然后找到Local History即可。   如上图所示,除了Show History之外,我们还可以配合着进行Put Label的操作,这也是Local History中一个非常棒的辅助功能。   针对IDEA中,博主目前还发现了几个强大的功能,如SSH Session和Test Restful Web Service,这两个可以在顶部的工具栏“Tools”中展开找到。 SSH Session:类似于XShell、SecureCRT和Putty这样的服务器客户端连接工具,好像暂时不能存储连接的会话信息Test Restful Web Service:类似于Postman的测试工具,功能方面可能还有待完善   还有一个就是Database功能,这个可以在顶部的工具栏“View”,然后是“Tool Windows”中展开找到。   当然了,IDEA中还有一些其它的功能也很强大,这可能在整一个的拆解系列中没有细说到,又或者是还没留意到,如果后续有需要补充和完善的,博主会及时的更新到这里,也非常欢迎各位的留言。孰能生巧,快捷键用多了自然而然就熟了,但前提是得知道有这么一个的快捷键,包括博主本人目前也还在一直的学习当中。好啦,持续了断断续续一整个月的时间,本系列到此就先暂告一段落了。最后,与大家一起加油、共勉!","categories":[{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/categories/编辑器/"},{"name":"个人感悟","slug":"编辑器/个人感悟","permalink":"https://blog.mariojd.cn/categories/编辑器/个人感悟/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"Eclipse","slug":"Eclipse","permalink":"https://blog.mariojd.cn/tags/Eclipse/"},{"name":"个人感悟","slug":"个人感悟","permalink":"https://blog.mariojd.cn/tags/个人感悟/"}]},{"title":"解决 IDEA 无法提示导入 java.util.Date 的问题","slug":"解决 IDEA 无法提示导入 java.util.Date 的问题","date":"2018-03-27","updated":"2019-06-02","comments":true,"path":"solve-the-problem-that-idea-cannot-prompt-to-import-java.util.Date.html","link":"","permalink":"https://blog.mariojd.cn/solve-the-problem-that-idea-cannot-prompt-to-import-java.util.Date.html","excerpt":"","keywords":"","text":"  之前有一段时间在使用IDEA的时候,发现通过快捷键Alt + Enter导入并没有提示有java.util.Date的包,仅仅只有java.sql.Date的包。于是每次使用都需要通过手写import java.util.Date;来进行导包。博主在好生不爽了一段时间后,终于在网上找到了解决办法,本文就是用来记录一下解决过程的。   找到设置(Alt + Shift + S),搜索“Auto Import”。如下图所示,只需要把java.util.Date导入提示的排除设置删除即可。   同理,因为一般项目中很少使用得到java.sql.Date,所以我们可以添加这么一条设置,用于排除java.sql.Date的导入提示。","categories":[{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/categories/编辑器/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"JetBrains","slug":"JetBrains","permalink":"https://blog.mariojd.cn/tags/JetBrains/"}]},{"title":"IDEA 快捷键拆解系列(十九):Postfix 篇","slug":"IDEA 快捷键拆解系列(十九):Postfix 篇","date":"2018-03-26","updated":"2019-06-02","comments":true,"path":"idea-shortcut-key-disassembly-series-nineteen.html","link":"","permalink":"https://blog.mariojd.cn/idea-shortcut-key-disassembly-series-nineteen.html","excerpt":"","keywords":"","text":"  这是IDEA快捷键拆解系列的第十九篇。   本文将介绍一下IDEA另外一个强大的功能:Postfix。   Postfix和Live Template有点类似,但目前来看是不支持自定义的,在设置面板(快捷键:Ctrl + Alt + S)搜索“Postfix”即可。   从上图可以看到,目前官方定义了Java、JavaScript和Kotlin三种编程语言的Postfix。其中,Description区域描述了该Postfix的作用,Before区域是我们编码前,默认使用Tab键后,即可转换成After区域的代码效果。以下是Java中常用的一些Postfix。 ! assert cast else filed for fori format forr if inst instanceof iter lambda nn not notnull null opt return sout stream switch synchronized throw try twr while   从定义来看,Java中的Postfix也就大概三十个,相信熟练掌握之后可以大大提高我们的编码效率。","categories":[{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/categories/编辑器/"},{"name":"快捷键","slug":"编辑器/快捷键","permalink":"https://blog.mariojd.cn/categories/编辑器/快捷键/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"JetBrains","slug":"JetBrains","permalink":"https://blog.mariojd.cn/tags/JetBrains/"}]},{"title":"IDEA 快捷键拆解系列(十八):Live Templates 篇","slug":"IDEA 快捷键拆解系列(十八):Live Templates 篇","date":"2018-03-25","updated":"2019-06-02","comments":true,"path":"idea-shortcut-disassembly-series-eighteen.html","link":"","permalink":"https://blog.mariojd.cn/idea-shortcut-disassembly-series-eighteen.html","excerpt":"","keywords":"","text":"  这是IDEA快捷键拆解系列的第十八篇。   本文将介绍一下IDEA强大的Live Template功能。   首先,我们要知道Live Template是在哪里定义的,先按Ctrl + Shift + S进入设置,接着在输入框输入“Live Template”就可以定位到了,如下图所示。   从上图可以看到,IDEA官方已经帮我们定义好了一些常用的Live Template,而且针对不同的文件类型也划分了不同的Live Template Group。查看Live Template的快捷键是Ctrl + J,这里列几个比较常用的。 iterate (迭代) itar:Iterate elements of array,操作顺序迭代数组 //1.定义一个数组String[] strings = new String[];//2.输入itar后会有提示,按`Enter`确认后会自动输出以下内容,默认是按`Tab`键快速进行光标切换for (int i = 0; i < strings.length; i++) { String string = strings[i];} ritar:Iterate elements of array in reverse order,反转迭代数组 String[] strings = new String[];//输入ritar后会有提示,按`Enter`确认后会自动输出以下内容,默认是按`Tab`键进行光标快速切换for (int i = strings.length - 1; i >= 0; i--) { String string = strings[i];} iter:Iterate (for each..in),ForEach迭代 List<String> stringList = new ArrayList<>();//输入iter后会有提示,按`Enter`确认后会自动输出以下内容,默认是按`Tab`键进行光标快速切换for (String s : stringList) {} fori:Create iteration loop,含下标的普通迭代 //输入fori后会有提示,按`Enter`确认后会自动输出以下内容,默认是按`Tab`键进行光标快速切换for (int i = 0; i < ; i++) {} itli:Iterate elements of java.util.List,List迭代List<String> stringList = new ArrayList<>();//输入itli后会有提示,按`Enter`确认后会自动输出以下内容,默认是按`Tab`键进行光标快速切换for (int i = 0; i < stringList.size(); i++) { String s = stringList.get(i);} itco:Iterate elements of java.util.Collection,iterator迭代 List<String> stringList = new ArrayList<>();//输入itco后会有提示,按`Enter`确认后会自动输出以下内容,默认是按`Tab`键进行光标快速切换for (Iterator<String> iterator = stringList.iterator(); iterator.hasNext(); ) { String next = iterator.next();} iten:Iterate java.util.Enumeration itit:Iterate java.util.Iterator ittok:Iterate tokens from String itve:Iterate elements of java.util.Vector define (定义) St String thrthrow new psf public static final prsf private static final psfi public static final int psfs public static final String psfs public static final String geti:Inserts singleton method getInstance public static $CLASS_NAME$ getInstance() { return ;} ifn:Inserts if null statement if ($VAR$ == null) {} inn:Inserts if not null statement if ($VAR$ != null) {} inst:Checks object type with instanceof and down-casts it if ($EXPR$ instanceof $TYPE$) { $TYPE$ $VAR1$ = ($TYPE$)$EXPR$; $END$} lazy:Performs lazy initialization if ($VAR$ == null) { $VAR$ = new $TYPE$();} lst:Fetches last element of an array mn:Sets lesser value to a variable mx:Sets greater value to a variable toar:Stores elements of java.util.Collection into array main psvm public static void main(String[] args) {} print (打印) sout:Prints a string to System.out System.out.println(); souf:Prints a formatted string to System.out System.out.printf(\"\"); serr:Prints a string to System.err System.err.println(\"\"); soutm:Prints current class and method names to System.out System.out.println(\"className.methodName\"); soutv:Prints a value to System.out System.out.println(\"variable name = \" + variable value); soutp:Prints method parameter names and values to System.out System.out.println(\"parameter name = [\" + parameter value + \"]\"); Maven dep:dependency <dependency> <groupId></groupId> <artifactId></artifactId> <version></version></dependency> pl:plugin <plugin> <groupId></groupId> <artifactId></artifactId> <version></version></plugin> repo:repository <repository> <id></id> <name></name> <url></url></repository> SQL col:new column definition $col$ $type$ $null$$END$ ins:insert rows into a table insert into $table$ ($columns$) values ($END$); sel:select all rows from a table select * from $table$$END$; selc:select the number of specific rows in a table select count(*) from $table$ $alias$ where $alias$.$END$; selw:select specific rows from a table select * from $table$ $alias$ where $alias$.$END$; tab:new table definition create table $table$ ( $col$ $type$ $null$$END$); upd:update values in a table update $table_name$ set $col$ = $value$ where $END$;   当然了,Live Templat强大的地方不仅在于官方定义好的这部分,更重要是还支持自定义的Live Template。在学习工作中,我们可以尝试把一些重复的代码抽出来变成模板,接下来我们就来看看如何自定义Live Template。 配置自定义Live Template 修改作用范围 K 新建POJO,输入自定义的Live Template快捷键,如这里是cps,按Enter键选择后光标将停留在VAR1的位置,默认按Tab键即可快速切换到VAR2的位置。 由于个人疏忽,所以截图部分Live Template存在部分错误,下面是更正后的Live Template。/** * $VAR1$ */private String $VAR2$;$END$","categories":[{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/categories/编辑器/"},{"name":"快捷键","slug":"编辑器/快捷键","permalink":"https://blog.mariojd.cn/categories/编辑器/快捷键/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"JetBrains","slug":"JetBrains","permalink":"https://blog.mariojd.cn/tags/JetBrains/"}]},{"title":"IDEA 快捷键拆解系列(十七):Debug 篇","slug":"IDEA 快捷键拆解系列(十七):Debug 篇","date":"2018-03-24","updated":"2019-06-02","comments":true,"path":"idea-shortcut-key-disassembly-series-seventeen.html","link":"","permalink":"https://blog.mariojd.cn/idea-shortcut-key-disassembly-series-seventeen.html","excerpt":"","keywords":"","text":"  这是IDEA快捷键拆解系列的第十七篇。   本文主要讲解如何利用好IDEA强大的断点调试功能,含快捷键、经验分享等。 Shortcuts:快捷键 快捷键 描述 Ctrl + F8 添加/取消断点,或直接在左侧点击添加 Ctrl + Shift + F8 查看所有断点,为断点添加条件等 F8 执行下一步 Shift + Alt + F8 强制执行下一步 F9 跳到下一个断点,如果没有则直接运行结束 Alt + F9 运行到光标所在处 Ctrl + Alt + F9 强制运行到光标处 F7 进入代码内部 Shift + F8 退出代码内部 Alt + F10 跳转到断点执行处 Alt + F8 表达式求值 Mute Breakpoints:禁用断点 Condition Breakpoints:条件断点 若光标在断点处,则快捷键为Ctrl + Shift + F8 若光标不在断点处,可通过查看所有断点来添加条件,快捷键同上Ctrl + Shift + F8 通过右键点击断点来添加条件 Evaluate Expression:表达式求值,快捷键Alt + F8 setValue:一般用于动态修改Debug中运行的值   在分析源码的时候,良好的Debug能力可以帮助我们快速的读懂别人的代码。IDEA为开发者们提供了全面的Debug支持,相信熟练掌握后可以大大的提高我们的Debug能力。","categories":[{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/categories/编辑器/"},{"name":"快捷键","slug":"编辑器/快捷键","permalink":"https://blog.mariojd.cn/categories/编辑器/快捷键/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"JetBrains","slug":"JetBrains","permalink":"https://blog.mariojd.cn/tags/JetBrains/"}]},{"title":"IDEA 快捷键拆解系列(十六):插件篇","slug":"IDEA 快捷键拆解系列(十六):插件篇","date":"2018-03-23","updated":"2019-06-02","comments":true,"path":"idea-shortcut-key-disassembly-series-sixteen.html","link":"","permalink":"https://blog.mariojd.cn/idea-shortcut-key-disassembly-series-sixteen.html","excerpt":"","keywords":"","text":"  这是IDEA快捷键拆解系列的第十六篇。   本文整理了博主用过的一些IDEA插件。其中,五星是强烈推荐,四星是比较推荐,三星仅供参考,两星的就不多说了,一星是炫酷型的插件,一般帮助也不大。最后,欢迎留言补充。 插件名称 作用描述 推荐指数 Jrebel for Intellij 热部署插件,用Jrebel做热部署,效果比使用Spring-loaded和Spring-boot-devtools好多了 ☆☆☆☆☆ CodeGlance 在代码区右边显示代码小地图,方便快速定位代码 ☆☆☆☆☆ Translation 翻译插件,目前支持谷歌和有道。切换翻译源的快捷键是:Ctrl + Shift + S,翻译选中内容的快捷键是:Ctrl + Shift + Y,弹出翻译框的快捷键是:Ctrl + Shift + O ☆☆☆☆☆ Lombok plugin 使用了Lombok需要此插件,否则可能会让人抓狂 ☆☆☆☆☆ Alibaba Java Coding Guidelines 阿里巴巴Java团队开发的一款代码规范检查插件 ☆☆☆☆☆ Properties to YAML Converter 用于将properties类型的文件转换成yaml类型 ☆☆☆☆☆ GenerateAllSetter 快速生成相应实体所有属性的Setter方法 ☆☆☆☆☆ Git Commit Template Create a git commit message with the following template ☆☆☆☆☆ Markdown Navigator Markdown编辑插件 ☆☆☆☆ Protobuf Support protobuf文件支持插件 ☆☆☆☆ stackoverflow 右键选择并跳转Stack Overflow的插件 ☆☆☆☆ snakeYAML plugin YAML文件支持检查插件 ☆☆☆☆ FindBugs-IDEA 潜在Bug检查 ☆☆☆☆ MetricsReloaded 代码复杂度检查 ☆☆☆☆ Statistic 代码统计插件 ☆☆☆☆ CheckStyle-IDEA 代码规范检查插件 ☆☆☆☆ AceJump 用于查找代码并快速定位跳转。快捷键:Ctrl + ;;然后出现蓝色区域可输入要查找的关键字;黄色部分就是对应进行跳转快捷键,挨着的绿色部分是查找的结果 ☆☆☆☆ emacsIDEAs 类似于AceJump插件 ☆☆☆☆ IdeaVim 支持在IDEA中使用Vim的插件 ☆☆☆☆ Key Promoter 快捷键显示插件,适合新手 ☆☆☆☆ ANSI Highlighter 高亮插件 ☆☆☆ Identifier Highlighter 高亮插件 ☆☆☆ GsonFormat 把Json转换成实体类 ☆☆☆ IdeaJad 反编译插件 ☆☆☆ Maven helper Maven帮助插件 ☆☆ CamelCase Switch easily between CamelCase, camelCase, snake_case and SNAKE_CASE. 快捷键:SHIFT + ALT + U ☆☆ Eclipse Code Formatter Eclipse 代码风格格式化插件 ☆☆ UML Support UML支持插件 ☆☆ Nyan Progress Bar 彩虹进度条插件 ☆☆ Background Image Plus 背景图片 ☆ Power mode II 抖动气泡 ☆ Activate-power-mode 抖动气泡 ☆","categories":[{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/categories/编辑器/"},{"name":"快捷键","slug":"编辑器/快捷键","permalink":"https://blog.mariojd.cn/categories/编辑器/快捷键/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"JetBrains","slug":"JetBrains","permalink":"https://blog.mariojd.cn/tags/JetBrains/"}]},{"title":"IDEA 快捷键拆解系列(十五):经验篇","slug":"IDEA 快捷键拆解系列(十五):经验篇","date":"2018-03-22","updated":"2019-06-02","comments":true,"path":"idea-shortcut-key-disassembly-series-fifteen.html","link":"","permalink":"https://blog.mariojd.cn/idea-shortcut-key-disassembly-series-fifteen.html","excerpt":"","keywords":"","text":"  这是IDEA快捷键拆解系列的第十五篇。   本文整理了一些博主本人在学习工作中比较常用到的快捷键,有需要的可以参考一下,也欢迎留言补充。 类型 快捷键 描述 1. 查找 Ctrl + N 查找类文件 2. 查找 Ctrl + Shift + N 查找文件 3. 查找 Ctrl + F 当前文件查找 4. 查找 Ctrl + R 当前文件查找和替换 5. 查找 Ctrl + Shift + F 全局查找 6. 查找 Ctrl + Shift + R 全局查找和替换 7. 查找 Ctrl + Shift + A 查找指定动作 8. 查找 Double Shift Search everywhere 类型 快捷键 描述 1. 定位 F2 定位到下一处的错误地方 2. 定位 Shift + F2 定位到上一处的错误地方 3. 定位 F3 跳转到下一同一内容处 4. 定位 Shift + F3 跳转到上一同一内容处 5. 定位 F4 跳转到引用处 6. 定位 F12 跳转到上一次光标所在的面板 7. 定位 Ctrl + Shift + F12 用于打开当前文件所在本地的文件夹 8. 定位 Ctrl + B 或者 Ctrl + 鼠标点击 跳转声明处 9. 定位 Ctrl + Alt + B 跳转实现处 10. 定位 Ctrl + Shift + B 跳转返回值类型的声明处 11. 定位 Ctrl + G 跳转指定行 12. 定位 Ctrl + U 跳转父类 13. 定位 Ctrl + Shift + Alt + U 图表方式查看继承结构 14. 定位 Ctrl + Alt + Home 跳转项目的启动、入口类 15. 定位 Alt + Home 跳转顶部的项目导航条 16. 定位 Alt + 向左箭头 跳转左边的Tab 17. 定位 Alt + 向右箭头 跳转右边的Tab 18. 定位 Ctrl + Alt + 向左箭头 跳转上一次光标所在的位置 19. 定位 Ctrl + Alt + 向右箭头 跳转下一次光标所在的位置 20. 定位 Alt + Shift + Enter 将光标定位到上一行 21. 定位 Shift + Enter 将光标定位到下一行 22. 定位 Ctrl + Shift + Enter 将光标定位到方法外 23. 定位 Ctrl + 向左箭头 将光标定位到头部 24. 定位 Ctrl + 向右箭头 将光标定位到尾部 25. 定位 Ctrl + ] 将光标定位到代码块开始处 26. 定位 Ctrl + [ 将光标定位到代码块结尾处 类型 快捷键 描述 1. 选择 Alt + J 选中下一处当前选择的内容 2. 选择 Shift + Alt + J 取消选中下一处当前选择的内容 3. 选择 Ctrl + Alt + Shift + J 全部选中当前文件中当前选择的内容 4. 选择 Ctrl + Tab Switcher 5. 选择 Ctrl + E 最近操作过的文件列表 6. 选择 Ctrl + Shift + E 最近修改过的文件列表 7. 选择 Ctrl + A 选择当前全部 8. 选择 Ctrl + W 逐层往外扩展并选中内容 9. 选择 Ctrl + Shift + W 取消逐层往外扩展选中的内容 10. 选择 Ctrl + Shift + 向左箭头 从光标处起,依次往左选中内容 11. 选择 Ctrl + Shift + 向右箭头 从光标处起,依次往右选中内容 12. 选择 Shift + Home 从光标处起,一次选中至本行的头部 13. 选择 Shift + End 从光标处起,一次选中至本行的尾部 14. 选择 Ctrl + Shift + 空格键 智能代码提示 类型 快捷键 描述 1. 查看 Alt + Q 查看类定义信息 2. 查看 Ctrl + P 查看参数定义 3. 查看 Ctrl + Q 查看Documentation 4. 查看 Ctrl + F12 查看当前类结构 5. 查看 Ctrl + Shift + V 查看剪贴板 6. 查看 Ctrl + H 查看类的层次关系 7. 查看 Ctrl + Shift + H 查看方法的层次关系 8. 查看 Ctrl + Alt + H 查看方法的调用层次结构 9. 查看 Ctrl + Shift + I 弹框查看方法实现 类型 快捷键 描述 1. 编辑 Ctrl + Z 撤销 2. 编辑 Ctrl + Shift + Z 取消撤销 3. 编辑 Ctrl + X 剪切 4. 编辑 Ctrl + C 复制 5. 编辑 Ctrl + V 粘贴 6. 编辑 CTRL + D 拷贝当前行到下一行 7. 编辑 Ctrl + Y 删除当前行 8. 编辑 Delete 删除 9. 编辑 Alt + Delete 带检查的安全删除,可用于方法 10. 编辑 Ctrl + Shift + U 英文大小写切换 11. 编辑 Ctrl + O 覆盖父类方法 12. 编辑 Ctrl + I 实现接口方法 13. 编辑 Alt + Enter 最常用的快捷键,含包选择导入,帮助创建等 14. 编辑 Alt + Insert 在包中就是选择文件类型用于新建;在文件中就是添加构造器,Getter/Setter,toString实现等 15. 编辑 Ctrl + Alt + Insert 在当前文件夹下选择文件类型用于创建 16. 编辑 Ctrl + Alt + T 选择并进行代码包围 17. 编辑 Ctrl + J 插入Live Template 18. 编辑 Ctrl + Alt + J 选择Live Tmeplate 19. 编辑 Ctrl + 斜杠 单行注释 20. 编辑 Ctrl + Shift + 斜杠 多行注释 21. 编辑 Ctrl + Alt + L 格式化代码 22. 编辑 Ctrl + Alt + O 去掉未使用的导包 23. 编辑 Alt + 向上箭头 定位到上一个方法 24. 编辑 Alt + 向下箭头 定位到下一个方法 25. 编辑 Ctrl + Shift + 向上箭头 整行(方法)上移 26. 编辑 Ctrl + Shift + 向下箭头 整行(方法)下移 27. 编辑 Ctrl + Shift + Alt + 向上箭头 定位到上一处修改过的地方 28. 编辑 Ctrl + Shift + Alt + 向下箭头 定位到下一处修改过的地方 29. 编辑 Ctrl + Shift + T 创建单元测试 30. 编辑 Ctrl + Shift + J 转换为单行连接 31. 编辑 Ctrl + Delete 从光标处往后删除 32. 编辑 Ctrl + Backspace 从光标处往前删除 类型 快捷键 描述 1. 切換 Ctrl + F4 关闭当前Tab 2. 切換 Ctrl + Shift + ] 切换到下一个项目 3. 切換 Ctrl + Shift + [ 切换到上一个项目 4. 切換 Shift + ESC 关闭、隐藏当前面板 5. 切換 Ctrl + Shift + F12 关闭、隐藏所有面板 类型 快捷键 描述 1. 重构 F5 拷贝 2. 重构 F6 移动 3. 重构 Shift + F6 重命名 4. 重构 Ctrl + Alt + Shift + T 重构当前 5. 重构 Ctrl + Alt + V 抽取变量 6. 重构 Ctrl + Alt + C 抽取常量 7. 重构 Ctrl + Alt + F 抽取字段 8. 重构 Ctrl + Alt + P 抽取参数 9. 重构 Ctrl + Alt + M 抽取方法 10. 重构 Ctrl + Alt + N 内联 11. 重构 Ctrl + F6 修改签名 类型 快捷键 描述 1. Run Shift + F10 普通运行当前 2. Run Shift + F9 Debug运行当前 3. Run Alt + Shift + F10 普通运行所选 4. Run Alt + Shift + F9 Debug运行所选 5. Run Ctrl + F2 停止当前运行 类型 快捷键 描述 1. VCS Alt + 反引号 VCS操作 2. VCS Ctrl + T 拉取远程仓库 3. VCS Ctrl + K 提交本地暂存区 4. VCS Ctrl + M 查看提交信息历史列表 5. VCS Ctrl + Alt + A 添加版本控制 6. VCS Ctrl + Shift + K 提交远程仓库 7. VCS Ctrl + Alt + Z 撤销当前的修改 8. VCS Ctrl + Enter commit、提交 9. VCS Alt + Shift + C 查看最近的修改 类型 快捷键 描述 1. 编译 Ctrl + F9 编译项目 2. 编译 Ctrl + Shift + F9 编译当前 类型 快捷键 描述 1. 书签 F11 添加、取消书签 2. 书签 Ctrl + F11 带标志的书签 3. 书签 Shift + F11 查看所有书签 类型 快捷键 描述 1. 工具窗 Alt + 1 项目面板 2. 工具窗 Alt + 4 普通Run项目的面板 3. 工具窗 Alt + 5 Debug运行项目的面板 4. 工具窗 Alt + 6 TODO面板 5. 工具窗 Alt + 7 结构面板 6. 工具窗 Alt + 9 版本控制面板 7. 工具窗 Alt + 12 终端面板 8. 工具窗 Alt + F4 关闭当前IDEA 9. 工具窗 ESC 从工具窗返回到代码区 类型 快捷键 描述 1. 断点调试 Ctrl + F8 添加、取消断点 2. 断点调试 Ctrl + Shift + F8 查看所有断点 3. 断点调试 F8 跳到下一步 4. 断点调试 Alt + Shift + F8 强制跳到下一步 5. 断点调试 F7 进入代码内部 6. 断点调试 Shift + F8 退出代码内部 7. 断点调试 Alt + F9 运行到光标处 8. 断点调试 Ctrl + Alt + F9 强制运行到光标处 类型 快捷键 描述 1. 折叠展开 Ctrl + 减号 折叠当前方法 2. 折叠展开 Ctrl + 加号 展开当前方法 3. 折叠展开 Ctrl + Shift + 减号 折叠当前类的所有方法 4. 折叠展开 Ctrl + Shift + 加号 展开当前类的所有方法 类型 快捷键 描述 1. 配置设置 Ctrl + 反引号 配置开关 2. 配置设置 Ctrl + Alt + S 系统设置 3. 配置设置 Ctrl + Shift + Alt + S 项目结构设置","categories":[{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/categories/编辑器/"},{"name":"快捷键","slug":"编辑器/快捷键","permalink":"https://blog.mariojd.cn/categories/编辑器/快捷键/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"JetBrains","slug":"JetBrains","permalink":"https://blog.mariojd.cn/tags/JetBrains/"}]},{"title":"IDEA 快捷键拆解系列(十四):Help 篇","slug":"IDEA 快捷键拆解系列(十四):Help 篇","date":"2018-03-21","updated":"2019-06-12","comments":true,"path":"idea-shortcut-key-disassembly-series-fourteen.html","link":"","permalink":"https://blog.mariojd.cn/idea-shortcut-key-disassembly-series-fourteen.html","excerpt":"","keywords":"","text":"  这是IDEA快捷键拆解系列的第十四篇。   以下是关于Help导航项及其每一子项的拆解介绍,其中,加粗部分的选项是博主认为比较重要的。 Help 1. Find Action ( 通过描述动作,查找相关命令 ) Ctrl + Shift + A Help ( 查找相关帮助,含快捷键等 ) Getting started Keymap Reference ( PDF快捷键清单 ) Demos and screencasts Tip of the Day What’s New in IDEA ( 用于查看IDEA新特性 ) Productivity Guide Setting Summary Support Center ( 帮助中心 ) Submit Feedback ( 反馈提交 ) Show Log in Explorer ( IDEA日志 ) Edit Custom Properties ( 在idea.properties中添加个人配置 ) Edit Custom VM Options ( 在idea64.exe.vmoptions中添加个人启动配置 ) Debug Log Settings Register ( 注册 ) Check for Update ( 检查更新 ) JRebel… ( Jrebel插件 ) About","categories":[{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/categories/编辑器/"},{"name":"快捷键","slug":"编辑器/快捷键","permalink":"https://blog.mariojd.cn/categories/编辑器/快捷键/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"JetBrains","slug":"JetBrains","permalink":"https://blog.mariojd.cn/tags/JetBrains/"}]},{"title":"IDEA 快捷键拆解系列(十三):Window 篇","slug":"IDEA 快捷键拆解系列(十三):Window 篇","date":"2018-03-20","updated":"2019-06-02","comments":true,"path":"idea-shortcut-key-disassembly-series-thirteen.html","link":"","permalink":"https://blog.mariojd.cn/idea-shortcut-key-disassembly-series-thirteen.html","excerpt":"","keywords":"","text":"  这是IDEA快捷键拆解系列的第十三篇。   以下是关于Window导航项及其每一子项的拆解介绍,其中,加粗部分的选项是博主认为比较重要的。 Window Quick Run Root Maven Goal Ctrl + Shift + Alt + R Quick Run Maven Goal Ctrl + Alt + R Store Current Layout as Default ( 存储当前为默认布局 ) Restore Default Layout ( 还原默认布局 ) Shift + F12 Active Tool Window (Tool Windows相关) Hide Active Tool Window( 隐藏激活工具窗 ) Shift + Esc Hide Side Tool Windows Restore Windows ( 还原窗口 ) Ctrl + Shift + F12 Close Active Tab ( 关闭激活的Tab )Ctrl + Shift + F4 Jump to Last Tool Window ( 定位到上一个操作工具窗 ) F12 Maximize Tool Window ( 最大化工具窗口 ) Ctrl + Shift + 引号 Pinned mode ( 勾选上为固定模式的窗口(默认),不勾选上当离开当前窗口时会隐藏 ) Docked mode ( 勾选上为对接模式的窗口(默认),不勾选上当离开当前窗口时会隐藏 ) Floting mode ( 勾选上为悬浮模式的窗口 ) Windowed mode Split mode Group Tabs Resize Stretch to Left Ctrl + Shift + 向左箭头 Stretch to Right Ctrl + Shift + 向右箭头 Stretch to Top Ctrl + Shift + 向上箭头 Stretch to Bottom Ctrl + Shift + 向下箭头 Editor Tabs (Tabs相关) Select Next Tab ( 切换到下一个Tab ) Alt + 向右箭头 Select Previous Tab ( 切换到上一个Tab ) Alt + 向左箭头 Pin Active Tab ( 为当前窗口添加Pin ) Show Hidden Tabs Close ( 关闭当前 ) Ctrl + F4 Close Others Close All ( 关闭所有 ) Close Unmodified ( 关闭未修改的 ) Close All But Pinned ( 关闭所有但除了有Pin的 ) Reopen Closed Tab ( 重新打开关闭的窗口 ) Move Right ( 分割到右边窗口 ) Move Down ( 分割到下边窗口 ) Change Splitter Oritentation ( 修改分割方向,就是如果是右边窗口就换到下边,下边窗口就换到右边 ) Unsplit ( 取消当前分割窗口 ) Unsplit All ( 取消全部分割窗口 ) Goto Next Splitter ( 跳转下一个分割窗口 ) Goto Previous Splitter ( 跳转前一个分割窗口 ) Tabs Placement ( 对应Tab窗口所在位置,默认是: Top ) Top Bottom Left Right None Show Tabs In Single Row Sort Tabs By Filename ( Tab窗口按文件名排序 ) Open New Tabs At The End Natifications Close First Close All Background Tasks Show Auto Show Next Project Window (切换到下一个项目窗口) Ctrl + Alt + ] Previous Project Window (切换到前一个项目窗口) Ctrl + Alt + [","categories":[{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/categories/编辑器/"},{"name":"快捷键","slug":"编辑器/快捷键","permalink":"https://blog.mariojd.cn/categories/编辑器/快捷键/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"JetBrains","slug":"JetBrains","permalink":"https://blog.mariojd.cn/tags/JetBrains/"}]},{"title":"IDEA 快捷键拆解系列(十二):VCS 篇","slug":"IDEA 快捷键拆解系列(十二):VCS 篇","date":"2018-03-19","updated":"2019-06-02","comments":true,"path":"idea-shortcut-key-disassembly-series-twelve.html","link":"","permalink":"https://blog.mariojd.cn/idea-shortcut-key-disassembly-series-twelve.html","excerpt":"","keywords":"","text":"  这是IDEA快捷键拆解系列的第十二篇。   以下是关于VCS导航项及其每一子项的拆解介绍,其中,加粗部分的选项是博主认为比较重要的。 VCS Local history Show History ( 查看本地历史 ) Show History for Selection ( 查看所选文件的本地历史 ) Put Label VCS Operations Popup ( 弹出版本控制操作窗 ) Alt + 反引号 Commit Changes ( 提交修改,用Git话是提交到本地暂存区 ) Ctrl + K Update Project ( 项目更新,用Git话是从Remote拉取代码 ) Ctrl + T Integrate Project Refresh File Status Show Changes ( 查看本地修改列表 ) Ctrl + Shift + Alt + D Git Commit File ( 进行提交修改 ) Ctrl + K Add ( 添加版本控制 ) Ctrl + Alt + A Annotate ( 显示/隐藏注释 ) Show Current Revision( 查看当前版本号 ) Compare with the Same Repository Version ( 与当前远程仓库的当前文件进行对比 ) Compare with Latest Repository Version ( 与当前最新仓库的当前文件进行对比 ) Compare with ( 弹出版本历史列表进行选择对比 ) Compare With Branch ( 弹出分支进行选择对比 ) Show history ( 查看当前文件的Git提交修改历史 ) Show History for Selection ( 查看当前选择内容的Git提交修改历史) Revert ( 撤销修改 ) Ctrl + Alt + Z Resolve Conflicts Branches ( 分支相关操作 ) Tag ( 标签相关操作 ) Merge Changes ( 合并修改 ) Stash Changes ( 暂存修改 ) UnStash Changes ( 取消暂存修改 ) Reset HEAD Remotes Clone Fetch Pull Push ( 从本地暂存区提交到Remote ) Ctrl + Shift + K Rebase Rebase my GitHub fork Create Pull Request Create Patch Apply Patch Apply Patch from Clipboard Shelve Changes Checkout from Version Control ( 从版本控制中检出项目 ) Github Git Mercurial Subversion TFS Import into Version Control ( 导入项目到版本控制 ) Import into CVS Create Git Repository ( 创建Git出库 ) Import into Subversion ( 导入到SVN ) Create Mercurial Repository Share Project On Github ( 分享项目到Github ) Browse VCS Repository Browse VCS Repository Show Git Repository Log Browse Subversion Repository Sync Settings","categories":[{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/categories/编辑器/"},{"name":"快捷键","slug":"编辑器/快捷键","permalink":"https://blog.mariojd.cn/categories/编辑器/快捷键/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"JetBrains","slug":"JetBrains","permalink":"https://blog.mariojd.cn/tags/JetBrains/"}]},{"title":"IDEA 快捷键拆解系列(十一):Tools 篇","slug":"IDEA 快捷键拆解系列(十一):Tools 篇","date":"2018-03-18","updated":"2019-06-02","comments":true,"path":"idea-shortcut-key-disassembly-series-eleven.html","link":"","permalink":"https://blog.mariojd.cn/idea-shortcut-key-disassembly-series-eleven.html","excerpt":"","keywords":"","text":"  这是IDEA快捷键拆解系列的第十一篇。   以下是关于Tools导航项及其每一子项的拆解介绍,其中,加粗部分的选项是博主认为比较重要的。 Tools Tasks & Contexts Switch Task Alt + Shift + T Open Task Alt + Shift + N Close Active Task Alt + Shift + W Edit ‘Default tack’ Add changelist for ‘Default task’ Show Description Open in Browser Alt + Shift + B Analyze Stacktrace From Task - Configure Servers Save Context ( 保存当前编辑区状态 ) Alt + Shift + S Load Context ( 加载某一保存的编辑区状态 ) Alt + Shift + L Clear Context ( 清空编辑区状态 ) Alt + Shift + X Save Live as Template ( 将当前设为Live Template ) Generate JavaDoc ( 生成JavaDoc ) Save Project as Template ( 将项目设为Template ) Manage Project Templates ( 管理项目Template ) 6. Capture Memory Snapshot 阿里编码规约 ( Alibaba Java Coding Guidelines plugin ) Deployment ( 部署,可选择连接FTP、SFTP等 ) Upload to Default Server Upload to Ctrl + Alt + Shift + X Download from Default Server Download from Compare Local File with Deployed Version Sync Local Subtree with Deployed Sync with Deployed to Configuration Options Automatic Upload Browse Remote Host Test RESTful Web Service ( 用于进行接口测试,类似于”Postman” ) Groovy Console WebServices Generate Java Code From Wsdl ShowDeployed Web Services Axis… Start SSH session ( 开启ssh会话连接 )","categories":[{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/categories/编辑器/"},{"name":"快捷键","slug":"编辑器/快捷键","permalink":"https://blog.mariojd.cn/categories/编辑器/快捷键/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"JetBrains","slug":"JetBrains","permalink":"https://blog.mariojd.cn/tags/JetBrains/"}]},{"title":"IDEA 快捷键拆解系列(十):Run 篇","slug":"IDEA 快捷键拆解系列(十):Run 篇","date":"2018-03-17","updated":"2019-06-02","comments":true,"path":"idea-shortcut-key-disassembly-series-ten.html","link":"","permalink":"https://blog.mariojd.cn/idea-shortcut-key-disassembly-series-ten.html","excerpt":"","keywords":"","text":"  这是IDEA快捷键拆解系列的第十篇。   以下是关于Run导航项及其每一子项的拆解介绍,其中,加粗部分的选项是博主认为比较重要的。 Run Run current file ( 运行当前文件 ) Shift + F10 Debug current file ( Debug运行当前文件 ) Shift + F9 Run File with Coverage ( 以统计覆盖的形式运行当前文件 ) Run with JRebel File ( JRebel插件,以普通形式运行 ) Debug with JRebel File ( JRebel插件,以Debug形式运行 ) JREBEL synchronized project with remote server Run ( 选择文件运行 ) Alt + Shift + F10 Debug ( 选择文件Debug运行 ) Alt + Shift + F9 Attach to Local Process Edit Configurations ( 配置编辑 ) Import Test Results From File Stop ( 关闭运行 ) Ctrl + F2 Show Running List ( 查看运行列表 ) Reload Changed Classes Step Over ( 跳到下一步 ) F8 Force Step Over ( 强制跳到下一步) Alt + Shift + F8 Step Into ( 进入代码内部 ) F7 Force Step Into ( 强制进入代码内部 ) Alt + Shift + F7 Smart Step Into ( 智能进入代码内部 ) Shift + F7 Step Out ( 退出代码内部 ) Shift + F8 Run to Cursor ( 运行到光标处 ) Alt + F9 Force Run to Cursor ( 强制运行到光标处 ) Ctrl + Alt + F9 Drop Frame Pause Program Resume Program F9 Evaluate Expression Alt + F8 Quick Evaluate Expression Ctrl + Alt + F8 Show Execution Point Alt + F10 Toggle Line Breakpoint ( 添加/取消行断点,断点会在当前行一直存在 ) Ctrl + F8 Toggle Method Breakpoint ( 添加/取消方法断点,断点会在整个方法内存在 ) Toggle Temporary Line Breakpoint ( 添加/取消临时断点,断点会在使用一次之后消失 ) Ctrl + Shift + Alt + F8 Toggle Breakpoint enabled View Breakpoints ( 查看所有断点 ) Ctrl + Shift +F8 Get thread dump","categories":[{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/categories/编辑器/"},{"name":"快捷键","slug":"编辑器/快捷键","permalink":"https://blog.mariojd.cn/categories/编辑器/快捷键/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"JetBrains","slug":"JetBrains","permalink":"https://blog.mariojd.cn/tags/JetBrains/"}]},{"title":"IDEA 快捷键拆解系列(九):Build 篇","slug":"IDEA 快捷键拆解系列(九):Build 篇","date":"2018-03-16","updated":"2019-06-02","comments":true,"path":"idea-shortcut-key-disassembly-series-nine.html","link":"","permalink":"https://blog.mariojd.cn/idea-shortcut-key-disassembly-series-nine.html","excerpt":"","keywords":"","text":"  这是IDEA快捷键拆解系列的第九篇。   以下是关于Build导航项及其每一子项的拆解介绍,其中,加粗部分的选项是博主认为比较重要的。 Build Build Project ( 项目构建 ) Ctrl + F9 Build Module ( 模块构建 ) Recompile ( 重新编译 ) Ctrl + Shift + F9 4. Rebuild Project ( 项目重新构建 ) 5. Generate Ant Build Build Artifacts","categories":[{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/categories/编辑器/"},{"name":"快捷键","slug":"编辑器/快捷键","permalink":"https://blog.mariojd.cn/categories/编辑器/快捷键/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"JetBrains","slug":"JetBrains","permalink":"https://blog.mariojd.cn/tags/JetBrains/"}]},{"title":"IDEA 快捷键拆解系列(八):Refactor 篇","slug":"IDEA 快捷键拆解系列(八):Refactor 篇","date":"2018-03-15","updated":"2019-06-02","comments":true,"path":"idea-shortcut-key-disassembly-series-eight.html","link":"","permalink":"https://blog.mariojd.cn/idea-shortcut-key-disassembly-series-eight.html","excerpt":"","keywords":"","text":"  这是IDEA快捷键拆解系列的第八篇。   以下是关于Refactor导航项及其每一子项的拆解介绍,其中,加粗部分的选项是博主认为比较重要的。 Refactor Refactor This ( 重构当前 ) Ctrl + Alt + Shift + T Rename ( 重命名 ) Shift + F6 Rename File Change Signature ( 修改方法、类的签名,含参数、返回值类型等 ) Ctrl + F6 Type Migration ( 类型迁移 ) Ctrl + Shift + F6 Make Static ( 添加Static关键字 ) Convert To Instance Method ( 转换为实例方法 ) Move ( 移动文件 ) F6 Copy ( 拷贝文件 ) F5 Safe Detele ( 安全删除,可用在方法上进行快速删除 ) Alt + Delete Extract( 提取 ) Variable ( 变量 ) Ctrl + Alt + V Constant ( 常量 ) Ctrl + Alt + C Filed ( 类字段 ) Ctrl + Alt + F Parameter ( 参数 ) Ctrl + Alt + p Functional Parameter ( 函数式参数 ) Ctrl + Alt + Shift + P Parameter Object Mehtod ( 方法 ) Ctrl + Alt + M Type Parameter Method Object Delegate Interrface Superclass Subquery ad CTE Inline ( 转换为内联、方法链形式的调用 ) Ctrl + Alt + N Find and Replace Code Duplicates Invert Boolean Pull Members Up Push Members Down Push ITDs In Use Interface Where Possible Replace Inheritance with Delegation Remobe Middleman Wrap Method Return Value Convert Anonymous to Inner Encapsulate Fields ( 封装字端,用于生成Getter/Setter ) Replace Temp With Query Replace Constructor with Factory Method Replace Constructor with Builder Generify Migrate Lombok ( Lombok插件:添加 )- Default @Date Default @Getter Default @Setter Default @EqualsAndHashcode Default @ToString @Log (and friends) Delombok ( Lombok插件:删除 )- All lombok annotations @Data @Value @Wither @Delegate @Builder @Constructors @Getter @Setter @EqualsAndHashcode @ToString @Log (and friends) Internationalize(国际化)","categories":[{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/categories/编辑器/"},{"name":"快捷键","slug":"编辑器/快捷键","permalink":"https://blog.mariojd.cn/categories/编辑器/快捷键/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"JetBrains","slug":"JetBrains","permalink":"https://blog.mariojd.cn/tags/JetBrains/"}]},{"title":"IDEA 快捷键拆解系列(七):Analyze 篇","slug":"IDEA 快捷键拆解系列(七):Analyze 篇","date":"2018-03-14","updated":"2019-06-02","comments":true,"path":"idea-shortcut-key-disassembly-series-seven.html","link":"","permalink":"https://blog.mariojd.cn/idea-shortcut-key-disassembly-series-seven.html","excerpt":"","keywords":"","text":"  这是IDEA快捷键拆解系列的第七篇。   以下是关于Analyze导航项及其每一子项的拆解介绍,其中,加粗部分的选项是博主认为比较重要的。 Analyze Inspect Code ( 代码分析 ) Code Cleanup ( 代码清理 ) Run Inspection by Name Ctrl + Alt + Shift + I Configure Current File Analysis Ctrl + Alt + Shift + H View Offline Inspection Results Infer Nullity ( 推断无效代码 ) Locate Duplicates Calculate Metrics ( 指标计算 ) 9. Show Coverage Data Ctrl + Alt + F6 Analyze Dependencies ( 分析依赖 ) Analyze Backward Dependencies Analyze Module Dependencies( 分析模块依赖 ) Analyze Dependency Matrix Analyze Cyclic Dependencies Analyze Data Flow to Here Analyze Data Flow from Here Analyze Stacktrace FindBugs (FindBugs插件) Analyze Current File Ctrl + Alt + Shift + F Analyze Class ( non-anonymous ) under Cursor: Ctrl + Alt + Shift + Analyze Module Files Analyze Project Files Analyze All Modified Files: Alt + Shift + C Analyze changelist files: Alt + Shift + A","categories":[{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/categories/编辑器/"},{"name":"快捷键","slug":"编辑器/快捷键","permalink":"https://blog.mariojd.cn/categories/编辑器/快捷键/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"JetBrains","slug":"JetBrains","permalink":"https://blog.mariojd.cn/tags/JetBrains/"}]},{"title":"IDEA 快捷键拆解系列(六):Code 篇","slug":"IDEA 快捷键拆解系列(六):Code 篇","date":"2018-03-13","updated":"2019-06-02","comments":true,"path":"idea-shortcut-key-disassembly-series-six.html","link":"","permalink":"https://blog.mariojd.cn/idea-shortcut-key-disassembly-series-six.html","excerpt":"","keywords":"","text":"  这是IDEA快捷键拆解系列的第六篇。   以下是关于Code导航项及其每一子项的拆解介绍,其中,加粗部分的选项是博主认为比较重要的。 Code Override Methods ( 重写覆盖方法 ) Ctrl + O Implements Methods ( 实现接口方法 ) Ctrl + I Delegate Methods Gengrate ( 用于生成Construct、Getter/Setter、toString等) Alt + Insert Surround With ( 生成包围代码 ) Ctrl + Alt +T Unwarp/Remove ( 取消代码包围 ) Ctrl + Shift + Delete Completion Basic Ctrl + 空格 SmartType ( 智能选择并实现 ) Ctrl + Shift + 空格 Cyclic Expand Word ( 循环往上选择单词 ) Alt + / Cyclic Expand Word (Backwrad)( 循环往下选择单词 ) Alt + Shift + / Folding Expand ( 方法展开 ) Ctrl + 加号 Collapse ( 方法折叠 ) Ctrl + 减号 Expand Recursively ( 同上,方法展开 ) Ctrl + Alt + 加号 Collapse Recursively ( 同上,方法折叠 ) Ctrl + Alt + 减号 Expand All ( 全部方法展开 ) Ctrl + Shift + 加号 Collapse All ( 全部方法折叠 ) Ctrl + Shift + 减号 Expand to level… Expand all to level… Expand doc comments ( 展开Java doc注释 ) Collapse doc comments ( 折叠Java doc注释 ) Fold Selection / Remove region Ctrl + 句点 Fold Code Block Ctrl + Shift + 句点 Insert Live Template ( 选择Live Templates模板 ) Ctrl + J Surround with Live Template ( 选择Live Templates模板 ) Ctrl + Alt + J Comment with Line Comment ( 行注释 ) Ctrl + / Comment with Block Comment ( 块注释 ) Ctrl + Shift + / Reformat Code ( 格式化代码 ) Ctrl + Alt + L Show Reformat File Dialog ( 弹出格式化弹框 ) Ctrl + Alt + Shift + L Auto-Indent Lines Ctrl + Alt + I Optimize Imports ( 去除未引用的包导入声明 ) Ctrl + Alt + O Rearrange Code ( 重新整理代码 ) Move Statement Down ( 方法、代码下移 ) Ctrl + Shift + 向下箭头 Move Statement Up ( 方法、代码上移 ) Ctrl + Shift + 向上箭头 Move Element Left Ctrl + Alt + Shift + 向左箭头 Move Element Rigth Ctrl + Alt + Shift + 向右箭头 Move Line Down Alt + Shift + 向下箭头 Move Line Up Alt + Shift + 向上箭头 Update Copyright","categories":[{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/categories/编辑器/"},{"name":"快捷键","slug":"编辑器/快捷键","permalink":"https://blog.mariojd.cn/categories/编辑器/快捷键/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"JetBrains","slug":"JetBrains","permalink":"https://blog.mariojd.cn/tags/JetBrains/"}]},{"title":"IDEA 快捷键拆解系列(五):Navigate 篇","slug":"IDEA 快捷键拆解系列(五):Navigate 篇","date":"2018-03-12","updated":"2019-06-02","comments":true,"path":"idea-shortcut-key-disassembly-series-five.html","link":"","permalink":"https://blog.mariojd.cn/idea-shortcut-key-disassembly-series-five.html","excerpt":"","keywords":"","text":"  这是IDEA快捷键拆解系列的第五篇。   以下是关于Navigate导航项及其每一子项的拆解介绍,其中,加粗部分的选项是博主认为比较重要的。 Navigate Class ( 查找类 ) Ctrl + N File ( 查找文件 ) Ctrl + Shift + N Symbol ( 查找标志符号,常用与查找方法名,变量名等 ) Ctrl + Shift +Alt + N Custom Folding Ctrl + Alt + 句点 Line/Column ( 跳转指定行 ) Ctrl + G Back ( 跳转前一个编辑过的位置 ) Ctrl + Alt + 左箭头 Forward ( 跳转后一个编辑过的位置 ) Ctrl + Alt + 右箭头 Last Edit Location ( 同上,跳转前一个编辑过的位置 ) Ctrl + Shift + Backspace Next Edit Location Bookmarks Toogle Bookmark ( 定义书签 ) F11 Toogle BookMark With Mnemonic ( 定义带标记的书签 ) Ctrl + F11 Show Bookmarks ( 查看所有书签 ) Shift + F11 Next Bookmark ( 上一个书签 ) previous Bookmark ( 下一个书签 ) Select In ( 弹出多功能的Select Target面板 ) Alt + F1 Jump to Navigation Bar ( 跳转到项目导航条 ) Alt + Home Declaration ( 跳转到声明处,同F4 ) Ctrl + B Implementations ( 跳转到实现处 ) Ctrl + Alt + B Type Declaration Ctrl + Shift + B Super Class ( 跳转父类 ) Ctrl + U Test ( 创建单元测试 ) Ctrl + Shift + T Related Symbol ( 跳转项目的启动入口类 ) Ctrl + Alt + Home File structure ( 查看文件(类)结构 ) Ctrl + F12 File Path ( 弹出所在的本地文件路径 ) Ctrl + Alt+ F12 Type Hierarchy ( 类的层级结构 ) Ctrl + H Method Hierarchy ( 方法的层级结构 ) Ctrl + Shift + H Call Hierarchy ( 调用层级结构 ) Ctrl + Alt+ H Next Highlighted Error ( 下一个高亮的错误 ) F2 Previous Highlighted Error ( 前一个高亮的错误 ) Shift + F2 Next Method ( 下一个方法 ) Alt + 向上箭头 Previous Method ( 上一个方法 ) Alt + 向下箭头","categories":[{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/categories/编辑器/"},{"name":"快捷键","slug":"编辑器/快捷键","permalink":"https://blog.mariojd.cn/categories/编辑器/快捷键/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"JetBrains","slug":"JetBrains","permalink":"https://blog.mariojd.cn/tags/JetBrains/"}]},{"title":"IDEA 快捷键拆解系列(四):View 篇","slug":"IDEA 快捷键拆解系列(四):View 篇","date":"2018-03-11","updated":"2019-06-02","comments":true,"path":"idea-shortcut-key-disassembly-series-four.html","link":"","permalink":"https://blog.mariojd.cn/idea-shortcut-key-disassembly-series-four.html","excerpt":"","keywords":"","text":"  这是IDEA快捷键拆解系列的第四篇。   以下是关于View导航项及其每一子项的拆解介绍,其中,加粗部分的选项是博主认为比较重要的。 View Tool Windows(工具窗) Project (项目) Alt + 1 Favorites (标签) Alt + 2 Run (项目运行) Alt + 4 Debug (项目Debug运行)Alt + 5 TODO (TODO) Alt + 6 Structure (结构) Alt + 7 Version Control (版本控制) Alt + 9 Terminal (终端) Alt + 12 Quick Definition (查看方法,类or变量的定义) Ctrl + shift +I Show siblings Quick Documentation (查看JavaDoc文档) Ctrl + Q Show ByteCode (查看编译后的文件字节码) Parameter Info (查看参数信息) Ctrl + P Expression Type (查看表达式类型) Ctrl + Shift + P Context Info (查看上下文) Alt +Q Jump to Source (快速跳转到类、变量定义处,相当于Ctrl + 鼠标点击,也相当与Ctrl + B) F4 Recent Files (列出最近打开的文件列表) Ctrl + E Recently changed Files (列出最近编辑过的文件列表) Ctrl + Shift + E Recent Changes (最近修改) Alt + Shift + C Compare With Ctrl + D Compare With Clipboard 15. Quick Switch Scheme (快读修改计划、组合)Ctrl + 后引号 Toolbar (用于上面的工具栏展示状态切换) Tool Buttons (用于周边的工具窗口展示状态切换) Status Bar (用于最下边的状态栏窗口展示状态切换) Navigation Bar (用于导航条的展示状态切换) Active Editor Show Whitespaces (展示空白) Show Line Numbers (展示行号) Show Gutter Icons (展示行号旁边的指导图标) Show Indent Guides (展示缩进的指导) Use Soft Wraps (使用合适的自动换行) Show Import Popups (选中则表示要展示手动导入的弹框,否则就会进行自动匹配导入) BiDi Text Direction Content-based Left-to-Right Right-to-Left Enter(Exit) Presentation Mode (展示模式) Enter Distraction Free Mode Enter(Exit) Full Screen (全屏模式)","categories":[{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/categories/编辑器/"},{"name":"快捷键","slug":"编辑器/快捷键","permalink":"https://blog.mariojd.cn/categories/编辑器/快捷键/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"JetBrains","slug":"JetBrains","permalink":"https://blog.mariojd.cn/tags/JetBrains/"}]},{"title":"IDEA 快捷键拆解系列(三):Edit 篇","slug":"IDEA 快捷键拆解系列(三):Edit 篇","date":"2018-03-10","updated":"2019-06-02","comments":true,"path":"idea-shortcut-key-disassembly-series-three.html","link":"","permalink":"https://blog.mariojd.cn/idea-shortcut-key-disassembly-series-three.html","excerpt":"","keywords":"","text":"  这是IDEA快捷键拆解系列的第三篇。   以下是关于Edit导航项及其每一子项的拆解,其中,加粗部分的选项是博主认为比较重要的。 Edit Undo ( 撤销 ) Ctrl + Z Redo ( 重做还原、取消撤销 ) Ctrl + Shift + Z Cut ( 裁剪 ) Ctrl + X Copy ( 复制 ) Ctrl + C Copy Path ( 复制绝对路径 ) Ctrl + Shift + C Copy as Plain Test Copy Reference ( 复制相对路径 ) Ctrl + Alt + Shift + C Paste ( 粘贴 ) Ctrl + V Paste from History ( 从粘贴板历史选择粘贴,可使用对应数字进行快速选择 ) Ctrl + Shift + V Paste Simple ( 简单粘贴 ) Ctrl + Alt + Shift + V Delete ( 删除 ) Delete Find Find ( 在当前文件查找 ) Ctrl + F Replace ( 在当前文件查找替换 ) Ctrl + R Find Next / Move to Next Occurrence ( 让光标快速移动到所选内容的下一个出现处 ) F3 Find Previous / Move to Previoud Occurrence ( 让光标快速移动到所选内容的上一个出现处 ) Shift + F3 Find Word at Caret Ctrl + F3 Select All Occurrences ( 选择当前选中内容在当前文件的全部内容。举个栗子:当前文件选中了“User”,那么当前文件中的所有“User”都会被选择,可用于快速更改或删除 ) Ctrl +Alt + Shift + J Add Selection for Next Occurrence ( 依次往下开始选择当前选中内容的下一个同一内容 ) Alt + J Unselect Occurrence ( 依次往下取消选择当前选中内容的下一个同一内容 ) Alt + Shift + J Find in Path ( 在路径中查找,全局查找 ) Ctrl + Shift + F Replace in Path ( 在路径中查找替换,全局查找替换 ) Ctrl + Shift + R Search Structurally ( 通过模板结构查找 ) Replace Structurally ( 替换模板结构 ) Find Usages ( 查找所选内容在项目中的所有出现处 ) Alt + F7 Find Usages Settings Ctrl + Alt + Shift + F7 Show Usages ( 在项目中查找展示所选内容的全部使用处 ) Ctrl + Alt + F7 Find Usages in File ( 在当前文件中查找所选内容的使用处 ) Ctrl + F7 Highlight Usages in File ( 在当前文件中高亮所选内容使用处 ) Ctrl + Shift + F7 Recent Find Usages Find by XPath Ctrl + Alt + X , F Macros ( 类似于录像机器人功能,用于记录操作,然后点播放可以还原记录的操作过程 ) Play Back Last Macro Start Macro Recording Edit Macros Play Saved Macros Column Selection Mode ( 行、列选择模式,默认是行选择模式 ) Alt + Shift + Insert Select All ( 选择全部 ) Ctrl + A Extend Selection ( 逐渐扩展选择区域 ) Ctrl + W Shrink Selection ( 逐渐缩减选择区域 ) Ctrl + Shift + W Complete Current Statement ( 把光标快速定位到当前代码块外 ) Ctrl + Shift + Enter Join Lines ( 用于折叠代码到一行 ) Ctrl + Shift + J Fill Paragraph Duplicate Kibe ( 向下复制整行 ) Ctrl + D Indent Selection ( 整行缩进 ) Tab Unindent Line or Selection ( 整行缩退 ) Shift + Tab Toggle Cas ( 大小写切换 ) Ctrl + Shift + U Concert Indents ( 切换缩进风格 ) To Spaces To Tabs Next Parameter Tab Previous Parameter Shift + Tab 28. Encode XML/HTML Special Characters Shift-Up Ctrl + Alt + Shift + 句点 Shift-Down Ctrl + Alt + Shift + 逗号 Shift-Up more ( 快速修改驼峰单词定义,可转换为按“-”或者“_”分隔 ) Ctrl + Alt + Shift + K Shift-Down more ( 选择当前选中内容在当前文件的全部内容。举个栗子:当前文件选中了“User”,那么当前文件中的所有“User”都会被选择,可用于快速更改或删除 ) Ctrl + Alt + Shift + J Edit as Table","categories":[{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/categories/编辑器/"},{"name":"快捷键","slug":"编辑器/快捷键","permalink":"https://blog.mariojd.cn/categories/编辑器/快捷键/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"JetBrains","slug":"JetBrains","permalink":"https://blog.mariojd.cn/tags/JetBrains/"}]},{"title":"IDEA 快捷键拆解系列(二):File 篇","slug":"IDEA 快捷键拆解系列(二):File 篇","date":"2018-03-09","updated":"2019-06-02","comments":true,"path":"idea-shortcut-key-disassembly-series-two.html","link":"","permalink":"https://blog.mariojd.cn/idea-shortcut-key-disassembly-series-two.html","excerpt":"","keywords":"","text":"  这是IDEA快捷键拆解系列的第二篇。   以下是关于File导航项及其每一子项的拆解,其中,加粗部分的选项是博主认为比较重要的。 File New Project ( 新建项目 ) Project from Existing Sources ( 从本地导入项目,包括Eclipse、Maven、Gradle项目等 ) Project from Version Control ( 版本控制,含以下五种版本管理系统 ) GitHub Git Mercurial Subversion TFS Module( 新建子模块 ) Module from Existing Sources ( 从本地导入模块,包括Eclipse、Maven、Gradle项目等 ) Java Class Aspect File Scratch File Ctrl + Alt + Shift + Insert Directory FXML File package-info.java HTML File Styleshoot JavaScript File TypeScript File CFML/CFC file CoffeeScript File JavaFXApplication Singleton ( 新建单例类( 饿汉模式 )) XSLT Stylesheet - Edit File Templates ( 编辑文件模板 ) GUI Form ( Swing开发中,用于快速创建GUI表单类 ) Dialog ( Swing开发中,用于创建GUI对话类 ) Form Snapshot Resource Bundle XML Configuration File ( 创建XML配置文件 ) JSP Tag Library Descriptor ( JSP标签库描述 ) Spring Config ( Spring配置文件 ) Diagram ( 图表建模 ) Java Class Diagram Module Dependencies Google Guice ( 谷歌开源的一款依赖注入框架 ) Guice Module Guice Provider Guice Binding Annotation Guice Scope Annotation Guice Method Interceptor Data Source ( 数据源 ) Open( 从本地打开 ) Open URL( 通过URL打开 ) Open Recent… ( 选择最近的项目打开 ) Close Project ( 关闭项目,回到欢迎界面 ) Settings ( 设置 ) Ctrl + Alt + S Project Structure ( 项目结构 ) Ctrl + Alt + Shift + S Other Settings ( 其它配置 ) Default Settings ( 默认设置 ) Default Project Structure ( 默认项目结构 ) Import Settings Export Settings Export to Eclipse ( 导出Eclipse项目 ) Settings Repository Save All Ctrl + S Synchronized Ctrl + Alt + Y Invalidate Caches/Restart ( 清除项目索引缓存 ) Export to HTML Print Add Favorites… File Encoding ( 设置文件编码 ) Line Separators… ( 设置行分隔符 ) Make File Read-only 22. Power Save Mode Exit ( 直接关闭所有的IDEA项目,跟Close Project的区别是不会再回到欢迎界面,可使用快捷键Alt + F4一个个的关闭 )","categories":[{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/categories/编辑器/"},{"name":"快捷键","slug":"编辑器/快捷键","permalink":"https://blog.mariojd.cn/categories/编辑器/快捷键/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"JetBrains","slug":"JetBrains","permalink":"https://blog.mariojd.cn/tags/JetBrains/"}]},{"title":"Python 学习资源整理","slug":"Python 学习资源整理","date":"2018-03-05","updated":"2019-06-02","comments":true,"path":"python-learning-resources-collation.html","link":"","permalink":"https://blog.mariojd.cn/python-learning-resources-collation.html","excerpt":"","keywords":"","text":"  本文用于整理学习Python的在线资源,更多关于Python方面的知识和技术分享,请关注我的后续更新。 官方必备 Python官网 Pip Awesome Python(Python资源大全) Python 资源大全中文版 在线资源 菜鸟教程 慕课网 极客学院 伯乐在线 网易云课堂 实验楼 python中文学习大本营 技术博客 廖雪峰-python教程 Web开发 Django Flask Sanic Tornado webpy Bottle 网页爬虫 网页抓取 urllib Requests 网页解析 BeautifulSoup lxml PyQuery 自动化框架 Selenium 爬虫框架 Scrapy PySpider 数据科学 NumPy Pandas Matplotlib SciPy 图像识别 OpenCV 自然语言 NLTK 机器学习 Scikit-learn TensorFlow Keras 公众号 Python那些事 Python开发者 Python绿色通道 欢迎大家留言补充","categories":[{"name":"Python","slug":"Python","permalink":"https://blog.mariojd.cn/categories/Python/"}],"tags":[{"name":"Python","slug":"Python","permalink":"https://blog.mariojd.cn/tags/Python/"},{"name":"学习资源","slug":"学习资源","permalink":"https://blog.mariojd.cn/tags/学习资源/"}]},{"title":"Mycat(实践篇 - 基于 PostgreSQL 的水平切分、主从复制、读写分离)","slug":"Mycat(实践篇 - 基于 PostgreSQL 的水平切分、主从复制、读写分离)","date":"2018-03-04","updated":"2019-06-02","comments":true,"path":"mycat-practice-based-postgresql.html","link":"","permalink":"https://blog.mariojd.cn/mycat-practice-based-postgresql.html","excerpt":"","keywords":"","text":"写在前面  Mycat作为独立的数据库中间件,我们只需要进行相关的配置,就可以非常方便的帮我们实现水平切分、垂直切分、读写分离等功能,但PostgreSQL的主从复制需要我们通过其它方式实现。这里假设我们已经搭建好相关的环境,下面就开始我们的实践吧! 准备环境 PostgreSQL(Version : 10.1)主从环境搭建 对应数据库建立(以下例子中使用的都是默认存在的postgres数据库,可以不用额外添加) 配置server.xml<user name=\"postgresmycat\"> <property name=\"password\">postgresmycat</property> <property name=\"schemas\">postgresmycats</property> </user> 配置schema.xml <schema name=\"postgresmycats\" checkSQLschema=\"false\" sqlMaxLimit=\"100\"> <table name=\"tb_user\" dataNode=\"mydn3,mydn4\" rule=\"user-mod-long\" /> <table name=\"tb_student\" dataNode=\"mydn3,mydn4\" rule=\"student-mod-long\" /> </schema> <dataNode name=\"mydn3\" dataHost=\"myhost3\" database=\"postgres\" /> <dataNode name=\"mydn4\" dataHost=\"myhost4\" database=\"postgres\" /><!-- 这里的dbDriver使用jdbc的方式来连接,用native方式似乎目前还不太兼容,试过了好像不可以 --> <dataHost name=\"myhost3\" maxCon=\"100\" minCon=\"10\" balance=\"3\" writeType=\"0\" dbType=\"postgresql\" dbDriver=\"jdbc\"> <heartbeat>select user</heartbeat><!-- 注意这里的心跳检测命令跟mysql的有点不同 --> <writeHost host=\"hostM3\" url=\"jdbc:postgresql://localhost:5432/postgres\" user=\"postgres\" password=\"xxx\"> <readHost host=\"hostS3\" url=\"jdbc:postgresql://localhost:5433/postgres\" user=\"postgres\" password=\"xxx\"/> </writeHost> </dataHost> <dataHost name=\"myhost4\" maxCon=\"100\" minCon=\"10\" balance=\"3\" writeType=\"0\" dbType=\"postgresql\" dbDriver=\"jdbc\"> <heartbeat>select user</heartbeat> <writeHost host=\"hostM4\" url=\"jdbc:postgresql://localhost:5434/postgres\" user=\"postgres\" password=\"xxx\" > <readHost host=\"hostS4\" url=\"jdbc:postgresql://localhost:5435/postgres\" user=\"postgres\" password=\"xxx\"/> </writeHost> </dataHost> dbDriver 属性  指定连接后端数据库使用的 Driver,目前可选的值有 native 和 jdbc。使用 native 的话,因为这个值执行的是二进制的 mysql 协议,所以可以使用 mysql 和 maridb。其他类型的数据库则需要使用 JDBC 驱动来支持 引述《Mycat权威指南》里面的原话: 从 1.6 版本开始支持 postgresql 的 native 原始协议。如果使用 JDBC 的话需要将符合 JDBC4 标准的驱动 JAR 包放到 MYCAT\\lib 目录下,并检查驱动 JAR 包中包括如下目录结构的文件:META-INF\\services\\java.sql.Driver。在这个文件内写上具体的 Driver 类名,例如:com.mysql.jdbc.Driver。   所以,具体的解决方案就是找一个postgresql的jar包,然后丢到mycat的lib目录下,不然就会出现启动失败或者连接不到postgre数据库的异常情况。本例中用到的jar包是:postgresql-42.1.4.jar 配置rule.xml <tableRule name=\"user-mod-long\"> <rule> <columns>id</columns> <algorithm>mod-long</algorithm> </rule> </tableRule> <tableRule name=\"student-mod-long\"> <rule> <columns>user_id</columns> <algorithm>mod-long</algorithm> </rule> </tableRule><function name=\"mod-long\" class=\"io.mycat.route.function.PartitionByMod\"> <property name=\"count\">2</property></function>   修改了配置文件后,别忘了重启Mycat,如果有异常出现,请通过查看logs目录下的日志文件进行排查。 项目搭建(SpringBoot + JPA) 准备:首次建表,设置application.yml中的spring.jpa.hibernate.ddl-auto属性为:create(JPA自动建表解决方案,使用update的话在连接mycat的时候会报找不到表的错误)。之后似乎必须更改为:none,否则使用其它属性都会报错(这里Mysql与PostgreSQL不同,似乎是一个未解决的bug,这也就意味着以后新增字段都要手动连上数据库进行添加了…) 添加application.yml(注意了,这里都是用连mysql的方式去配置,Mycat会在后端做好对其它数据库的连接): spring: jpa: show-sql: true hibernate: ddl-auto: update naming: strategy: org.hibernate.cfg.ImprovedNamingStrategy properties: hibernate: dialect: org.hibernate.dialect.MySQL5Dialect datasource: url: jdbc:mysql://localhost:8066/postgresmycats?characterEncoding=UTF-8&useSSL=false&autoReconnect=true&rewriteBatchedStatements=true username: postgresmycat password: postgresmycat 添加User Entity @Entity@Table(name = \"tb_user\")@Datapublic class User { @Id private Long id; private String name; private Integer gender;} 添加Student Entity @Entity@Table(name = \"tb_student\")@Datapublic class Student { @Id private Long id; private String name; @Column(unique = true) private Long userId;} 添加UserDao public interface UserDao extends JpaRepository<User, Long> { Page<User> findByNameLike(String name, Pageable pageable);} 添加StudentDao public interface StudentDao extends JpaRepository<Student, Long> { Page<User> findByNameLike(String name, Pageable pageable);} 项目测试 测试添加 @Testpublic void testAdd() { for (long i = 0; i < 30; i++) { User user = new User(); user.setId(i); user.setName(\"李四\" + i); user.setGender(i % 2 == 0 ? 1 : 0); userDao.save(user); Student student = new Student(); student.setId(System.currentTimeMillis() + i); student.setName(\"李四学生\" + i); student.setUserId(i); studentDao.save(student); }} 测试结果:数据按id取模的方式划分到了两个数据库中,同时从库同步了主库的数据 测试模糊查询+分页 @Testpublic void testFind() { Pageable pageable = new PageRequest(0, 10, Sort.Direction.ASC, \"id\"); List<User> userList = userDao.findByNameLike(\"%李四1%\", pageable).getContent(); userList.forEach(System.out::println); Pageable pageable2 = new PageRequest(0, 10, Sort.Direction.ASC, \"userId\"); List<Student> studentList = studentDao.findByNameLike(\"%李四学生2%\", pageable2).getContent(); studentList.forEach(System.out::println);} 测试结果:按照模糊匹配及id升序的方式输出结果 测试结果:读操作都走了从库 删除及修改请自行测试 参考链接 Mycat官网Mycat从零开始Mycat权威指南GitHub:Mycat-ServerWiki:Mycat-ServerIssues:Mycat-Servermysql中间件研究(Atlas,Cobar,TDDL)mysql中间件研究(Atlas,Cobar,TDDL,Mycat,Heisenberg,Oceanus,Vitess,OneProxy)","categories":[{"name":"数据库中间件","slug":"数据库中间件","permalink":"https://blog.mariojd.cn/categories/数据库中间件/"},{"name":"Mycat","slug":"数据库中间件/Mycat","permalink":"https://blog.mariojd.cn/categories/数据库中间件/Mycat/"}],"tags":[{"name":"postgresql","slug":"postgresql","permalink":"https://blog.mariojd.cn/tags/postgresql/"},{"name":"Mycat","slug":"Mycat","permalink":"https://blog.mariojd.cn/tags/Mycat/"},{"name":"数据库中间件","slug":"数据库中间件","permalink":"https://blog.mariojd.cn/tags/数据库中间件/"},{"name":"实践篇","slug":"实践篇","permalink":"https://blog.mariojd.cn/tags/实践篇/"},{"name":"主从复制","slug":"主从复制","permalink":"https://blog.mariojd.cn/tags/主从复制/"},{"name":"读写分离","slug":"读写分离","permalink":"https://blog.mariojd.cn/tags/读写分离/"}]},{"title":"Mycat(实践篇 - 基于 MySQL 的水平切分、主从复制、读写分离)","slug":"Mycat(实践篇 - 基于 MySQL 的水平切分、主从复制、读写分离)","date":"2018-03-03","updated":"2019-06-02","comments":true,"path":"mycat-practice-based-mysql.html","link":"","permalink":"https://blog.mariojd.cn/mycat-practice-based-mysql.html","excerpt":"","keywords":"","text":"写在前面  Mycat作为独立的数据库中间件,我们只需要进行相关的配置,就可以非常方便的帮我们实现水平切分、垂直切分、读写分离等功能,但Mysql的主从复制需要我们通过其它方式实现。这里假设我们已经搭建好相关的环境,下面就开始我们的实践吧! 准备环境 Mysql(Version : 5.7)主从环境搭建 对应数据库建立(以下例子中要建的数据库是:master1mycat 和 master2mycat) 配置server.xml<user name=\"mysqlmycat\"> <property name=\"password\">mysqlmycat</property> <property name=\"schemas\">mysqlmycats</property></user> 配置schema.xml <schema name=\"mysqlmycats\" checkSQLschema=\"false\" sqlMaxLimit=\"100\"> <table name=\"tb_user\" dataNode=\"mydn1,mydn2\" rule=\"user-mod-long\" /> <table name=\"tb_student\" dataNode=\"mydn1,mydn2\" rule=\"student-mod-long\" /> </schema><dataNode name=\"mydn1\" dataHost=\"myhost1\" database=\"master1mycat\" /> <dataNode name=\"mydn2\" dataHost=\"myhost2\" database=\"master2mycat\" /> <dataHost name=\"myhost1\" maxCon=\"100\" minCon=\"10\" balance=\"3\" writeType=\"0\" dbType=\"mysql\" dbDriver=\"native\"> <heartbeat>select user()</heartbeat> <writeHost host=\"hostM1\" url=\"localhost:3306\" user=\"root\" password=\"xxx\"> <readHost host=\"hostS1\" url=\"localhost:3307\" user=\"root\" password=\"xxx\"/> </writeHost> </dataHost> <dataHost name=\"myhost2\" maxCon=\"100\" minCon=\"10\" balance=\"3\" writeType=\"0\" dbType=\"mysql\" dbDriver=\"native\"> <heartbeat>select user()</heartbeat> <writeHost host=\"hostM2\" url=\"localhost:3308\" user=\"root\" password=\"xxx\" > <readHost host=\"hostS2\" url=\"localhost:3309\" user=\"root\" password=\"xxx\"/> </writeHost> </dataHost> 配置rule.xml <tableRule name=\"user-mod-long\"> <rule> <columns>id</columns> <algorithm>mod-long</algorithm> </rule> </tableRule> <tableRule name=\"student-mod-long\"> <rule> <columns>user_id</columns> <algorithm>mod-long</algorithm> </rule> </tableRule><function name=\"mod-long\" class=\"io.mycat.route.function.PartitionByMod\"> <property name=\"count\">2</property></function>   修改了配置文件后,别忘了重启Mycat,如果有异常出现,请通过查看logs目录下的日志文件进行排查。 项目搭建(SpringBoot + JPA) 准备:首次建表,设置application.yml中的spring.jpa.hibernate.ddl-auto属性为:create(JPA自动建表解决方案,使用update的话在连接mycat的时候会报找不到表的错误)。为保证数据不被丢失,在建表之后可以更改为:update 添加application.yml: spring: jpa: show-sql: true hibernate: ddl-auto: update naming: strategy: org.hibernate.cfg.ImprovedNamingStrategy properties: hibernate: dialect: org.hibernate.dialect.MySQL5Dialect datasource: url: jdbc:mysql://localhost:8066/mysqlmycats?characterEncoding=UTF-8&useSSL=false&autoReconnect=true&rewriteBatchedStatements=true username: mysqlmycat password: mysqlmycat 添加User Entity @Entity@Table(name = \"tb_user\")@Datapublic class User { @Id private Long id; private String name; private Integer gender;} 添加Student Entity @Entity@Table(name = \"tb_student\")@Datapublic class Student { @Id private Long id; private String name; @Column(unique = true) private Long userId;} 添加UserDao public interface UserDao extends JpaRepository<User, Long> { Page<User> findByNameLike(String name, Pageable pageable);} 添加StudentDao public interface StudentDao extends JpaRepository<Student, Long> { Page<User> findByNameLike(String name, Pageable pageable);} 项目测试 测试添加 @Testpublic void testAdd() { for (long i = 0; i < 30; i++) { User user = new User(); user.setId(i); user.setName(\"张三\" + i); user.setGender(i % 2 == 0 ? 0 : 1); userDao.save(user); Student student = new Student(); student.setId(System.currentTimeMillis() + i); student.setName(\"张三学生\" + i); student.setUserId(i); studentDao.save(student); }} 测试结果:数据按id取模的方式划分到了两个数据库中,同时从库同步了主库的数据 测试模糊查询+分页 @Testpublic void testFind() { Pageable pageable = new PageRequest(0, 10, Sort.Direction.DESC, \"id\"); List<User> userList = userDao.findByNameLike(\"%张三2%\", pageable).getContent(); userList.forEach(System.out::println); Pageable pageable2 = new PageRequest(0, 10, Sort.Direction.DESC, \"userId\"); List<Student> studentList = studentDao.findByNameLike(\"%张三学生1%\", pageable2).getContent(); studentList.forEach(System.out::println);} 测试结果:按照模糊匹配及id降序的方式输出结果 测试结果:读操作都走了从库 删除及修改请自行测试 参考链接 Mycat官网Mycat从零开始Mycat权威指南GitHub:Mycat-ServerWiki:Mycat-ServerIssues:Mycat-Servermysql中间件研究(Atlas,Cobar,TDDL)mysql中间件研究(Atlas,Cobar,TDDL,Mycat,Heisenberg,Oceanus,Vitess,OneProxy)","categories":[{"name":"数据库中间件","slug":"数据库中间件","permalink":"https://blog.mariojd.cn/categories/数据库中间件/"},{"name":"Mycat","slug":"数据库中间件/Mycat","permalink":"https://blog.mariojd.cn/categories/数据库中间件/Mycat/"}],"tags":[{"name":"mysql","slug":"mysql","permalink":"https://blog.mariojd.cn/tags/mysql/"},{"name":"Mycat","slug":"Mycat","permalink":"https://blog.mariojd.cn/tags/Mycat/"},{"name":"数据库中间件","slug":"数据库中间件","permalink":"https://blog.mariojd.cn/tags/数据库中间件/"},{"name":"实践篇","slug":"实践篇","permalink":"https://blog.mariojd.cn/tags/实践篇/"},{"name":"主从复制","slug":"主从复制","permalink":"https://blog.mariojd.cn/tags/主从复制/"},{"name":"读写分离","slug":"读写分离","permalink":"https://blog.mariojd.cn/tags/读写分离/"}]},{"title":"Mycat(配置篇)","slug":"Mycat(配置篇)","date":"2018-03-02","updated":"2018-09-29","comments":true,"path":"mycat-configuration.html","link":"","permalink":"https://blog.mariojd.cn/mycat-configuration.html","excerpt":"","keywords":"","text":"Mycat目录说明 bin:启动目录 conf:配置文件目录 server.xml:是Mycat服务器参数调整和用户授权的配置文件 schema.xml:是逻辑库定义和表以及分片定义的配置文件 rule.xml: 是分片规则的配置文件,分片规则的具体一些参数信息单独存放为文件,也在这个目录下,配置文件修改需要重启MyCAT log4j.xml: 日志存放在logs/log中,每天一个文件,日志的配置是在conf/log4j.xml中,根据自己的需要可以调整输出级别为debug,debug级别下,会输出更多的信息,方便排查问题 autopartition-long.txt,partition-hash-int.txt,sequence_conf.properties, sequence_db_conf.properties 分片相关的id分片规则配置文件 lib:jar包目录 logs :日志目录 tmlogs:临时日志目录 配置文件目录说明图: 3大配置文件说明server.xml  包含了Mycat需要的系统配置信息,用户配置信息以及逻辑库配置信息,源代码中的映射类为:SystemConfig.class 添加如下配置:相当于建立了一个叫做mycat用户,对应密码为mycat,该用户管理了mycats这个逻辑库。当然了,也可以为用户添加管理多个逻辑库,以,(英文逗号)分隔开即可<user name=\"mycat\"> <property name=\"password\">mycat</property> <property name=\"schemas\">mycats</property><!--schemas:逻辑库名称,具体配置在scheme.xml中--></user> schema.xml  可以说是最重要的配置文件,管理着 MyCat 的逻辑库、表、分片规则、DataNode 以及 DataSource schema是实际逻辑库的配置,多个schema代表多个逻辑库 dataNode是逻辑库对应的分片,如果配置多个分片则需要添加多个dataNode即可 dataHost是实际的物理库配置,可以根据业务需要配置多主、主从等其他配置,多个dataHost代表分片对应的物理库地址,下面的writeHost、readHost代表该分片是否配置多写,主从,读写分离等高级特性 添加如下配置:水平切分,数据按Id取模均匀划分到两个数据库中<schema name=\"mycats\" checkSQLschema=\"false\" sqlMaxLimit=\"100\"> <!-- 逻辑表配置 --> <table name=\"tb_user\" dataNode=\"dn1,dn2\" rule=\"mod-long\" /><!--name:实际物理库的数据表名;dataNode:表对应的分片;rule:分片规则名称,具体配置在rule.xml中--></schema><dataNode name=\"dn1\" dataHost=\"host1\" database=\"mycat1\" /><!--name:分片名称;database:实际物理库的数据库名--><dataNode name=\"dn2\" dataHost=\"host1\" database=\"mycat2\" /><dataHost name=\"host1\" maxCon=\"100\" minCon=\"10\" balance=\"0\" writeType=\"0\" dbType=\"mysql\" dbDriver=\"native\"> <heartbeat>select user()</heartbeat><!--mysql心跳检测命令--> <writeHost host=\"hostM1\" url=\"localhost:3306\" user=\"root\" password=\"xxx\" /><!--实际物理库的配置信息--></dataHost> rule.xml  定义了表拆分所涉及到的规则定义。根据业务可以灵活的对表使用不同的分片算法(目前已实现十余种不同的分片规则,对应所在源码包为:io.mycat.route.function),或者对表使用相同的算法但具体的参数不同。 添加如下配置:水平切分,数据按Id取模均匀划分到两个数据库中<tableRule name=\"mod-long\"> <!-- 对应表的分片规则 --> <rule> <columns>id</columns><!-- 对应数据表要取模的字段名称 --> <algorithm>mod-long</algorithm><!-- 对应function的名称 --> </rule></tableRule><function name=\"mod-long\" class=\"io.mycat.route.function.PartitionByMod\"><!-- name:对应tableRule的名称;class:切分规则对应的切分类 --> <!-- scheme.xml中有多少个dataNode就改成多少个 --> <property name=\"count\">2</property></function> 代码测试(SpringBoot + JPA) 准备:在对应的数据库中建好相关的表(下一篇文章将给出JPA自动建表解决方案) DROP TABLE IF EXISTS `tb_user`;CREATE TABLE `tb_user` ( `id` bigint(20) NOT NULL, `name` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 添加application.yml: spring: jpa: show-sql: true hibernate: ddl-auto: update naming: strategy: org.hibernate.cfg.ImprovedNamingStrategy properties: hibernate: dialect: org.hibernate.dialect.MySQL5Dialect datasource: url: jdbc:mysql://localhost:8066/mycats?characterEncoding=UTF-8&useSSL=false&autoReconnect=true&rewriteBatchedStatements=true username: mycat password: mycat 添加User Entity @Entity@Table(name = \"tb_user\")@Datapublic class User { @Id private Long id; private String name;} 添加UserDao public interface UserDao extends JpaRepository<User, Long> { Page<User> findByNameLike(String name, Pageable pageable);} 测试添加@Testpublic void testAdd() { for (long i = 0; i < 50; i++) { User user = new User(); user.setId(i); user.setName(\"ls\" + i); userDao.save(user); }} 测试结果:数据按id取模的方式划分到了两个数据库中 测试模糊查询+分页@Testpublic void testFind() { Pageable pageable = new PageRequest(0, 10, Sort.Direction.ASC, \"id\"); List<User> userList = userDao.findByNameLike(\"%ls1%\", pageable).getContent(); userList.forEach(System.out::println);} 测试结果:按照模糊匹配及id升序的方式输出结果 删除及修改请自行测试 参考链接 Mycat官网Mycat从零开始Mycat权威指南GitHub:Mycat-ServerWiki:Mycat-ServerIssues:Mycat-Servermysql中间件研究(Atlas,Cobar,TDDL)mysql中间件研究(Atlas,Cobar,TDDL,Mycat,Heisenberg,Oceanus,Vitess,OneProxy)","categories":[{"name":"数据库中间件","slug":"数据库中间件","permalink":"https://blog.mariojd.cn/categories/数据库中间件/"},{"name":"Mycat","slug":"数据库中间件/Mycat","permalink":"https://blog.mariojd.cn/categories/数据库中间件/Mycat/"}],"tags":[{"name":"Mycat","slug":"Mycat","permalink":"https://blog.mariojd.cn/tags/Mycat/"},{"name":"数据库中间件","slug":"数据库中间件","permalink":"https://blog.mariojd.cn/tags/数据库中间件/"},{"name":"配置篇","slug":"配置篇","permalink":"https://blog.mariojd.cn/tags/配置篇/"}]},{"title":"Mycat(入门篇)","slug":"Mycat(入门篇)","date":"2018-03-01","updated":"2018-08-23","comments":true,"path":"mycat-introduction.html","link":"","permalink":"https://blog.mariojd.cn/mycat-introduction.html","excerpt":"","keywords":"","text":"Mycat是什么Mycat是一款基于阿里开源产品Cobar而研发的开源数据库分库分表中间件(基于Java语言开发)。官网所言:Mycat国内最活跃的、性能最好的开源数据库中间件! 一个彻底开源的,面向企业应用开发的大数据库集群 支持事务、ACID、可以替代MySQL的加强版数据库 一个可以视为MySQL集群的企业级数据库,用来替代昂贵的Oracle集群 一个融合内存缓存技术、NoSQL技术、HDFS大数据的新型SQL Server 结合传统数据库和新型分布式数据仓库的新一代企业级数据库产品 一个新颖的数据库中间件产品 Mycat关键特性 支持SQL92标准 支持MySQL、Oracle、DB2、SQL Server、PostgreSQL等DB的常见SQL语法 遵守Mysql原生协议,跨语言,跨平台,跨数据库的通用中间件代理 基于心跳的自动故障切换,支持读写分离,支持MySQL主从,以及galera cluster集群 支持Galera for MySQL集群,Percona Cluster或者MariaDB cluster 基于Nio实现,有效管理线程,解决高并发问题 支持数据的多片自动路由与聚合,支持sum,count,max等常用的聚合函数,支持跨库分页 支持单库内部任意join,支持跨库2表join,甚至基于caltlet的多表join 支持通过全局表,ER关系的分片策略,实现了高效的多表join查询 支持多租户方案 支持分布式事务(弱xa) 支持XA分布式事务(1.6.5) 支持全局序列号,解决分布式下的主键生成问题 分片规则丰富,插件化开发,易于扩展 强大的web,命令行监控 支持前端作为MySQL通用代理,后端JDBC方式支持MySQL、PostgreSQL、Oracle、DB2、SQLServer、MongoDB、巨杉 支持密码加密 支持服务降级 支持IP白名单 支持SQL黑名单、sql注入攻击拦截 支持prepare预编译指令(1.6) 支持非堆内存(Direct Memory)聚合计算(1.6) 支持PostgreSQL的native协议(1.6) 支持mysql和oracle存储过程,out参数、多结果集返回(1.6) 支持zookeeper协调主从切换、zk序列、配置zk化(1.6) 支持库内分表(1.6) 集群基于ZooKeeper管理,在线升级,扩容,智能优化,大数据处理(2.0开发版) 安装与使用 下载(目前最新发行版是1.6.5):http://dl.mycat.io/ 安装:解压即可 配置:Mycat是基于Java开发的,确保安装好了Java环境,可命令行输入:java -version 进行测试。Linux下还需配置Mycat的解压目录:vim /etc/profile,配置完成后使用:source /etc/profile: export JAVA_HOME=xxxexport MYCAT_HOME=xxx 运行(Linux): ./mycat start 启动 ./mycat stop 停止 ./mycat console 前台运行 ./mycat install 添加到系统自动启动(暂未实现) ./mycat remove 取消随系统自动启动(暂未实现) ./mycat restart 重启服务 ./mycat pause 暂停 ./mycat status 查看启动状态 运行(Windows):双击bin/tartup_nowrap.bat,如果出现闪退,可在cmd命令行运行,并查看出错原因 内存配置:启动前,一般需要修改JVM配置参数,打开conf/wrapper.conf文件,可根据本机配置情况修改为512M或其它值 # Java Additional Parameters#wrapper.java.additional.1=wrapper.java.additional.1=-DMYCAT_HOME=.wrapper.java.additional.2=-serverwrapper.java.additional.3=-XX:MaxPermSize=64Mwrapper.java.additional.4=-XX:+AggressiveOptswrapper.java.additional.5=-XX:MaxDirectMemorySize=2Gwrapper.java.additional.6=-Dcom.sun.management.jmxremotewrapper.java.additional.7=-Dcom.sun.management.jmxremote.port=1984wrapper.java.additional.8=-Dcom.sun.management.jmxremote.authenticate=falsewrapper.java.additional.9=-Dcom.sun.management.jmxremote.ssl=falsewrapper.java.additional.10=-Xmx4Gwrapper.java.additional.11=-Xms1G 连接测试: 测试mycat与测试mysql完全一致,mysql怎么连接,mycat就怎么连接 命令行:mysql -uroot -proot -P8066 -h127.0.0.1(其中,user和password可在conf/server.xml配置查找,8066是默认的服务端口,也可以在conf/server.xml中配置修改) 客户端:1.3和1.4版本目前部分工具无法连接,会提示database not selected,建议使用高版本的Mycat,1.5版本已经修复了部分客户端工具的连接 常见分库分表产品对比 分库分表产品 MyCat Sharding-JDBC Cobar Cobar-client TDDL 分库 有 有 有 有 未开源 分表 有 有 无 无 未开源 中间层 是 否 是 否 否 ORM支持 任意 任意 任意 仅MyBatis 任意 数据库支持 任意 任意 仅MySQL 任意 任意 社区情况 活跃 活跃 停更 未知 未知 相关链接 Mycat官网Mycat从零开始Mycat权威指南GitHub:Mycat-ServerWiki:Mycat-ServerIssues:Mycat-Servermysql中间件研究(Atlas,Cobar,TDDL)mysql中间件研究(Atlas,Cobar,TDDL,Mycat,Heisenberg,Oceanus,Vitess,OneProxy)","categories":[{"name":"数据库中间件","slug":"数据库中间件","permalink":"https://blog.mariojd.cn/categories/数据库中间件/"},{"name":"Mycat","slug":"数据库中间件/Mycat","permalink":"https://blog.mariojd.cn/categories/数据库中间件/Mycat/"}],"tags":[{"name":"Mycat","slug":"Mycat","permalink":"https://blog.mariojd.cn/tags/Mycat/"},{"name":"数据库中间件","slug":"数据库中间件","permalink":"https://blog.mariojd.cn/tags/数据库中间件/"},{"name":"入门篇","slug":"入门篇","permalink":"https://blog.mariojd.cn/tags/入门篇/"}]},{"title":"面向切面的 Spring","slug":"面向切面的 Spring","date":"2018-02-11","updated":"2019-06-02","comments":true,"path":"oriented-face-in-spring.html","link":"","permalink":"https://blog.mariojd.cn/oriented-face-in-spring.html","excerpt":"","keywords":"","text":"写在前面  本文是博主在看完面向切面的Spring(《Spring实战》第4章)后的一些实践笔记。  为什么要用AOP呢?作者在书中也明确提到了,使用AOP,可以让代码逻辑更多的去关注自己本身的业务,而不用混杂和关注一些其它的东西。包括:安全,缓存,事务,日志等等。 名词概念 通知(Advice)   定义了切面做什么和什么时候去做。简单点来说,就是AOP执行时会调用的方法,通知除了定义切面要完成的工作(What),还会定位什么时候(When)去履行这项工作,是在方法调用前,还是调用之后,还是前后都是,还是抛出异常时 在切面定义中,一共有以下五种通知类型 类型 作用 Before 某方法调用之前发出通知 After 某方法完成之后发出通知,不考虑方法运行的结果 AfterReturning 将通知放置在被通知的方法成功执行之后 AfterThrowing 将通知放置在被通知的方法抛出异常之后 Around 通知包裹在被通知的方法的周围,在方法调用之前和之后发出(环绕通知 = 前置 + 目标方法执行 + 后置通知) 切点,也叫切入点(Pointcut)   上面说的连接点的基础上,来定义切入点,你的一个类里,有15个方法,那就有十几个连接点了对吧,但是你并不想在所有方法附件都使用通知(使用叫织入,下面再说),你只是想让其中几个,在调用这几个方法之前、之后或者抛出异常时干点什么,那么就用切入点来定义这几个方法,让切点来筛选连接点,选中那几个你想要的方法 连接点,也叫参加点(JoinPoint)   连接点是切面在应用程序执行过程中插入的地方,可能是方法调用(前、后)的时候,也可能是异常抛出的时候。连接点如果可以说是切点的全集,那么切点就是连接点的子集 切面(Aspect)   切面其实就是通知和切点的结合。通知说明了干什么和什么时候干(通过方法上使用@Before、@After等就能知道),则切点说明了在哪干(指定到底是哪个方法),这就组成了一个完整的切面定义 Spring对AOP的支持 Spring建议在Java中编写AOP,虽然用XML也可以实现 Spring通过使用代理类,在运行阶段将切面编织进bean中 Spring只支持方法级别的连接点,不像AspectJ还可以通过构造器或属性注入 切点表达式  切点表达式算是一些比较概念性的知识,下面截了两个图供大家参考参考   看得头晕了吧,不过好在只有execution()是用来执行匹配的,剩下的都是为了限制或定制连接点要匹配的位置  以下是execution()定义的格式(其中,带?号的为可选,否则必须给出) : execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)   还是举个真实栗子模仿一下吧 execution(* com.example.aspectj.UserDao.updateName(..)) execution:用于定义什么方法执行时会被触发,这里是指com.example.aspectj包下的UserDao接口中的updateName方法执行时触发 * :忽略方法返回值类型 (..) :匹配任意参数 实战测试(SpringBoot + JPA) Create Entity @Table(name = \"tb_user\")@Entity@Datapublic class User { @Id @GeneratedValue private Integer id; private String name;} Create Dao public interface UserDao extends JpaRepository<User, Integer> { @Modifying @Transactional @Query(\"update User u set u.name = ?1 where u.id = ?2\") int updateName(String name, int id);} Create Service @Servicepublic class UserService { @Resource private UserDao userDao; @Transactional public void save(User user) { userDao.save(user); } public void update(String name, int id) { userDao.updateName(name, id); }} 第一种风格的切面 Create Aspect(使用了@Before、@After、@AfterReturning和@AfterThrowing这四个注解) @Aspectpublic class UserAspectjOne { @Resource private UserService userService; @Before(\"execution(* com.example.aspectj.UserDao.updateName(..))\") public void before() { System.out.println(\"1.------------before()\"); } @After(\"execution(* com.example.aspectj.UserDao.updateName(..))\") public void after() { System.out.println(\"1.------------after()\"); } @AfterReturning(\"execution(* com.example.aspectj.UserDao.updateName(..))\") public void afterReturning() { System.out.println(\"1.------------afterReturning()\"); User user = new User(); user.setName(\"afterReturning1\"); userService.save(user); } @AfterThrowing(\"execution(* com.example.aspectj.UserDao.updateName(..))\") public void afterThrowing() { System.out.println(\"1.------------afterThrowing()\"); User user = new User(); user.setName(\"afterThrowing1\"); userService.save(user); }} Create Configuration @Configuration// @EnableAspectJAutoProxy //实测可以不添加该注解,因为SpringBoot中已经默认开启了AOP功能public class AspectjConfiguration { @Bean public UserAspectjOne userAspectjOne() { return new UserAspectjOne(); }} Test updateName() with UserAspectjOne 6.1 先往数据库里添加一条数据@Test public void testAdd() { User user = new User(); user.setName(\"jared\"); userDao.save(user); } ![添加User][4] - 6.2 测试正常执行updateName() @Test public void testUpdateName() { userService.update(\"jared qiu\", 1); } - 6.3.1 打印结果 ![输出结果][5] - 6.3.2 数据库结果 ![数据库结果][6] - 6.4 测试非正常执行updateName(),只需要把UserDao类中updateName()上的@Modifying或者@Transactional注解去掉即可 @Test public void testUpdateName() { userService.update(\"error jared qiu\", 1); } - 6.5.1 打印结果 ![输出结果][7] - 6.5.2 数据库结果 ![数据库结果][8] 第二种风格的切面 Create Aspect(依旧使用了@Before、@After、@AfterReturning和@AfterThrowing这四个注解,但新增了@Pointcut注解,把切面的定义抽离了出来进行统一) @Aspectpublic class UserAspectjTwo { @Resource private UserService userService; @Pointcut(\"execution(* com.example.aspectj.UserDao.updateName(..))\") public void pointcut() { } @Before(\"pointcut()\") public void before() { System.out.println(\"2.------------before()\"); } @After(\"pointcut()\") public void after() { System.out.println(\"2.------------after()\"); } @AfterReturning(\"pointcut()\") public void afterReturning() { System.out.println(\"2.------------afterReturning()\"); User user = new User(); user.setName(\"afterReturning2\"); userService.save(user); } @AfterThrowing(\"pointcut()\") public void afterThrowing() { System.out.println(\"2.------------afterThrowing()\"); User user = new User(); user.setName(\"afterThrowing2\"); userService.save(user); }} Create Configuration @Configurationpublic class AspectjConfiguration { @Bean public UserAspectjTwo userAspectjTwo() { return new UserAspectjTwo(); }} Test updateName() with UserAspectjTwo 6.1 先往数据库里添加一条数据@Test public void testAdd() { User user = new User(); user.setName(\"jared\"); userDao.save(user); } ![添加User][9] - 6.2 测试正常执行updateName() @Test public void testUpdateName() { userService.update(\"jared qiu\", 1); } - 6.3.1 打印结果 ![输出结果][10] - 6.3.2 数据库结果 ![数据库结果][11] - 6.4 测试非正常执行updateName(),只需要把UserDao类中updateName()上的@Modifying或者@Transactional注解去掉即可 @Test public void testUpdateName() { userService.update(\"error jared qiu\", 1); } - 6.5.1 打印结果 ![输出结果][12] - 6.5.2 数据库结果 ![数据库结果][13] 第三种风格的切面 Create Aspect(使用了@Around这个环绕注解) @Aspectpublic class UserAspectjThree { @Resource private UserService userService; /** * 方法的返回值类型须与切面所在方法的返回值类型保持一致 */ @Around(\"execution(* com.example.aspectj.UserDao.updateName(..))\") public int around(ProceedingJoinPoint joinPoint) { try { System.out.println(\"3.------------before()\"); System.out.println(\"3.------------after()\"); joinPoint.proceed();//用于启动目标方法执行(必须) System.out.println(\"3.------------afterReturning()\"); User user = new User(); user.setName(\"afterReturning3\"); userService.save(user); } catch (Throwable e) { System.out.println(\"3.------------afterThrowing()\"); User user = new User(); user.setName(\"afterThrowing3\"); userService.save(user); } return 1; }} Create Configuration @Configurationpublic class AspectjConfiguration { @Bean public UserAspectjThree userAspectjThree() { return new UserAspectjThree(); }} Test updateName() with UserAspectjThree 6.1 先往数据库里添加一条数据@Test public void testAdd() { User user = new User(); user.setName(\"jared\"); userDao.save(user); } ![添加User][14] - 6.2 测试正常执行updateName() @Test public void testUpdateName() { userService.update(\"jared qiu\", 1); } - 6.3.1 打印结果 ![输出结果][15] - 6.3.2 数据库结果 ![数据库结果][16] - 6.4 测试非正常执行updateName(),只需要把UserDao类中updateName()上的@Modifying或者@Transactional注解去掉即可 @Test public void testUpdateName() { userService.update(\"error jared qiu\", 1); } - 6.5.1 打印结果 ![输出结果][17] - 6.5.2 数据库结果 ![数据库结果][18] 第四种风格的切面 Create Aspect(依旧使用了@Around这个环绕注解,但加入了@Pointcut注解和传递了参数) @Aspectpublic class UserAspectjFour { @Resource private UserService userService; @Pointcut(\"execution(* com.example.aspectj.UserDao.updateName(String,*)) && args(name,*)\") public void pointcut(String name) { } @Around(value = \"pointcut(name)\", argNames = \"joinPoint,name\") public int around(ProceedingJoinPoint joinPoint, String name) { try { System.out.println(\"4.------------before()\"); System.out.println(\"4.------------after()\"); Object proceed = joinPoint.proceed(); System.out.println(proceed); System.out.println(\"4.------------afterReturning()\"); User user = new User(); user.setName(\"afterReturning4\" + name); userService.save(user); } catch (Throwable e) { System.out.println(\"4.------------afterThrowing()\"); User user = new User(); user.setName(\"afterThrowing4\" + name); userService.save(user); } return 1; }} Create Configuration @Configurationpublic class AspectjConfiguration { @Bean public UserAspectjFour userAspectjFour() { return new UserAspectjFour(); }} Test updateName() with UserAspectjFour 6.1 先往数据库里添加一条数据@Test public void testAdd() { User user = new User(); user.setName(\"jared\"); userDao.save(user); } ![添加User][19] - 6.2 测试正常执行updateName() @Test public void testUpdateName() { userService.update(\"jared qiu\", 1); } - 6.3.1 打印结果 ![输出结果][20] - 6.3.2 数据库结果 ![数据库结果][21] - 6.4 测试非正常执行updateName(),只需要把UserDao类中updateName()上的@Modifying或者@Transactional注解去掉即可 @Test public void testUpdateName() { userService.update(\"error jared qiu\", 1); } - 6.5.1 打印结果 ![输出结果][22] - 6.5.2 数据库结果 ![数据库结果][23] 扩展@EnableAspectJAutoProxy 表示开启AOP代理自动配置,如果配@EnableAspectJAutoProxy表示使用cglib进行代理对象的生成;设置@EnableAspectJAutoProxy(exposeProxy=true)表示通过aop框架暴露该代理对象,使得aopContext能够直接访问 从@EnableAspectJAutoProxy的定义可以看出,它引入AspectJAutoProxyRegister.class对象,该对象是基于注解@EnableAspectJAutoProxy注册了一个AnnotationAwareAspectJAutoProxyCreator,通过调用AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry),注册了一个aop代理对象生成器 参考链接 AspectJSpring AOP系列Spring AOP中JoinPoint的表达式定义描述","categories":[{"name":"Spring","slug":"Spring","permalink":"https://blog.mariojd.cn/categories/Spring/"}],"tags":[{"name":"Spring","slug":"Spring","permalink":"https://blog.mariojd.cn/tags/Spring/"},{"name":"Spring in action","slug":"Spring-in-action","permalink":"https://blog.mariojd.cn/tags/Spring-in-action/"},{"name":"AcpectJ","slug":"AcpectJ","permalink":"https://blog.mariojd.cn/tags/AcpectJ/"},{"name":"Aop","slug":"Aop","permalink":"https://blog.mariojd.cn/tags/Aop/"},{"name":"读书笔记","slug":"读书笔记","permalink":"https://blog.mariojd.cn/tags/读书笔记/"}]},{"title":"IDEA 快捷键拆解系列(一)","slug":"IDEA 快捷键拆解系列(一)","date":"2018-02-10","updated":"2019-06-02","comments":true,"path":"idea-shortcut-key-disassembly-series-one.html","link":"","permalink":"https://blog.mariojd.cn/idea-shortcut-key-disassembly-series-one.html","excerpt":"","keywords":"","text":"  这是IDEA快捷键拆解系列的第一篇。   本系列从最顶部的导航栏,以及周围、中间区域的快捷键提示开始讲起。在此之前,请记住非常重要的一个快捷键: Alt + 下划线那个符号。举个栗子,左上角有++F++ile这么个导航项,所以对应的快捷键就是:Alt + F,其它任意有下划线的都是同样的操作,包括导航项展开的任意子项,只要选项中带有某一下划线的字符,我们就可以通过这种形式来进行快速定位。除此之外,我们还可以通过 ↑和 ↓来上下移动,然后通过Enter键来选择相应的操作。   在IDEA中,中间区域的快捷键是最明显的,所以应该也是最重要的,以下是对每一项的详细拆解。 中间提示区域快捷键 作用 快捷键 拆解 Search Everywhere Double Shift 全局搜索,按两下Shift弹出此界面,再按两下Shift可以搜索非当前项目的文件(如依赖的Jar包),右上角还可以设置展示的类型 Project View Alt + 1 Project面板的展开与折叠切换,此外,还可用于代码区快速跳转至项目区(返回使用ESC) Go to File Ctrl + Shift + N 搜素文件,按Ctrl+Shift+N弹出此界面,再按一下Ctrl+Shift+N可以搜索非当前项目的文件,右上角还可以设置搜索的文件类型 Recent Files Ctrl + E 按Ctrl+E可以弹出记录了最近操作的面板,一般左边对应的是整个IDEA界面周边的各个功能选项,右边则是你最近操作的文件列表。左右方向键用于左右跳转,上下方向键用于切换,Enter用于打开选择 Navigation Bar Alt + Home 跳转到项目的导航栏,也可以通过这种方式进行文件切换和打开 周边工具窗口快捷键 位置(面板) 快捷键 拆解 左边(Project) Alt + 1 快速展开(折叠)项目窗口 左边(Structure) Alt + 7 快速展开(折叠)结构窗口,一般用于查看类结构 左边(Favorites) Alt + 2 快速展开(折叠)书签窗口,一般用于查看书签 下边(Run) Alt + 4 项目正常运行的时候会有此窗口 下边(Debug) Alt + 5 项目Debug运行的时候会有此窗口 下边(TODO) Alt + 6 快速展开(折叠)TODO窗口,一般用于查看待办事项 下边(Version Control) Alt + 9 快速展开(折叠)版本控制窗口 下边(Terminal) Alt + 12 快速展开(折叠)终端窗口","categories":[{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/categories/编辑器/"},{"name":"快捷键","slug":"编辑器/快捷键","permalink":"https://blog.mariojd.cn/categories/编辑器/快捷键/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"JetBrains","slug":"JetBrains","permalink":"https://blog.mariojd.cn/tags/JetBrains/"}]},{"title":"IDEA 快捷键拆解系列(前言)","slug":"IDEA 快捷键拆解系列(前言)","date":"2018-02-09","updated":"2019-06-02","comments":true,"path":"idea-shortcut-key-disassembly-series-preface.html","link":"","permalink":"https://blog.mariojd.cn/idea-shortcut-key-disassembly-series-preface.html","excerpt":"","keywords":"","text":"  在学校那会,前两年入门写代码用的IDE都是Eclipse,后来也不知道从哪里看到了IDEA,就这样开始慢慢入坑了。博主不是来吐槽的,但博主现在确实对Eclipse不太感冒了,只记得代码提示似乎不太智能,默认的主题是白色,更换的主题又总是不太搭配。就这样,使用了几个学期的Eclipse,最后博主对Eclipse快捷键也还不是很熟悉,感觉就是不(hao)太(bu)棒(shuang)。   接触IDEA到现在有一年多了。刚跳坑那会,博主就觉得这款编辑器打开蛮快的,智能提示很棒,但对编辑器的基本结构、快捷键等等都不太熟悉,现在出来实(gong)习(zuo),才慢慢重视起快捷键的培养。正所谓,花点时间去了解和学习好的东西都是非常值得的,特别是对于程序员这种职业来说,提高速度就意味着提高了生(zao)产(xia)率(ban);提高了生产率,就有了更多的时间去展开新的学习。熟练使用快捷键的好处不只是节省时间,更是将大脑从重复机械的劳动中解放出来,让时间能够关注到更为重要的部分。日常生活中,大到操作系统,小到浏览器,都有各种各样的快捷键,这也是博主为什么会写这么一个快捷键系列的原因。   JetBrains真是一家很棒的公司,官网的Title上写着:Developer Tools for Professionals and Teams。在产品一栏中,有很多优秀的开发者工具。博主希望通过这一系列的教程,能够让大家快速的上手IDEA快捷键,那么以后我们就可以非常轻松的使用像PyCharm、WebStrom、GoLand等等这一系列其他语言开发者喜欢的编辑器。正因为它们都出自同一家公司,因此很多快捷键都是一通百通,特别好上手。   如果说还有什么需要犹豫,那么产品付费可能是阻挡代码狗进步的唯一理由了。不过好在JetBrains提供了免费的社区版,相较与旗舰版,虽然少了一些功能,但对于学生或者入门级开发者而言,社区版的这些功能基本是够用的,如果确实想使用旗舰版,大天朝当然有很多的鬼点子,可以自己去了解一下,有能力的还是支持一下正版吧。   如果是第一次安装IDEA,博主强烈建议,先下载JetBrains的Toolbox,以后我们通过这款工具来下载、安装甚至升级编辑器都会非常方便。   不积跬步,无以至千里。要想成为优秀程序员,光有一腔热血是远远不够的,要持之以恒,要坚持不懈的学习。废话不多说,装好IDEA,一起来拆解快捷键吧!","categories":[{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/categories/编辑器/"},{"name":"个人感悟","slug":"编辑器/个人感悟","permalink":"https://blog.mariojd.cn/categories/编辑器/个人感悟/"}],"tags":[{"name":"IDEA","slug":"IDEA","permalink":"https://blog.mariojd.cn/tags/IDEA/"},{"name":"编辑器","slug":"编辑器","permalink":"https://blog.mariojd.cn/tags/编辑器/"},{"name":"Eclipse","slug":"Eclipse","permalink":"https://blog.mariojd.cn/tags/Eclipse/"},{"name":"个人感悟","slug":"个人感悟","permalink":"https://blog.mariojd.cn/tags/个人感悟/"}]},{"title":"Hexo 整合 GitHub Pages","slug":"Hexo 整合 GitHub Pages","date":"2018-02-08","updated":"2019-06-02","comments":true,"path":"hexo-integration-of-github-pages.html","link":"","permalink":"https://blog.mariojd.cn/hexo-integration-of-github-pages.html","excerpt":"","keywords":"","text":"什么是hexo   Hexo is a fast, simple and powerful blog framework. You write posts in Markdown (or other languages) and Hexo generates static files with a beautiful theme in seconds.   Hexo 是一个快速、简洁且高效的博客框架。Hexo 使用 Markdown(或其他渲染引擎)解析文章,在几秒内,即可利用靓丽的主题生成静态网页。 准备Node,Git环境 到Node官网下载安装包。Windows系统下的安装也很简单,下载最新版本的msi安装包,运行后一路点击Next就行,默认node会自动添加Path环境变量中,安装完后按Windows窗口键+R,cmd打开命令提示符界面,用node -v测试即可(PS:使用如下命令可更改为国内淘宝的NPM镜像源:npm install -g cnpm --registry=https://registry.npm.taobao.org, 这样以后就可以使用 cnpm 命令来安装第三方模块:cnpm install ) 安装Git,配置Git环境,具体操作过程这里就不在演示了,安装完了可以用命令git --version来测试 安装hexo cnpm install hexo -g,开始安装hexo hexo -v,用于检查hexo是否安装成功 创建空文件夹,在当前文件夹中输入hexo init进行初始化,最后输出的一句是:“Start blogging with Hexo!” cnpm install,安装相关依赖的组件 输入hexo g(等价于hexo generate),开始生成Hexo 输入hexo s(等价于hexo server),开启本地服务,默认为4000端口。端口占用的情况下,命令hexo server -p {port}可改变监听端口号 关联GitHub Page所在的仓库。在当前文件夹下,找到_config.yml文件,修改repository 新建文章,执行命令:hexo new post “my first blog”,可以在_posts文件夹下看到新创建的my first blog.md文件 安装扩展:cnpm install hexo-deployer-git --save 编辑好Markdown文章后,使用命令:hexo d -g,快速生成和部署远程仓库 部署成功后访问:https://{username}.github.io. 即可查看生成后的文章 关联域名 在source文件夹下新建名为CNAME的文件,输入域名,如:blog.mariojd.cn 使用命令hexo d -g部署到远程仓库 到域名提供商那里添加相应的域名解析 主机记录 记录类型 记录值 blog CNAME happyjared.github.io. 参考链接 Hexo官网Hexo文档Node官网Git官网","categories":[{"name":"开源项目","slug":"开源项目","permalink":"https://blog.mariojd.cn/categories/开源项目/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://blog.mariojd.cn/tags/Hexo/"},{"name":"GitHub Pages","slug":"GitHub-Pages","permalink":"https://blog.mariojd.cn/tags/GitHub-Pages/"},{"name":"个人博客","slug":"个人博客","permalink":"https://blog.mariojd.cn/tags/个人博客/"},{"name":"开源项目","slug":"开源项目","permalink":"https://blog.mariojd.cn/tags/开源项目/"}]},{"title":"GitHub Pages 介绍及搭建","slug":"GitHub Pages 介绍及搭建","date":"2018-02-07","updated":"2019-06-02","comments":true,"path":"introduction-and-construction-of-github-pages.html","link":"","permalink":"https://blog.mariojd.cn/introduction-and-construction-of-github-pages.html","excerpt":"","keywords":"","text":"GitHub Page  GitHub Page,一般多用于托管个人的静态网站,所以现在很多人也用来它来搭建私人博客,也算是省去了购买服务器、域名等等一系列复杂的操作。搭建博客网站有各种各样的方法,像懂php的可以用WordPress,懂Java的可以用Jpress等等。如果你想简单和简约,那么我强烈推荐你使用Github Page。在学习以下内容之前,请先准备好GitHub账号,如果没有请自行注册。 搭建流程 在GitHub中创建一个托管仓库,仓库的名字必须为:(or organization name).github.io。这里的username就是你的GitHub用户名,如果不知道,可以在GitHub中点击右上角的头像,在下拉里面有Signed in as XXX的信息,这就是你的账号名称。如果想修改你的账号名称,可以参考以下的步骤。 克隆当前仓库。 git clone 创建一个index.html到当前项目,可以尝试输入以下内容: <!DOCTYPE html><html> <body> <h1>Hello World</h1> <p>I'm hosted with GitHub Pages.</p> </body></html> 提交并推送到远程仓库。 git add index.html git commit -m “add index.html” 到此你就已经成功搭建完了GitHub Page。打开浏览器,并通过访问https://.github.io.来查看你的网站。 参考链接 GitHub PageGitHub HelpGitHub Pages Basics","categories":[{"name":"开源项目","slug":"开源项目","permalink":"https://blog.mariojd.cn/categories/开源项目/"}],"tags":[{"name":"GitHub Pages","slug":"GitHub-Pages","permalink":"https://blog.mariojd.cn/tags/GitHub-Pages/"},{"name":"个人博客","slug":"个人博客","permalink":"https://blog.mariojd.cn/tags/个人博客/"},{"name":"开源项目","slug":"开源项目","permalink":"https://blog.mariojd.cn/tags/开源项目/"}]},{"title":"Java 类库:Lombok","slug":"Java 类库:Lombok","date":"2017-10-22","updated":"2019-06-02","comments":true,"path":"java-class-library-about-lombok.html","link":"","permalink":"https://blog.mariojd.cn/java-class-library-about-lombok.html","excerpt":"","keywords":"","text":"前言  前阵子闲逛的时候,留意到了Lombok这个Java第三方库,后来亲自试用了一下,还真有一种相见恨晚的感觉,对于博主这样的懒人来说,这简直是太实用了。这不趁周末,赶紧把好东西写出来分享一下。 Lombok引述官网介绍:   Project Lombok is a java library that automatically plugs into your editor and build tools, spicing up your java.Never write another getter or equals method again. Early access to future java features such as val, and much more.  Lombok是一个java库,它可以自动插入您的编辑器和构建工具,为您的java提供帮助。再也不要写其他的getter或equals方法了。尽早访问诸如val之类的未来java特性。   Tip:单人项目开发中使用极佳,或多人协作开发中强制要求使用相同环境。因为需要配置的原因,但目前编辑器和编译器还没有那么智能和友善o(╥﹏╥)o… 效果 未使用Lombok前的POJO大概是这样的(为了方便,下面把Entity、DTO、VO之类的都统称为POJO) public class User { private Integer id; private String nickname; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } @Override public String toString() { return \"User{\" + \"nickname='\" + nickname + '\\'' + \", id=\" + id + '}'; }} 使用了Lombok之后的POJO大概是这样的 @Datapublic class User { private Integer id; private String nickname;} 搭建 添加依赖 <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional></dependency> 下载插件(IDEA) 配置支持   PS:IDEA中的Spring Initializr已集成Lombok,新建Spring Boot项目的时候勾选Lombok选择即可 说明图  下面先来看下Lombok中几个主要常用注解介绍说明图: 具体说明 ```(常用)&emsp;&emsp;在类上使用此注解,相当于为当前类的非final字段添加了getter()、setter()、toString()、equals()以及hashCode()方法,同时这也是一个多功能组合注解,组合了下面的@ToString,@EqualsAndHashCode,@Getter,@Setter和@RequiredArgsConstructor这五个注解![@Data][7]* ``` @Builder ```(常用)&emsp;&emsp;此注解需用在类上。Lombok底层通过构造者模式来转换当前类,在项目实际使用的时候,我们可以以参数链的形式组装该对象,或者是以全参构造器的形式来new一个当前对象。当组合使用@Data注解的时候(一般来说是没必要的),Lombok也不会再为当前对象生成getter()、setter()方法。使用链式构造一个对象会显得更加优雅,所以这个注解也是非常实用的![@Builder][8]* ``` @Builder.Default ```(常用)&emsp;&emsp;此注解用在字段上。当前类使用了@Builder进行构造且某些字段含有默认值的情况下,需要为这些字段添加此注解,不加此注解默认值在构造的时候是不生效的,需要进行手动设值![@Builder.Default][9]* ``` @Slf4j ```(常用)``` java private final Logger logger = LoggerFactory.getLogger(getClass());   上面这段代码很熟悉吧。为当前类添加@Slf4j后,我们就可以舍弃这段代码了,是不是简洁多了 var ```(常用)&emsp;&emsp;val将局部变量申明为final类型,而var则用于修饰变量但不是final类型``` java val noLombok = new HashSet<String>(); var noLombok2=new ArrayList<Integer>(); ////=> 以上这段代码相当于: final Set<String> useLombok = new HashSet<>(); List<Integer> useLombok2=new ArrayList<>(); ```(常用)&emsp;&emsp;该注解能够为方法,或构造函数的参数提供非空检查``` java public void notNull(@NonNull String arg) { } //=> 以上这段代码相当于: public static void notNull(String arg) { if (arg != null) { } else { throw new NullPointerException("arg"); } } ```(较常用)&emsp;&emsp;常用于资源释放``` java public void CleanUp() { try { @Cleanup Jedis jedis = redisService.getJedis(); } catch (Exception e) { e.printStackTrace(); } //=> 以上这段代码相当于: Jedis jedis = null; try { jedis = redisService.getJedis(); } catch (Exception e) { e.printStackTrace(); } finally { if (jedis != null) { try { jedis.close(); } catch (Exception e) { e.printStackTrace(); } } } } ```(较常用)&emsp;&emsp;类似于 Synchronized 关键字,但是可以隐藏同步锁``` javapublic class SynchronizedExample { private final Object readLock = new Object (); @Synchronized public static void test1() { System.out.println("test1"); } @Synchronized("readLock") public void test2() { System.out.println("test2"); }} //=> 以上这段代码相当于:public class SynchronizedExample { private static final Object $LOCK = new Object[0]; private final Object readLock = new Object (); @Synchronized public static void test1() { synchronized($LOCK) { System.out.println("test1"); } } @Synchronized("readLock") public void test2() { synchronized(readLock) { System.out.println("test2"); } }} @NoArgsConstructor, @RequiredArgsConstructor、@AllArgsConstructor (较常用)   分别对应可生成无参构造器,指定参数的构造器和包含全部字段的构造器。第1个和第3个注解还是比较挺实用的,如果要生成部分参数的构造器,博主是比较建议手动生成,感觉注解还不太好用了。(注意:当类中有final字段没有被初始化时,编译器就会报错,此时可用@NoArgsConstructor(force = true),然后就会为没有初始化的final字段设置默认值 0 / false / null。而对于具有约束的字段(如@NonNull字段),则不会生成检查或分配,因此要注意正确初始化final修饰的字段,否则这些约束都是无效的 @Getter、@Setter、@ToString、@EqualsAndHashCode (不常用)   这几个注解都很好理解,见其名知其意,只是要注意注解的使用位置。对于@Getter和@Setter,默认生成的方法是public的,如果要修改方法的修饰符,可以设置AccessLevel的值,如:@Getter(access = AccessLevel.PROTECTED) @Value,@SneakyThrows ...(目测不常用)   一些注解的具体用法还是参考Lombok官网吧 小结  具体到实现原理方面,在下面的参考链接中也有相关的介绍,感兴趣的还请自行研究。结合最近的使用情况,觉得最舒服的就是临时加字段的情况下,也不需要我们再补上getter()、setter()和toString()。一般来说,我们只需要在POJO上用@Data 一个注解就基本满足了,还有@NoArgsConstructor,@AllArgsConstructor这两个注解有时候也经常用得上。 参考链接 lombok官网lombok - 简书lombok的使用和原理Java开发神器Lombok的使用与原理","categories":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/categories/Java/"},{"name":"Lombok","slug":"Java/Lombok","permalink":"https://blog.mariojd.cn/categories/Java/Lombok/"}],"tags":[{"name":"Java","slug":"Java","permalink":"https://blog.mariojd.cn/tags/Java/"},{"name":"Lombok","slug":"Lombok","permalink":"https://blog.mariojd.cn/tags/Lombok/"},{"name":"Java类库","slug":"Java类库","permalink":"https://blog.mariojd.cn/tags/Java类库/"}]}]}