-
Notifications
You must be signed in to change notification settings - Fork 0
/
searchData.json
1 lines (1 loc) · 865 KB
/
searchData.json
1
[{"title":"PHP Migrating to 8.2","url":"/2024/08/16/php-migrating-to-82/","content":"\n- 实验环境:https://onlinephp.io/\n- Polyfill https://github.com/symfony/polyfill/tree/main/src\n\n## PHP8.2 \n\n> https://www.php.net/manual/en/migration81.php\n>\n> https://php.watch/versions/8.1\n> brings major new features such as Enums, Fibers, never return type, Intersection Types, readonly properties, and more, while ironing out some of its undesired legacy features by deprecating them.\n\n### New Features 8.1\n\n```php\n// 八进制\nvar_dump(0o14);\n// int(12)\n```\n\n```php\n// 可解包字符串为 key 的数组\n$arr1 = [1, 'a' => 'b'];\nvar_dump([...$arr1, 'c' => 'd']);\n```\n\n```php\n// 参数解包后的命名参数\nfunction foo($a, $b, $c) {\n var_dump($a, $b, $c);\n}\nfoo(...[1,2], c: 3);\n```\n\n```php\n// Enumerations 枚举\n// https://www.php.net/manual/en/language.enumerations.examples.php\n```\n\n```php\n// Fibers 纤程\n// https://www.php.net/manual/en/language.fibers.php\n```\n\n```php\n// Intersection Types 交叉类型\n// 由类型 T、U 和 V 组成的交集类型将写为 T&U&V\n```\n\n```php\n// Never type\n```\n\n```php\n// new in Initializers\n// 现在可以使用 new ClassName() 表达式作为参数、静态变量、全局常量初始值设定项和属性参数的默认值\n// 现在也可以将对象传递给 define()\n```\n\n```php\n// Readonly properties\n// Support for readonly has been added.\n```\n\n### New Functions 8.1\n\n```php\n// 如果数组的键由从 0 到 count($array)-1 的连续数字组成,则数组被认为是 list\narray_is_list([]); // true\narray_is_list(['apple', 2, 3]); // true\narray_is_list([0 => 'apple', 'orange']); // true\n\n// The array does not start at 0\narray_is_list([1 => 'apple', 'orange']); // false\n```\n\n-- EOF --\n","tags":["php"]},{"title":"MongoDB 高手课","url":"/2023/04/19/mongodb-course/","content":"\n## 04 特色及优势\n\n对象模型,快速响应业务变化:\n\n- 多形性:同一个集合中可以包含不同字段(类型)的文档对象。\n- 动态性:线上修改数据模式,修改是应用与数据库均无须下线。\n- 数据治理:支持使用JSONSchema 来规范数据模式。在保证模式灵活动态的前提下,提供数据治理能力。\n\n快速的开发:\n\n- 只存储在一个存储区读写。\n- 反范式、无关联的组织极大优化查询速度。\n- 程序 API 自然,开发速度快。\n\n原生的高可用:\n\n- Replica Set - 2 to 50 个成员,建议单数。\n- 自恢复。\n- 多中心容灾能力。\n- 滚动服务,最小化服务终端。\n\n横向扩展能力:\n\n- 需要的时候无缝扩展。\n- 应用全透明。\n- 多种数据分布策略。\n- 轻松支持 TB-PB 数量级。\n\n## 06 基本操作\n\n- [cloud.mongodb.com 云服务](https://cloud.mongodb.com/v2/)\n- [样本数据 | geektime-mongodb-course](https://github.com/tapdata/geektime-mongodb-course/blob/master/aggregation/dump.tar.gz)\n\n```bash\ntar -xvf dump.tar.gz\nmongorestore --uri=\"mongodb://root:[email protected]/?&authMechanism=SCRAM-SHA-1\"\n```\n\n```javascript\n// 插入\ndb.fruit.insertOne({name: \"apple\"})\ndb.fruit.insertMany([\n {name: \"apple\"},\n {name: \"pear\"},\n {name: \"orange\"},\n])\n// 查询\ndb.customers.find({username: \"fmiller\", name: \"Elizabeth Ray\"})\ndb.customers.find({username: /^f/})\ndb.customers.find({$or: [{username: /^f/}, {name: /^E/}]})\n```\n\n- `a <> 1` `{a: {$ne: 1}}`\n- `a > 1` `{a: {$gt: 1}}`\n- `a >= 1` `{a: {$gte: 1}}`\n- `a < 1` `{a: {$lt: 1}}`\n- `a <= 1` `{a: {$lte: 1}}`\n- `a=1 OR b=1` `{$or: [{a: 1}, {b: 1}]}`\n- `a IS NULL` `{a: {$exists: false}}`\n- `a IS (1,2,3)` `{a: {$in: [1, 2, 3]}}`\n\n```javascript\n// 同时满足\n{$eleMatch: {\"city\": \"Rome\", \"country\": \"USA\"}}\n```\n\n```javascript\n// 投影 projection\n// like select\ndb.customers.find({username: /^f/}, {name: 0, email: 0})\ndb.customers.find({username: /^f/}, {_id: 1, name: 1})\n```\n\n```javascript\n// remove\ndb.customers.remove({username: \"abrown\"})\n\n// update\ndb.customers.updateOne({username: \"fmiller\"}, {$set: {from: \"China\"}})\ndb.customers.updateMany({username: \"fmiller\"}, {$set: {from: \"China\"}})\n// $set $unset\n// $push $pushAll $pop 数组操作\n// $pull $pullAll 如果匹配,从数组中删除相应的对象\n// $addToSet 如果不存在则增加一个值到数组\n\n// drop\ndb.fruit.drop()\nshow collections\n\ndb\ndb.dropDatabase()\nshow dbs\n```\n\n## 08 聚合查询\n\nAggregation Framework\n\n- Pipeline\n- Stage\n\n```javascript\npipeline = [stage1, stage2, ...]\ndb.<collection>.aggregate(\n pipeline,\n { option }\n)\n```\n\n- $match 过滤\n- $project 投影\n- $sort 排序\n- $group 分组\n- $skip $limit 结果限制\n- $lookup 左外连接\n\n## 09 聚合查询实验\n\n```javascript\n// 计算总合计\ndb.orders.aggregate([\n {\n $group: {\n _id: null,\n total: {$sum: \"$total\"}\n }\n }\n])\n// {\n// \"_id\": null,\n// \"total\": NumberDecimal(\"44019609\")\n// }\n\n// 查询 2019 第一季度,已完成订单(completed)总金额(金额+运费)和订单总数\ndb.orders.aggregate([\n {\n $match: {\n status: \"completed\",\n orderDate: { $gte: ISODate(\"2019-01-01\"), $lt: ISODate(\"2019-04-01\") }\n }\n },\n {\n $group: {\n _id: null,\n total: { $sum: \"$total\" },\n shippingFee: { $sum: \"$shippingFee\" },\n count: { $sum: 1 }\n }\n },\n {\n $project: {\n grandTotal: { $add: [\"$total\", \"$shippingFee\"] },\n count: 1,\n _id: 0\n }\n }\n])\n// {\n// \"count\": 5875,\n// \"grandTotal\": NumberDecimal(\"2636376\")\n// }\n```\n\n## 10 复制集机制及原理\n\n由3个以上具有投票权的节点组成:\n\n- 1个主节点 PRIMARY:接收写入操作和选举时投票。\n- 两个或多个从节点 SECONDARY:复制主节点上的新数据和选举时投票。\n\n数据是如何复制的:\n\n- 当一个修改操作,无论是插入、更新或删除,到达主节点时它对数据的操作将被记录下来(经过些必要的转换),这些记录称为 oplog。\n- 从节点通过在主节点上打开一个 tailable 游标不断获取新进入主节点的 oplog,并在自己的数据上回放,以此保持跟主节点的数据一致。\n\n通过选举完成故障恢复:\n\n- 具有投票权的节点之间两两互相发送心跳。\n- 当5次心跳未收到时判断为节点失联。\n- 如果失联的是主节点,从节点会发起选举,选出新的主节点。\n- 如果失联的是从节点则不会产生新的选举。\n- 选举基于RAFT一致性算法实现,选举成功的必要条件是大多数投票节点存活。\n- 复制集中最多可以有50个节点,但具有投票权的节点最多7个。\n\n影响选举的因素:\n\n- 整个集群必须有大多数节点存活。\n- 被选举为主节点的节点必须:\n - 能够与多数节点建立连接\n - 具有较新的 oplog\n - 具有较高的优先级(如果有配置)\n\n复制集节点有以下常见的选配项:\n\n- 是否具有投票权(v 参数):有则参与投票。\n- 优先级(priority 参数):优先级越高的节点越优先成为主节点。优先级为0的节点无法成为主节点。\n- 隐藏(hidden 参数):复制数据,但对应用不可见。隐藏节点可以具有投票仅,但优先级必须为0。\n- 延迟(slaveDelay 参数):复制 n 秒之前的数据,保持与主节点的时间差。\n\n复制集注意事项:\n\n- 关于硬件:\n - 因为正常的复制集节点都有可能成为主节点,它们的地位是一样的,因此硬件配置上必须致;\n - 为了保证节点不会同时岩机,各节点使用的硬件必须具有独立性。\n- 关于软件:\n - 复制集各节点软件版本必须一致,以避免出现不可预知的问题。\n- 增加节点不会增加系统写性能!\n\n## 11 搭建 MongoDB 复制集\n\n```bash\nmkdir -p runtime/data_db{1,2,3} && \\\nmkdir -p runtime/data_configdb{1,2,3}\n\nhostname -f\n\nrs.status()\n```\n\n## 12 全家桶\n\n- [launch-manage | mongodb](https://www.mongodb.com/docs/launch-manage/)\n- [database-tools | mongodb](https://www.mongodb.com/docs/database-tools/)\n\n## 13 模型设计基础\n\n- Entity\n- Attribute\n- Relationship\n\n概念模型 CDM -> 逻辑模型 LDM -> 物理模型 PDM\n对象 -> 实体、属性、关系 -> 表结构、字段列表、主外建\n\n## 14 JSON 文档模型设计\n\n无模式的由来:可以省略无论建模的具体过程,物理模型可省。\n\n设计原则:\n\n- 性能 Performance\n- 开发易用 Ease of Development\n\n## 15 基础设计\n\n集合、字段、基础形状 -> 引用及关联 -> 最终模式\n\n业务需求及逻辑模型 --逻辑导向-> 基础建模 ---> 集合、字段、基础形状\n\n一个文档 16MB max.\n\n内嵌为主。\n\n## 16 工况细化\n\n技术需求、读写比例、方式及数据 --技术导向-> 工况细化 ---> 引用及关联\n\n引用模式 $lookup:\n\n```js\ndb.contacts.aggregate([\n $lookup: {\n form: \"groups\",\n localField: \"groups_ids\",\n foreignField: \"groups_id\",\n as: \"groups\",\n }\n])\n```\n\n使用引用方式:\n\n- 内嵌文档太大\n- 内嵌文档或数组元素频繁修改\n- 内嵌文档数组元素持续增长且没有封顶\n\n使用引用的设计:\n\n- 没有主外键的检查\n- $lookup 只支持 left outer join\n- $lookup 的关系目标(from)不能是分片表\n\n## 17 模式套用\n\n经验和学习 --模式导向-> 套用设计模式 -> 优化的模型\n\n时序数据,分桶设计:利用文档内嵌组,将一个时间段的数据聚合到一个文档里。大量减少文档数据量,大量减少索引占用空间。\n\n## 18 设计模式集锦\n\n大文档,很多字段,很多索引。列转行。列数据变化为数组。多语言多国家属性,类似字段需要建立很多索引。转化为数组,一个索引解决所有查询问题。\n\n模型灵活了,如何管理文档不同版本?增加一个版本字段。schema_version。\n\n统计网页点击流量。近似计算。if random(0,9)==0 increment by 10。\n\n业绩排名、游戏排名等精确统计。消耗资源多,聚合计算时间长。用预聚合字段。模型中直接增加统计字段,每次更新数据时同时更新统计值。\n\n## 19 写操作事务 writeConcern\n\n[write-concern | mongodb](https://www.mongodb.com/docs/manual/reference/write-concern/)\n\n```txt\n{ w: <value>, j: <boolean>, wtimeout: <number> }\n```\n\n```js\nrs.status();\ndb.test.drop()\ndb.test.insert({count:2}, {writeConcern: {w: \"majority\"}})\ndb.test.insert({count:2}, {writeConcern: {w: 3}})\ndb.test.insert({count:2}, {writeConcern: {w: 1}})\n// WriteResult({ \"nInserted\" : 1, \"writeConcernError\" : [ ] })\n\ndb.test.insert({count:2}, {writeConcern: {w: 4}})\n// [Error] 100 - Not enough data-bearing nodes\n\ndb.test.find()\n```\n\n```js\n// 配置延迟节点,模拟网络延迟\nconf=rs.conf()\n// {\n// \"_id\": 3,\n// \"host\": \"mongo3:27017\",\n// \"arbiterOnly\": false,\n// \"buildIndexes\": true,\n// \"hidden\": false,\n// \"priority\": 1,\n// \"tags\": { },\n// \"slaveDelay\": NumberLong(\"0\"),\n// \"votes\": 1\n// }\nconf.members[2].slaveDelay=5\n// 没有选举权\nconf.members[2].priority=0\nrs.reconfig(conf)\n// {\n// \"_id\": 3,\n// \"host\": \"mongo3:27017\",\n// \"arbiterOnly\": false,\n// \"buildIndexes\": true,\n// \"hidden\": false,\n// \"priority\": 0,\n// \"tags\": { },\n// \"slaveDelay\": NumberLong(\"5\"),\n// \"votes\": 1\n// }\n\ndb.test.insert({count:2}, {writeConcern: {w: 3}})\n// Result Time 5s\n\n// 3s 超时\ndb.test.insert({count:5}, {writeConcern: {w: 3, wtimeout:3000}})\n// writeResult({\n// \"nInserted\" : 1,\n// \"writeConcernError\" : {\n// \"code\" : 64,\n// \"codeName\" : \"WriteConcernFailed\",\n// \"errmsg\" : \"waiting for replication timed out\",\n// \"errInfo\" : {\n// \"wtimeout\" : true,\n// \"writeConcern\" : {\n// \"w\" : 3,\n// \"wtimeout\" : 3000,\n// \"provenance\" : \"clientSupplied\"\n// }\n// }\n// }\n// })\n```\n\n## 20 读操作事务 readPreference\n\n- [Read Preference | mongodb](https://www.mongodb.com/docs/manual/core/read-preference/)\n- [Read Preference Tags | mongodb](https://www.mongodb.com/docs/v4.4/core/read-preference-tags/)\n\n配置:\n\n- mongodb://.../?replicaSet=rs&readPrefence=secondary\n- 通过驱动API,MongoCollection.withReadPrefence(ReadPrefence readPref)\n- Mongo Shell: db.collection.find({}).readPref(\"secondary\")\n\n实验:\n\n```js\n// 主节点\ndb.test.drop()\ndb.test.insert({count:2}, {writeConcern: {w: 3}})\ndb.test.find({})\n// 1 line\ndb.test.find({}).readPref(\"secondary\")\n// 1 line\n\n// 从节点 1、2\ndb.test.find({})\n// 1 line\ndb.fsyncLock()\n// now locked against writes, use db.fsyncUnlock() to unlock 锁住写入\n\n// 主节点\ndb.test.insert({count:3})\ndb.test.find({})\n// 2 line\ndb.test.find().readPref(\"secondary\")\n// 1 line\n\n// 从节点 1、2\ndb.fsyncUnlock()\n// 主节点\ndb.test.find().readPref(\"secondary\")\n```\n\n## 21 读操作事务 readConcern\n\n- [read-concern | mongodb](https://www.mongodb.com/docs/manual/reference/read-concern/)\n\nenableMajorityReadConcern: true\n\n```js\n// 从节点 1、2\ndb.fsyncLock()\n// 主节点\ndb.test.insert({count:3})\ndb.test.find().readConcern(\"local\")\n// 如果在一个写操作到达大多数节点前读取了这个写操作,然后因为系统故障该操作回滚了,则发生了脏读\n// {readConcern: \"majority\"} 可以避免脏读\ndb.test.find().readConcern(\"majority\")\n```\n\nmajority 约为:Read Committed。\n\n## 22 多文档事务\n\n- Atomocity 原子性\n- Consistency 一致性 writeConcern,readConcern\n- Isolation 隔离性 readConcern\n- Durability 持久性 Journal and Replication\n\n实验启用事务后的隔离性:\n\n```js\ndb.tx.drop();\ndb.tx.insertMany([{x:1},{x:2}]);\nvar session = db.getMongo().startSession();\nsession.startTransaction();\nvar coll = session.getDatabase('test').getCollection('tx');\ncoll.updateOne({x:2}, {$set: {y:1}});\n// {\n// acknowledged: true,\n// insertedId: null,\n// matchedCount: 1,\n// modifiedCount: 1,\n// upsertedCount: 0\n// }\ncoll.find();\n// [\n// { _id: ObjectId(\"64478074fcfac90fb90f1a65\"), x: 1 },\n// { _id: ObjectId(\"64478074fcfac90fb90f1a66\"), x: 2, y: 1 }\n// ]\ndb.tx.find();\n// [\n// { _id: ObjectId(\"64478074fcfac90fb90f1a65\"), x: 1 },\n// { _id: ObjectId(\"64478074fcfac90fb90f1a66\"), x: 2 }\n// ]\nsession.commitTransaction();\ndb.tx.find();\n```\n\n实验可重复读 Repeatable Read:\n\n```js\ndb.tx.drop();\ndb.tx.insertMany([{x:1},{x:2}]);\nvar session = db.getMongo().startSession();\nsession.startTransaction({\n readConcern: {\"level\": \"snapshot\"},\n writeConcern: {\"w\": \"majority\"}\n});\nvar coll = session.getDatabase('test').getCollection('tx');\ncoll.findOne({x: 1});\n// 模拟事务之外的操作\ndb.tx.updateOne({x:1}, {$set: {y:1}});\ndb.tx.find();\n// [\n// { _id: ObjectId(\"644782c3fcfac90fb90f1a69\"), x: 1, y: 1 },\n// { _id: ObjectId(\"644782c3fcfac90fb90f1a6a\"), x: 2 }\n// ]\ncoll.findOne({x: 1});\n// { _id: ObjectId(\"644782c3fcfac90fb90f1a69\"), x: 1 }\nsession.abortTransaction();\n```\n\n实验写冲突:\n\n```js\ndb.tx.drop();\ndb.tx.insertMany([{x:1},{x:2}]);\n// shell 1、2\nvar session = db.getMongo().startSession();\nsession.startTransaction({\n readConcern: {\"level\": \"snapshot\"},\n writeConcern: {\"w\": \"majority\"}\n});\nvar coll = session.getDatabase('test').getCollection('tx');\n// shell 1\ncoll.updateOne({x:1}, {$set: {y:1}});\n// ok\n\n// shell 2\ncoll.updateOne({x:1}, {$set: {y:1}});\n// MongoServerError: WriteConflict error: this operation conflicted with another operation. Please retry your operation or multi-document transaction.\n// session.abortTransaction();\n```\n\n- 事务默认 60s 内完成。\n- 多文档事务中的读操作必须使用主节点读。\n\n## 23 Change Stream\n\n类似触发器。\n\n- 触发方式:异步 | 同步(事务保证)\n- 触发位置:回调事件 | 数据库触发器\n- 触发次数:每个订阅事件的客户端 | 1次\n- 故障恢复:从上此断点重新触发 | 事务回滚\n\n基于 oplog 实现。被追踪的变更事件主要包括:\n\n- insert/update/delete\n- drop\n- rename\n- dropDatabase\n- invalidate:drop/rename/dropDatabase 将导致 invalidate 被触发,并关闭 chage stream\n\n可重复读。未开启 majority readConcern 的集群无法使用 Change Stream。当集群无法满足 {w: \"majority\"} 时,不会触发 Change Stream。\n\n可以使用集合管道的过滤步骤过滤事件。\n\n## 25 分片集群机制及原理\n\n- 路由节点 mongos\n- 配置节点 mongod\n- 数据节点 mongod\n\n分片集群数据发布方式\n\n- 基于范围 查询性能好,数据分布可能不均匀\n- 基于哈希 发布均匀,范围查询效率低;日志、物联网\n- Zone 数据天然分区\n\n## 25 分片集群设计\n\n- 片键 shard key 文档中的一个字段\n- 文档 doc\n- 块 chunk\n- 分片 shard\n- 集群 cluster\n\n片键:\n\n- 取值基数范围要大\n- 取值范围应尽可能均匀\n- 对主要查询要具有定向能力\n\n组合片键。{user_id:1, time:1}\n\n## 27 分片集群搭建及扩容\n\n```bash\n# 1 配置域名解析\n# 2 准备分片目录\n\n# 3 创建第1个分片复制集 member1:27010 member3:27010 member5:27010\nmongod --bind_ip_all --replSet shard1 --shardsvr --wiredTigerCacheSizeGB 1\n# --shardsvr 标注为分片节点\n# --wiredTigerCacheSizeGB 1 缓存大小\n# 4 初始化分片复制集\n\n# 5 创建 config server 复制集 member1:27019 member3:27019 member5:27019\nmongod --bind_ip_all --replSet config --configsvr --wiredTigerCacheSizeGB 1\n# --configsvr 标注为配置节点\n# 6 创建 config server 复制集\n\n# 7 搭建 mongos member1:27017\nmongos --bind_ip_all --configdb config/member1:27019,member3:27019,member5:27019,\n# 连接到 mongos 添加分片\nsh.addShard(\"shard1/member1:27010,member3:27010,member5:27010\")\n\n# 8 创建分片表\n# 连接到 mongos member1:27017\nsh.enableSharding(\"foo\");\nsh.shardCollection(\"foo.bar\", {_id: \"hashed\"});\nsh.status();\n# 插入测试数据\nuse foo\nfor(var i=0;1<10000;i++) {\n db.bar.insert({i:i))\n}\n\n# 9 创建第2个分片复制集 member2:27011 member4:27011 member6:27011\n# 10 初始化第2个分片复制集\n# 11 加入第2个分片\n# 连接到 mongos 添加分片\nsh.addShard(\"shard2/member2:27011,member4:27011,member6:27011\")\n```\n\n## 28 监控最佳实践\n\n- MongoDB Ops Manager\n- Percona\n- 程序脚本\n\n如何获取监控数据:\n\n- db.serverStatus() 从上次开机到现在为止\n- db.isMaster()\n- monogostats\n- db.serverStatus().opcounters\n\n## 29 备份与恢复\n\n- 延迟节点备份\n- 全量备份+Oplog增量,Oplog 幂等性 支持重放\n- mongodump --oplog;不遗漏 dump 时的数据\n- mongorestore --oplogReplay\n\n## 31 安全架构\n\n```js\ndb.createRole({\n role: \"readWriteRole\",\n privileges: [\n {\n resource: { db: \"myDatabase\", collection: \"sample\" },\n actions: [ \"find\", \"insert\", \"update\", \"remove\" ]\n }\n ],\n roles: [{\n role: 'read',\n db: 'sampledb',\n }]\n})\ndb.createUser({\n user: 'sampleUser',\n pwd: 'pwd',\n roles: [{\n role: 'readWriteRole',\n db: 'admin',\n }]\n})\n```\n\n## 32 安全加固实践\n\n```bash\n# 禁止脚本执行\n--noscripting\n# 必须鉴权\n--auth\n```\n\n## 33 索引机制(一)\n\n- Index、Key、DataPage\n- Covered Query/FETCH\n- IXSCAN/COLLSCAN 索引扫描/集合扫描\n- Big O Notation 时间复杂度\n- Query Shape 查询的形状\n- Index Prefix 索引前缀\n- Selectivity 过滤器 选择区别度大的\n- B-数结构\n\nB-树和B+树都是常见的多路搜索树结构,常用于数据库索引和文件系统中。它们的主要区别在于如何存储和检索数据。\n\nB-树是一种自平衡的搜索树,其中每个节点可以存储多个键和对应的值,并支持在O(log n)时间内进行搜索、插入和删除操作。B-树的每个节点都包含了一个子节点数组,可以用来搜索和遍历树。在B-树中,所有节点都可以存储键和值,而非仅仅是叶子节点。\n\nB+树与B-树非常相似,但是只有叶节点包含了所有的键和值,而且所有叶节点都通过指针链接在一起。这意味着在B+树上进行查找只需要搜索一条从根节点到叶节点的路径,而在B-树中可能需要搜索多个节点。B+树的非叶子节点只包含键,而不包含值,这使得B+树在维护索引时更加高效。\n\n因此,B+树比B-树更适用于存储和检索大量数据,尤其是数据库和文件系统中的索引。B+树的叶子节点形成了一个有序链表,可以方便地进行区间查找和遍历。而B-树则更适合内存较小的情况下,例如缓存。\n\n## 34 索引机制(二)\n\n.explain(true) 查看:\n\n- `executionTimeMillis`\n- `totalDocsExamined`\n- `executionStages.docsExamined`\n- `executionStages.inputStage.stage`\n\n组合索引 ESR 原则,Equal(最前) Sort(中间) Range(最后)\n\ndb.member.createIndex({city:1}, {background: true})\n\n## 36 性能诊断工具\n\nmongostat 了解 MongoDB 运行状态的工具。\n\n- dirty 没有刷盘的比例 <5%\n- used\n- qrw 排队的请求\n- con 连接数量\n\nmongotop 了解集合压力状态\n\n[mtools](https://github.com/rueckstiess/mtools)\n\n## 41 应用场景及选型\n\n优点:\n\n- 横向扩展能力,数据量或并发量增加时候架构可以自动扩容\n- 灵活模型,适合迭代开发,数据模型多变场景\n- JSON 数据结构,适合微服务/REST API\n\n## 44 关系型数据库迁移\n\n从基于关系型数据库应用迁移到 MongoDB 的理由:\n\n- 高并发需求(数千 - 数十万 ops),关系型数据库不容易扩展\n- 快速迭代 - 关系型模式太严谨\n- 灵活的 JSON 模式\n- 大数据量需求\n- 地理位置查询\n- 多数据中心跨地域部署\n\n## References\n\n- [MongoDB 高手课](https://time.geekbang.org/course/intro/100040001)\n- [mongodb-cluster-docker-compose](https://github.com/minhhungit/mongodb-cluster-docker-compose)\n\n-- EOF --\n","tags":["mongodb"]},{"title":"手把手教你落地 DDD Part1","url":"/2023/03/09/hands-on-ddd-part1/","content":"\n## 01 小传\n\nDomain-Driven Design 是一种开发复杂软件的系统化的 **方法学和思想**。\n\n面向对象方法学还不能很好地应用于企业应用原因:\n\n- 很多开发人员走了一条只重技术不重业务的弯路。\n- 围绕业务进行开发的方法本身就不好学。\n- 早期面向对象方法学主要考虑的是建模技术,很少考虑协作问题。\n- 难以适应变化。\n\n从业务出发进行系统的设计。\n\nDDD 从面向对象和敏捷中提炼出了一套原则、模式和实践,使面向对象方法学在企业应用中更加容易学习和掌握。\n\n如果没有迭代开发、持续重构、测试驱动、持续集成等敏捷实践的支持,构建良好的领域模型并在代码上落地是很困难的。\n\n微服务是一种架构风格(或者说架构模式)。\n\n## 02 迭代一\n\n领域专家需要对业务有总体性和本质性的把握,同时对业务发展也要有一定前瞻性,心里要有一盘棋。\n\n```bash\n捕获行为需求(事件风暴)-> 领域建模 -> 结构设计 -> 数据库设计 -> 代码实现\n|------- 模型的建立 --------| |--------- 模型的实现 ------|\n```\n\n## 03 事件风暴 上\n\n1. 识别领域事件;在业务流程中发生了什么?\n1. 识别命令;谁,做了什么事,导致领域事件的发生?\n1. 识别领域名词;命令、事件和那些名词性概念有关?\n\n领域事件表示的是,业务流程中每个步骤引发的结果。一般是完成时 + 被动语态。例:订单已被提交。\n\n在 DDD 中的各种命名,一般都优先使用约定俗成的 **业务术语**。\n\n1. 不要把技术事件当成领域事件。\n1. 查询功能不算领域事件。领域事件应该是对某样事物产生了影响,并被记录的事情。\n\n参加的人,各自写出领域事件 -> 一起讨论,统一理解。这种先发散,后收敛,反复迭代的过程实际上就是一种头脑风暴。“协作”才是事件风暴的精髓。\n\n## 04 事件风暴 下\n\n- 首先我们看看领域事件的作用。从代码实现的角度来看,领域事件一般会对应一段代码逻辑,这段逻辑可能会最终改变数据库中的数据。另外,在事件驱动的架构中,一个领域事件可能会表现为一个向外部发送的异步消息。\n- 那命令的作用体现在哪儿呢?领域建模时,我们可以通过对命令的走查(walkthrough),细化和验证领域模型。在实现层面,一个命令可能对应前端的一个操作,例如按下按钮;对于后端而言,一个命令可能对应一个 API。\n- 再来说命令的执行者。在领域建模时,执行者可能本身就是一个领域对象,也可能是领域对象充当的角色,或者是权限管理中的一个角色。\n\n如果要体现严格的时间顺序,我们可以用流程图、用顺序图等方法,但事件风暴不必关注这一点。\n\n我们是把每个小步骤都当作独立的一个事务来看待,还是把它们合起来作为同一个事务。另外,可以设想,如果每个小步骤都向外界发出一个领域事件,对系统后续的功能是不是有意义。原则上宜粗不宜细。\n\n抓住协作、统一语言、识别领域名词等要点。\n\n事件风暴是一种通过协作的方式捕获行为需求的方法,在这个过程里,业务人员和技术人员一起消化领域知识、形成统一语言、并为领域建模奠定基础。\n\n## References\n\n--\n","tags":["design-pattern","ddd"]},{"title":"正则表达式","url":"/2023/03/01/regular-expression-getting-started/","content":"\n## 00\n\n- [regex101](https://regex101.com/)\n- [regulex](https://jex.im/regulex/#!flags=&re=%5E(a%7Cb)*%3F%24)\n- [ihateregex](https://ihateregex.io/expr/username/)\n\n## 01 元字符\n\n正则表达式 —— 字符串的规则。\n\n元字符就是指那些在正则表达式中具有特殊意义的专用字符。\n\n- 特殊单字符\n - `.` 任意字符(换行除外)\n - `\\d` 任意数字 `\\D` 任意非数字\n - `\\w` A-Za-z0-9_ `\\W`\n - `\\s` 空白符 `\\S`\n- 空白符\n - `\\r` 回车符\n - `\\n` 换行符\n - `\\f` 换页符\n - `\\t` 制表符\n - `\\v` 垂直制表符\n- 范围\n - `|` 或\n - `[abc]` 多选一\n - `[a-z]` 之间\n - `[^abc]` 取反,不能是括号中的任意单个元素\n- 量词\n - `*` 0<=\n - `+` 1<=\n - `?` 0或1\n - `{m}` m\n - `{m,}` m<=\n - `{m,n}` m-n\n\n## 02 量词与贪婪\n\n贪婪(Greedy) `*`:匹配最长。在贪婪量词模式下,正则表达式会尽可能长地去匹配符合规则的字符串,且会回溯。\n\n```php\npreg_match_all(\"/a*/i\", \"aaabb\", $matches);\nvar_dump($matches);\n```\n\n非贪婪(Reluctant) `+?`:匹配最短。在非贪婪量词模式下,正则表达式会匹配尽可能短的字符串。\n\nENV:Python3\n\n```python\nimport re\nre.findall(r'a*', 'aaabb') # 贪婪模式\n# ['aaa', '', '', '']\nre.findall(r'a*?', 'aaabb') # 非贪婪模式\n# ['', 'a', '', 'a', '', 'a', '', '', '']\n\nre.findall(r'\".+\"', '\"the little cat\" is a toy, it lokks \"a little bad\"') # 贪婪模式\n# ['\"the little cat\" is a toy, it lokks \"a little bad\"']\nre.findall(r'\".+?\"', '\"the little cat\" is a toy, it lokks \"a little bad\"') # 非贪婪模式\n# ['\"the little cat\"', '\"a little bad\"']\n```\n\n独占模式(Possessive) `++`:同贪婪一样匹配最长。不过在独占量词模式下,正则表达式尽可能长地去匹配字符串,一旦匹配不成功就会结束匹配而 **不会回溯**。\n\n```python\n# 回溯示例:\nimport re\nre.findall(r'xy{1,3}z', 'xyyz') # 回溯\n# ['xyyz']\n# 正则 xy{1,3} 会尽可能长地去匹配到 xyyz,无法匹配 z,向前回溯 xyy\n# 正则 z 匹配到剩下字符串 z\nre.findall(r'xy{1,3}?z', 'xyyz') # 非贪婪\n# ['xyyz']\n# 正则 xy{1,3} 会尽可能短地去匹配到 xy\n# 正则 z 匹配到字符串 y,无法匹配,向前回溯\n# 正则 xy{1,3} 会尽可能短地去匹配 xyy\n# 正则 z 匹配到剩下字符串 z\n```\n\n```python\n# 独占模式示例:\n# pip install regex -i https://mirrors.aliyun.com/pypi/simple/\nimport regex\nregex.findall(r'xy{1,3}+z', 'xyyz') # 独占\n# ['xyyz']\n# 正则 xy{1,3}+ 会尽可能长地去匹配到 xyy 并占用\n# 正则 z 匹配到字符串 z\nregex.findall(r'xy{1,3}+yz', 'xyyz') # 独占\n# []\n# 正则 xy{1,3}+ 会尽可能长地去匹配到 xyy 并占用\n# 正则 yz 无法匹配到剩下字符串 z\n```\n\n## 03 分组与引用\n\n```python\nimport regex\n# 不保存分组 (?:正则)\nregex.sub(r'(\\d{4})-(?:\\d{2})-(\\d{2})', r\"年:\\1 日:\\2\", '2023-03-01')\n# '年:2023 日:01'\n\n# 去除重复连续单词\nregex.sub(r'(\\w+)(\\s\\1)+', r\"\\1\", 'the little cat cat is in the hat hat hat, we like it.')\n# 'the little cat is in the hat, we like it.'\n```\n\n## 04 匹配模式\n\n指改变元字符匹配行为。\n\n不区分大小写模式(Case-Insensitive)`(?模式标识)` `(?i)`。\n\n```python\nimport regex\nregex.findall(r\"(?i)cat\", \"cat Cat CAt\")\n# ['cat', 'Cat', 'CAt']\n```\n\n```re\n# https://regex101.com/r/3OUJda/1\n# 二次重复时的大小写一致\n((?i)cat) \\1\n```\n\n点号通配模式(Dot All)`(?s)` 让英文的点 `.` 可以匹配上包括换行的任何字符。等价 `[\\s\\S]` `[\\d\\D]` `[\\w\\W]`。\n\n```re\n# https://regex101.com/r/zXtwLv/1\n# 匹配包括换行符\n(?s).+\n```\n\n多行匹配模式(Multiline)`(?m)` 使 `^` 和 `$` 能匹配上每行的开头或结尾。\n\n```re\n# 分行匹配\n(?m)^cat|dog$\n```\n\n注释模式(Comment)`(?#)`\n\n```re\n(\\w+)(?#word) \\1(?#word repeat again)\n```\n\n## 05 断言 Assertion\n\n对要匹配的文本的位置也有一定的要求。只用于匹配位置,而不是文本内容本身,这种结构就是断言。\n\n边界(Boundary)\n\n```python\nimport re\n# 单词边界 \\b\n# tom -> jerry, tomorrow 不受影响\nre.sub(r'\\btom\\b', 'jerry', \"tom asked me if I would go fishing with him tomorrow.\")\n# 'jerry asked me if I would go fishing with him tomorrow.'\n\n# 行的开始结束\n# \\A \\z 不受模式影响\n# \\A -> ^, \\z -> $\nre.sub(r'\\Atom', 'jerry', \"tom asked me if I would go fishing with him tomorrow.\")\n```\n\n```python\n# 环视 左尖括号代表看左边,没有尖括号是看右边,感叹号是非的意思\n# (?<=Y) 左边是Y\n# (?<!Y) 左边不是Y\n# (?=Y) 右边是Y\n# (?!Y) 右边不是Y\n\nre.findall(r'[1-9]\\d{5}', \"138001380002\")\n# ['138001', '380002']\nre.findall(r'(?<!\\d)[1-9]\\d{5}(?!\\d)', \"138001380002\")\n# 左边不是数字、右边不是数字\n# []\nre.findall(r'(?<!\\d)[1-9]\\d{5}(?!\\d)', \"code138001code\")\n# 左边不是数字、右边不是数字\n# ['138001']\n\n# \\b\\w+\\b -> (?<!\\w)\\w+(?!\\w) -> (?<=\\W)\\w+(?=\\W)\n# https://regex101.com/r/PBEKxY/1\n\n# (\\w+)(\\s+\\b\\1\\b)+\n# 单词,单词的左边是单词边界、可以有一个及以上空格,右边是单词边界\n# 比 (\\w+)(\\s+\\1)+ 更严谨 eg: the little cat cat2 is in the hat hat2\n```\n\n## 06 转义\n\n转义字符 Escape Character 后面的字符,不是原来的意思了。\n\n```python\nimport re\nre.findall(r'\\\\d', 'abc\\\\d123d\\\\')\n# ['\\\\d']\nre.findall('\\\\', 'a*b+c?\\\\d123d\\\\')\n# bad escape (end of pattern) at position 0\nre.findall('\\\\\\\\', 'a*b+c?\\\\d123d\\\\')\n# ['\\\\', '\\\\']\n# 字符串->正则表达式:字符串转义和正则转义\n# \\\\\\\\ 字符串转义 \\\\\n# \\\\ 正则转义 \\\nre.findall(r'\\\\', 'a*b+c?\\\\d123d\\\\')\n# ['\\\\', '\\\\']\nre.findall('\\(\\)\\[]\\{}', '()[]{}')\n# ['()[]{}']\n# 方括号和花括号的转义一般转义开括号就可以,但圆括号两个都需要转义\n```\n\n```python\nimport re\nre.escape('\\d') # 反斜杠和字母d转义\n# '\\\\\\\\d'\nre.findall(re.escape('\\d'), '\\d')\n# ['\\\\d']\nre.escape('[+]')\n# '\\\\[\\\\+\\\\]'\nre.findall(re.escape('[+]'), '[+]')\n# ['[+]']\n```\n\n```python\nimport re\nre.findall(r'[^ab]', '^ab') # 转义前代表\"非\"\n# ['^']\nre.findall(r'[^cd]', '^ab')\n# ['^', 'a', 'b']\nre.findall(r'[\\^ab]', '^ab') # 转义后代表普通字符\n# ['^', 'a', 'b']\nre.findall(r'[a-c]', 'abc-') # 中划线在中间,代表\"范围\"\n# ['a', 'b', 'c']\nre.findall(r'[a\\-c]', 'abc-') # 中划线在中间,转义后的\nre.findall(r'[-ac]', 'abc-') # 在开头,不需要转义\nre.findall(r'[ac-]', 'abc-') # 在结尾,不需要转义\n# ['a', 'c', '-']\nre.findall(r'[]ab]', ']ab') # 右括号不转义,在首位\n# [']', 'a', 'b']\nre.findall(r'[a]b]', ']ab') # 右括号不转义,不在首位\n# []\nre.findall(r'[a\\]b]', ']ab') # 转义后代表普通字符\n# [']', 'a', 'b']\nre.findall(r'[.*+?()]', '[.*+?()]') # 单个长度的元字符在中括号里,可以不转义\n# ['.', '*', '+', '?', '(', ')']\nre.findall(r'[\\d]', 'd12\\\\') # \\w,\\d等在中括号中还是元字符的功能\n# ['1', '2']\n```\n\n```python\nimport re\nre.findall('\\n', '\\\\n\\n\\\\')\n# ['\\n'] \\n -> (\\n) -> (\\n)\nre.findall('\\\\n', '\\\\n\\n\\\\')\n# ['\\n'] \\\\n -> \\n -> (\\n)\nre.findall('\\\\\\n', '\\\\n\\n\\\\')\n# ['\\n'] \\\\\\n -> \\n -> (\\n)\nre.escape('\\n')\n# '\\\\\\n'\nre.findall('\\\\\\\\n', '\\\\n\\n\\\\')\n# ['\\\\n'] \\\\\\\\n -> \\\\\\n -> \\(\\n)\nre.escape('\\\\n')\n# '\\\\\\\\n'\n```\n\n## 07 流派及其特性\n\n- POSIX Portable Operating System Interface。不能使用 `\\d`。\n - BRE Basic Regular Expression 基本正则表达式。`grep` `sed` 花园问管家 `{}()?|+` 要转义。\n - ERE Extended Regular Expression 扩展正则表达式。`egrep` `grep -E` `sed -E`。\n- PCRE Perl Compatible Regular Expressions。可以使用 `\\d` `\\w` `\\s`。`grep -P` `sed -P`。\n\n```bash\ngrep --help | grep PATTERN\n# PATTERN is, by default, a basic regular expression (BRE).\n# -E, --extended-regexp PATTERN is an extended regular expression (ERE)\n# -F, --fixed-strings PATTERN is a set of newline-separated fixed strings\n# -G, --basic-regexp PATTERN is a basic regular expression (BRE)\n# -P, --perl-regexp PATTERN is a Perl regular expression\n```\n\n> [Linux/Unix 工具与正则表达式的 POSIX 规范 | 余晟](https://www.infoq.cn/article/2011/07/regular-expressions-6-posix)\n\n## 08 处理 Unicode 文本\n\nUnicode 相当于规定了字符对应的码值,这个码值得编码成字节的形式去传输和存储。最常见的编码方式是 UTF-8,另外还有 UTF-16,UTF-32 等。UTF-8 之所以能够流行起来,是因为其编码比较巧妙,采用的是变长的方法。也就是一个 Unicode 字符,在使用 UTF-8 编码表示时占用 1 到 4 个字节不等。最重要的是 Unicode 兼容 ASCII 编码,在表示纯英文时,并不会占用更多存储空间。而汉字呢,在 UTF-8 中,通常是用三个字节来表示。\n\n```python\n# python2.7\nimport re\nu'极客'.encode('utf-8')\n# '\\xe6\\x9e\\x81\\xe5\\xae\\xa2'\nu'时间'.encode('utf-8')\n# '\\xe6\\x97\\xb6\\xe9\\x97\\xb4'\n# 都含有 e6\n\nre.search(r'[时间]', '极客') is not None\n# True\n\nre.compile(r'[时间]', re.DEBUG)\n# in\n# literal 230\n# literal 151\n# literal 182\n# literal 233\n# literal 151\n# literal 180\n# <_sre.SRE_Pattern object at 0x10ab44d78>\n\nre.compile(r'[极客]', re.DEBUG)\n# in\n# literal 230\n# literal 158\n# literal 129\n# literal 229\n# literal 174\n# literal 162\n# <_sre.SRE_Pattern object at 0x10ab44e40>\n\nre.compile(ur'[时间]', re.DEBUG)\n# in\n# literal 26102\n# literal 38388\n# <_sre.SRE_Pattern object at 0x10ac02710>\n\nre.search(ur'[时间]', '时间') is not None\nFalse\n\nre.search(ur'[时间]', u'时间') is not None\nTrue\n```\n\n```python\n# python2.7\nimport re\nre.findall(r'^.$', '学')\n# []\nre.findall(r'^.$', u'学')\n# [u'\\u5b66']\nre.findall(ur'^.$', u'学')\n# [u'\\u5b66']\nprint(unichr(0x5B66))\n# 学\n```\n\n```python\n# python3\nimport re\nre.findall(r'^.$', '学')\n# ['学']\nre.findall(r'(?a)^.$', '学')\n# ['学']\n# (?a) 表示启用 ASCII 模式\nchr(0x5B66)\n# '学'\n```\n\n```php\n// 可以匹配汉语 in PHP\n\\p{Han}\n```\n\n```python\n# python2.7\nimport re\nre.findall(r'客{3}', '极客客客客')\n# []\nre.findall(ur'客{3}', '极客客客客')\n# []\nre.findall(r'客{3}', u'极客客客客')\n# []\nre.findall(ur'客{3}', u'极客客客客')\n# [u'\\u5ba2\\u5ba2\\u5ba2']\nre.findall(r'(客){3}', '极客客客客')\n```\n\n```python\n# python3\nre.findall(r'客{3}', '极客客客客')\n# ['客客客']\n# 在 Python3 中,不需要在正则表达式字符串前面添加 u 前缀,因为所有字符串都默认为 Unicode 字符串。\n```\n\n- [Script (Unicode) | wikipedia](https://en.wikipedia.org/wiki/Script_(Unicode)#Hani)\n\n## 09 编辑器中使用正则\n\n竖向编辑:MacOS alt + 鼠标纵向滑动。\n\n## 10 语言中用正则\n\n校验文本内容:\n\n```python\nimport re\nreg = re.compile(r'\\A\\d{4}-\\d{2}-\\d{2}\\Z') # 建议先编译,提高效率\nreg.search('2020-06-01') is not None\n# True\nreg.match('2020-06-01') is not None # 使用 match 时 \\A 可省略,match 就是从头匹配\n# True\n\nreg = re.compile(r'\\d{4}-\\d{2}')\nreg.findall('2020-05 2020-06')\n# ['2020-05', '2020-06']\n```\n\n```js\n/^\\d{4}-\\d{2}-\\d{2}$/.test(\"2020-06-01\")\n// true\nvar regex = new RegExp(/^\\d{4}-\\d{2}-\\d{2}$/)\nregex.test(\"2020-01-01\")\n// true\nvar regex = /^\\d{4}-\\d{2}-\\d{2}$/\n\"2020-06-01\".search(regex)\n// 0\n```\n\n```php\n$regex = '/^\\d{4}-\\d{2}-\\d{2}$/';\n$ret = preg_match($regex, \"2020-06-01\");\nvar_dump($ret);\n// int(1)\n```\n\n提取文本内容:\n\n```python\nimport re\n# 没有子组时\nreg = re.compile(r'\\d{4}-\\d{2}')\nreg.findall('2020-05 2020-06')\n# ['2020-05', '2020-06']\n\n# 有子组时\nreg = re.compile(r'(\\d{4})-(\\d{2})')\nreg.findall('2020-05 2020-06')\n[('2020', '05'), ('2020', '06')]\n\nreg = re.compile(r'(\\d{4})-(\\d{2})')\nfor match in reg.finditer('2020-05 2020-06'):\n print('date: ', match[0]) # 整个正则匹配到的内容\n print('year: ', match[1]) # 第一个子组\n print('month:', match[2]) # 第二个子组\n# date: 2020-05\n# year: 2020\n# month: 05\n# date: 2020-06\n# year: 2020\n# month: 06\n```\n\n```js\n// 使用g模式,查找所有符合要求的内容\n\"2020-06 2020-07\".match(/\\d{4}-\\d{2}/g)\n// ['2020-06', '2020-07']\n\n// 不使用g模式,找到第一个就会停下来\n\"2020-06 2020-07\".match(/\\d{4}-\\d{2}/)\n// ['2020-06', index: 0, input: '2020-06 2020-07', groups: undefined]\n```\n\n```php\n$regex = \"/\\d{4}-\\d{2}/\";\n$str = \"2020-05 2020-04\";\n$matchs = [];\npreg_match_all($regex, $str, $matchs, PREG_SET_ORDER);\nvar_dump($matchs);\n// array(2) {\n// [0] =>\n// array(1) {\n// [0] =>\n// string(7) \"2020-05\"\n// }\n// [1] =>\n// array(1) {\n// [0] =>\n// string(7) \"2020-04\"\n// }\n// }\n\n// PREG_PATTERN_ORDER: 结果排序为$matches[0]保存完整模式的所有匹配, $matches[1]保存第一个子组的所有匹配,以此类推。\n// PREG_SET_ORDER: 结果排序为$matches[0]包含第一次匹配得到的所有匹配(包含子组),$matches[1]是包含第二次匹配到的所有匹配(包含子组)的数组,以此类推。\n```\n\n替换文本内容:\n\n```python\nreg = re.compile(r'(\\d{2})-(\\d{2})-(\\d{4})')\nreg.sub(r'\\3年\\1月\\2日', '02-20-2020 05-21-2020')\n# '2020年02月20日 2020年05月21日'\n\n# 可以在替换中使用 \\g<数字>,如果分组多于10个时避免歧义\nreg.sub(r'\\g<3>年\\g<1>月\\g<2>日', '02-20-2020 05-21-2020')\n# '2020年02月20日 2020年05月21日'\n\n# 返回替换次数\nreg.subn(r'\\3年\\1月\\2日', '02-20-2020 05-21-2020')\n# ('2020年02月20日 2020年05月21日', 2)\n```\n\n```js\n// 使用g模式,替换所有的\n\"02-20-2020 05-21-2020\".replace(/(\\d{2})-(\\d{2})-(\\d{4})/g, \"$3年$1月$2日\")\n// \"2020年02月20日 2020年05月21日\"\n\n// 不使用 g 模式时,只替换一次\n\"02-20-2020 05-21-2020\".replace(/(\\d{2})-(\\d{2})-(\\d{4})/, \"$3年$1月$2日\")\n// \"2020年02月20日 05-21-2020\"\n```\n\n```php\n$ret = preg_replace('/(\\d{2})-(\\d{2})-(\\d{4})/', '\\3年\\1月\\2日', \"02-20-2020 05-21-2020\");\nvar_dump($ret);\n// string(35) \"2020年02月20日 2020年05月21日\"\n```\n\n切割文本内容:\n\n```python\nreg = re.compile(r'\\W+')\nreg.split(\"apple, pear! orange; tea\")\n# ['apple', 'pear', 'orange', 'tea']\n\n# 限制切割次数,比如切一刀,变成两部分\nreg.split(\"apple, pear! orange; tea\", 1)\n# ['apple', 'pear! orange; tea']\n```\n\n```js\n\"apple, pear! orange; tea\".split(/\\W+/)\n// [\"apple\", \"pear\", \"orange\", \"tea\"]\n\n// 传入第二个参数的情况\n\"apple, pear! orange; tea\".split(/\\W+/, 1)\n// [\"apple\"]\n\"apple, pear! orange; tea\".split(/\\W+/, 2)\n// [\"apple\", \"pear\"]\n\"apple, pear! orange; tea\".split(/\\W+/, 10)\n// [\"apple\", \"pear\", \"orange\", \"tea\"]\n```\n\n```php\n$ret = preg_split('/\\W+/', 'apple, pear! orange; tea');\nvar_dump($ret);\n// array(4) {\n// [0] =>\n// string(5) \"apple\"\n// [1] =>\n// string(4) \"pear\"\n// [2] =>\n// string(6) \"orange\"\n// [3] =>\n// string(3) \"tea\"\n// }\n$ret = preg_split('/\\W+/', 'apple, pear! orange; tea', 2);\nvar_dump($ret);\n// array(2) {\n// [0] =>\n// string(5) \"apple\"\n// [1] =>\n// string(17) \"pear! orange; tea\"\n// }\n```\n\n## 11 匹配原理以及优化原则\n\n回溯不可怕,我们要尽量减少回溯后的判断\n\n```python\nimport re\nx = '-' * 1000000 + 'abc'\ntimeit re.search('abc', x)\n```\n\n- 提前编译好正则。\n- 尽量准确表示匹配范围:匹配引号里面的内容 `.+?` 改写为 `[^\"]+`。\n- 提取出公共部分:`(abcd|abxy)` => `ab(cd|xy)`,`(^this|^that)` => `^th(is|at)`。\n- 出现可能性大的放左边:`\\.(?:com|net)\\b`。\n- 只在必要时才使用子组:把不需要保存子组的括号中加上 `?:` 来表示只用于归组。\n- 警惕嵌套的子组重复:`(.*)*` 匹配的次数会呈指数级增长,尽量不要写这样的正则。\n- 避免不同分支重复匹配。\n\nNFA 是以表达式为主导的,先看正则表达式,再看文本。而 DFA 则是以文本为主导的,先看文本,再看正则表达式。POSIX NFA 是指符合 POSIX 标准的 NFA 引擎,它会不断回溯,以确保找到最左侧最长匹配。\n\n## 12 常见问题\n\n```python\nimport re\nre.match(r'^(?:(?!\\d\\d)\\w){6}$', '11abcd') # 不能匹配上\n# 否定预测先行断言的语法\"(?!)\"来排除两个数字字符结尾的情况\n# (?!) 表示匹配不满足某个条件的位置\nre.match(r'^(?:\\w(?!\\d\\d)){6}$', '11abcd') # 错误正则示范\n# <re.Match object; span=(0, 6), match='11abcd'>\n# (11) 回溯\n# 1(1a) ok\n# 11ab... ok\n```\n\n- 正负号、可二位小数、小数位末尾 0 无影响 [Regulex](https://jex.im/regulex/#!flags=&re=%5E%5B-%2B%5D%3F%5Cd%2B(%3F%3A%5C.(%3F%3A%5Cd)%7B0%2C2%7D0*)%3F%24):`^[-+]?\\d+(?:\\.(?:\\d){0,2}0*)?$`\n- 手机号码:`1(?:3\\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\\d|9[1389])\\d{8}`\n- 身份证:`[1-9]\\d{14}(\\d\\d[0-9Xx])?`\n- 邮政编码:`(?<!\\d)\\d{6}(?!\\d)`\n- 中文字符:`[\\u4E00-\\u9FFF]` `\\p{Han}`\n- 邮箱:`a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+`\n\n## 程语言的角度来理解正则\n\n- 命令式编程的世界观是:程序是由若干行动指令组成的有序列表;\n- 命令式编程的方法论是:用变量来存储数据,用语句来执行指令。\n- 声明式编程的世界观是:程序是由若干目标任务组成的有序列表;\n- 声明式编程的方法论是:用语法元素来描述任务,由解析引擎转化为指令并执行。\n\n## References\n\n- 《精通正则表达式(第三版)》\n- 《正则指引(第二版)》\n\n-- EOF --\n","tags":["regular-expression"]},{"title":"Vim 实用技巧必知必会","url":"/2023/02/28/vim-practical-tips/","content":"\n## 安装\n\n```bash\nyum list installed | grep vim\n# vim-X11.x86_64 2:7.4.629-8.el7_9 @updates\n# vim-common.x86_64 2:7.4.629-8.el7_9 @updates\n# vim-enhanced.x86_64 2:7.4.629-8.el7_9 @updates\n# vim-filesystem.x86_64 2:7.4.629-8.el7_9 @updates\n# vim-minimal.x86_64 2:7.4.629-8.el7_9 @updates\n\n# https://gist.github.com/yevrah/21cdccc1dc65efd2a4712781815159fb\n# Update to Vim8 on CentOS 7\n\nrpm -Uvh http://mirror.ghettoforge.org/distributions/gf/gf-release-latest.gf.el7.noarch.rpm\nrpm --import http://mirror.ghettoforge.org/distributions/gf/RPM-GPG-KEY-gf.el7\nyum -y remove vim-minimal vim-common vim-enhanced\nyum -y --enablerepo=gf-plus install vim-enhanced vim-filesystem sudo\n\nvim --version\n# VIM - Vi IMproved 8.0 (2016 Sep 12, compiled Sep 18 2016 14:42:40)\nyum list installed | grep vim\n# vim-common.x86_64 2:8.0.003-1.gf.el7 @gf-plus\n# vim-enhanced.x86_64 2:8.0.003-1.gf.el7 @gf-plus\n# vim-filesystem.x86_64 2:8.0.003-1.gf.el7 @gf-plus\n# vim-minimal.x86_64 2:8.0.003-1.gf.el7 @gf-plus\n```\n\n```bash\n# 中文帮助\nmkdir -p ~/.vim/pack/my/start\ncd ~/.vim/pack/my/start\ngit clone git://github.com/yianwillis/vimcdoc.git\n```\n\n```bash\n# 自带教程\nLANG=zh_CN.UTF-8 vimtutor\n```\n\n##\n\n```\n\n```\n","tags":["vim"]},{"title":"Kubernetes 入门实战 Part3","url":"/2023/02/24/kubernetes-getting-started-part3/","content":"\n## 24 PersistentVolume 数据持久化\n\nPersistentVolume 属于集群的系统资源,是和 Node 平级的一种对象,Pod 对它没有管理权,只有使用权。\n\nStorageClass 它抽象了特定类型的存储系统(比如 Ceph、NFS),在 PVC 和 PV 之间充当“协调人”的角色,帮助 PVC 找到合适的 PV。\n\n```yaml\n# vim host-path-pv-pvc.yml\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n # 只有 10MB 容量的存储设备\n name: host-10m-pv\nspec:\n # kubectl explain PersistentVolume.spec.storageClassName\n storageClassName:\n host-vol\n # kubectl explain PersistentVolume.spec.accessModes\n accessModes:\n # ReadWriteOnce:存储卷可读可写,但只能被一个节点上的 Pod 挂载。\n # ReadOnlyMany:存储卷只读不可写,可以被任意节点上的 Pod 多次挂载。\n # ReadWriteMany:存储卷可读可写,也可以被任意节点上的 Pod 多次挂载。\n - ReadWriteOnce\n capacity:\n # Ki/Mi/Gi\n storage: 10Mi\n hostPath:\n path: /tmp/host-10m-pv/\n\n---\n# PVC 不表示实际的存储,而是一个“申请”或者“声明”\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: host-5m-pvc\nspec:\n storageClassName: host-vol\n accessModes:\n - ReadWriteOnce\n resources:\n requests:\n storage: 5Mi\n```\n\n```bash\nmkdir /tmp/host-10m-pv\nkubectl apply -f host-path-pv-pvc.yml\nkubectl get pv\n# NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE\n# host-10m-pv 10Mi RWO Retain Bound default/host-5m-pvc host-vol 4s\nkubectl get pvc\n# NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE\n# host-5m-pvc Bound host-10m-pv 10Mi RWO host-vol 6s\n```\n\n```yaml\n# vim host-pvc-pod.yml\napiVersion: v1\nkind: Pod\nmetadata:\n name: host-pvc-pod\nspec:\n volumes:\n - name: host-pvc-vol\n persistentVolumeClaim:\n claimName: host-5m-pvc\n containers:\n - name: ngx-pvc-pod\n image: nginx:alpine\n ports:\n - containerPort: 80\n volumeMounts:\n - name: host-pvc-vol\n mountPath: /tmp\n```\n\n```bash\nkubectl apply -f host-pvc-pod.yml\nkubectl get pod -o wide\nkubectl exec -it host-pvc-pod -- sh\ncd /tmp && touch a.md\n# check in worker node\n/tmp/host-10m-pv/\n```\n\n## 25 NFS 网络共享存储\n\n...\n\n## References\n\n- [chronolaw/k8s_study | GitHub](https://github.com/chronolaw/k8s_study)\n","tags":["kubernetes"]},{"title":"Kubernetes 入门实战 Part2","url":"/2023/02/22/kubernetes-getting-started-part2/","content":"\n## 17 多节点的 Kubernetes 集群\n\n在腾讯云 TencentOS Server 3.1 (TK4) 下测试:\n\n- master SA3.MEDIUM4 2 核 4GB 5Mbps\n- worker S5.SMALL2 1 核 2GB 1Mbps\n- worker S5.SMALL2 1 核 2GB 1Mbps\n\n```bash\n# 修改源 https://mirrors.cloud.tencent.com/\nmv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup\nwget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.cloud.tencent.com/repo/centos7_base.repo\nyum clean all\nyum makecache\n\n# install docker\nyum remove docker \\\n docker-client \\\n docker-client-latest \\\n docker-common \\\n docker-latest \\\n docker-latest-logrotate \\\n docker-logrotate \\\n docker-engine\n\nyum install -y yum-utils\n\nyum-config-manager \\\n --add-repo \\\n https://download.docker.com/linux/centos/docker-ce.repo\n\nyum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin\n\nsystemctl start docker\ndocker -v\ndocker run hello-world\n```\n\n一些准备工作:\n\n```bash\n# 改主机名\nvi /etc/hostname\n# reboot\n```\n\n```bash\n# 把 cgroup 的驱动程序改成 systemd\n# 使用 Docker 作为 Kubernetes 的底层支持\ncat <<EOF | sudo tee /etc/docker/daemon.json\n{\n \"exec-opts\": [\"native.cgroupdriver=systemd\"],\n \"log-driver\": \"json-file\",\n \"log-opts\": {\n \"max-size\": \"100m\"\n },\n \"storage-driver\": \"overlay2\"\n}\nEOF\n\nsystemctl enable docker\nsystemctl daemon-reload\nsystemctl restart docker\ndocker version\n```\n\n```bash\n# https://kubernetes.io/zh-cn/docs/setup/production-environment/container-runtimes/\n# 转发 IPv4 并让 iptables 看到桥接流量\ncat <<EOF | sudo tee /etc/modules-load.d/k8s.conf\noverlay\nbr_netfilter\nEOF\n\nsudo modprobe overlay\nsudo modprobe br_netfilter\n\n# check\nlsmod | grep br_netfilter\nlsmod | grep overlay\n\n# 设置所需的 sysctl 参数,参数在重新启动后保持不变\ncat <<EOF | sudo tee /etc/sysctl.d/k8s.conf\nnet.bridge.bridge-nf-call-iptables = 1\nnet.bridge.bridge-nf-call-ip6tables = 1\nnet.ipv4.ip_forward = 1\nEOF\n\n# 应用 sysctl 参数而不重新启动\nsudo sysctl --system\n\nsysctl net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-ip6tables net.ipv4.ip_forward\n# net.bridge.bridge-nf-call-iptables = 1\n# net.bridge.bridge-nf-call-ip6tables = 1\n# net.ipv4.ip_forward = 1\n```\n\n```bash\n# 关闭 Linux 的 swap 分区\nswapoff -a\nsed -ri '/\\sswap\\s/s/^#?/#/' /etc/fstab\n```\n\n```bash\n# https://developer.aliyun.com/mirror/kubernetes\ncat <<EOF > /etc/yum.repos.d/kubernetes.repo\n[kubernetes]\nname=Kubernetes\nbaseurl=https://mirrors.cloud.tencent.com/kubernetes/yum/repos/kubernetes-el7-x86_64/\nEOF\n\nyum clean all\nyum makecache\n\n# 将 SELinux 设置为 permissive 模式(相当于将其禁用)\nsetenforce 0\nsed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config\n\n# 新版本搞不定\n# yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes --nogpgcheck\n\nyum --showduplicate list kubelet\nyum install -y kubelet-1.23.16-0 kubeadm-1.23.16-0 kubectl-1.23.16-0 --disableexcludes=kubernetes --nogpgcheck\n\nsystemctl enable --now kubelet\n\nkubeadm version\nkubectl version --output=yaml\nkubelet --version\n```\n\n下载 Kubernetes 组件镜像:\n\n```bash\n# kubeadm config images list\nkubeadm config images list --kubernetes-version v1.23.16\n```\n\n安装 Master 节点:\n\n```bash\nvim /etc/containerd/config.toml\n#disabled_plugins = [\"cri\"]\n\nsystemctl enable containerd\nsystemctl restart containerd\nsystemctl status containerd\n\nsystemctl enable kubelet.service\nsystemctl restart kubelet\nsystemctl status kubelet\n\ncontainerd config default > /etc/containerd/config.toml\n\nyum install -y nc\nnc 127.0.0.1 6443\n\nkubeadm init -h\nkubeadm reset -f\nrm -rf ~/.kube/config\n\nkubeadm init \\\n --image-repository=registry.aliyuncs.com/google_containers \\\n --pod-network-cidr=10.10.0.0/16 \\\n --kubernetes-version=v1.23.16 \\\n --v=9\n\n# Run \"kubectl apply -f [podnetwork].yaml\" with one of the options listed at:\n# https://kubernetes.io/docs/concepts/cluster-administration/addons/\n```\n\n```bash\n# success\nmkdir -p $HOME/.kube\nsudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config\nsudo chown $(id -u):$(id -g) $HOME/.kube/config\n\nexport KUBECONFIG=/etc/kubernetes/admin.conf\n```\n\n```bash\nkubectl get node\n# NAME STATUS ROLES AGE VERSION\n# master NotReady control-plane,master 2m4s v1.23.16\n```\n\n```bash\n# dubug\nsystemctl restart docker\nsystemctl restart kubelet\nsystemctl restart containerd\njournalctl -xeu kubelet\ncrictr ps -a\ncrictl --runtime-endpoint unix:///var/run/containerd/containerd.sock ps -a | grep kube | grep -v pause\n\nkubectl get pods -n kube-system\nkubectl describe pods -n kube-system\n```\n\n```bash\n# Flannel 网络插件 https://github.com/flannel-io/flannel/tree/master\n# curl https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml --output kube-flannel.yml\n# net-conf.json: |\n# {\n# \"Network\": \"10.10.0.0/16\",\n# \"Backend\": {\n# \"Type\": \"vxlan\"\n# }\n# }\nkubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml\nkubectl get node\n# NAME STATUS ROLES AGE VERSION\n# master Ready control-plane,master 14h v1.23.16\n```\n\n```bash\n# show join command in control-plane\nkubeadm token create --print-join-command\n# work join; 云服务记得开放入站端口\ntelnet 172.21.0.5 6443\nsystemctl enable kubelet.service\nkubeadm join 172.21.0.5:6443 --token xxx --discovery-token-ca-cert-hash sha256:xxx --v=9\n# check in control-plane\nkubectl get nodes\n# NAME STATUS ROLES AGE VERSION\n# master Ready control-plane,master 14h v1.23.16\n# vm-0-9-centos Ready <none> 3m27s v1.23.16\n```\n\n```bash\n# run nginx\nkubectl run ngx --image=nginx:alpine\nkubectl get pod -o wide\n# NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES\n# ngx 1/1 Running 0 52m 10.10.1.2 woker01 <none> <none>\n```\n\n## 18 Deployment 部署应用\n\n“单一职责”和“对象组合”。既然 Pod 管理不了自己,那么我们就再创建一个新的对象,由它来管理 Pod,采用和 Job/CronJob 一样的形式——“对象套对象”。\n\n```bash\nkubectl api-resources\n\nexport out=\"--dry-run=client -o yaml\"\nkubectl create deploy ngx-dep --image=nginx:alpine $out > ngx-dep.yml\nvim ngx-dep.yml\n```\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n creationTimestamp: null\n labels:\n app: ngx-dep\n name: ngx-dep\nspec:\n # 定义了 Pod 的“期望数量”,Kubernetes 会自动维护 Pod 数量到正常水平\n replicas: 2\n # 定义了基于 labels 筛选 Pod 的规则,它必须与 template 里 Pod 的 labels 一致\n selector:\n matchLabels:\n app: ngx-dep\n strategy: {}\n template:\n metadata:\n creationTimestamp: null\n # 贴标签\n labels:\n app: ngx-dep\n spec:\n containers:\n - image: nginx:alpine\n name: nginx\n resources: {}\nstatus: {}\n```\n\nDeployment 实际上并不“持有”Pod 对象,它只是帮助 Pod 对象能够有足够的副本数量运行。\n\n通过标签这种设计,Kubernetes 就解除了 Deployment 和模板里 Pod 的强绑定,把组合关系变成了“弱引用”。\n\n```bash\n# replicas: 2\nkubectl apply -f ngx-dep.yml\n\nkubectl get deploy\n# NAME READY UP-TO-DATE AVAILABLE AGE\n# ngx-dep 2/2 2 2 57s\nkubectl get pod\n# NAME READY STATUS RESTARTS AGE\n# ngx-dep-bfbb5f64b-96scb 1/1 Running 0 3m20s\n# ngx-dep-bfbb5f64b-qnzbh 1/1 Running 0 3m20s\n```\n\n- READY:运行的 Pod 数量,当前数量/期望数量。\n- UP-TO-DATE:当前已经更新到最新状态的 Pod 数量。\n- AVAILABLE:不仅要求已经运行,还必须是健康状态,能够正常对外提供服务,它才是我们最关心的 Deployment 指标。\n- AGE:从创建到现在所经过的时间。\n\n```bash\n# 测试自启恢复\nkubectl delete pod ngx-dep-bfbb5f64b-qnzbh\nkubectl get pod\n# NAME READY STATUS RESTARTS AGE\n# ngx-dep-bfbb5f64b-7n724 1/1 Running 0 33s\n# ngx-dep-bfbb5f64b-96scb 1/1 Running 0 4m52s\n\n# 测试伸缩\nkubectl scale --replicas=5 deploy ngx-dep\nkubectl get pod\n# NAME READY STATUS RESTARTS AGE\n# ngx-dep-bfbb5f64b-7n724 1/1 Running 0 77s\n# ngx-dep-bfbb5f64b-7xhbs 1/1 Running 0 7s\n# ngx-dep-bfbb5f64b-96scb 1/1 Running 0 5m36s\n# ngx-dep-bfbb5f64b-97qp5 1/1 Running 0 7s\n# ngx-dep-bfbb5f64b-vjn4q 1/1 Running 0 7s\n```\n\n```bash\n# 筛选标签 ==、!=、in、notin\nkubectl get pod -l app=nginx\nkubectl get pod -l 'app in (ngx, nginx, ngx-dep)'\n```\n\n## 19 DaemonSet 看门狗\n\n在 Deployment 看来,Pod 的运行环境与功能是无关的,只要 Pod 的数量足够,应用程序应该会正常工作。\n\n有些场景下,要在集群里的每个节点上都运行 Pod,也就是说 Pod 的数量与节点数量保持同步。防止在集群里漂移。\n\nDaemonSet 的目标是在集群的每个节点上运行且仅运行一个 Pod。\n\n```bash\nkubectl api-resources\n```\n\n```yaml\n# export out=\"--dry-run=client -o yaml\"\n# kubectl create deploy redis-ds --image=redis:5-alpine $out\n# kind modify DaemonSet, delete spec.replicas\n# https://kubernetes.io/zh-cn/docs/concepts/workloads/controllers/daemonset/\n# vim redis-ds.yml\napiVersion: apps/v1\nkind: DaemonSet\nmetadata:\n name: redis-ds\n labels:\n app: redis-ds\nspec:\n # 和 deplayment 比没有 replicas\n selector:\n matchLabels:\n name: redis-ds\n template:\n metadata:\n labels:\n name: redis-ds\n spec:\n containers:\n - image: redis:5-alpine\n name: redis\n ports:\n - containerPort: 6379\n```\n\n```bash\nkubectl apply -f redis-ds.yml\nkubectl get ds\n# NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE\n# redis-ds 2 2 2 2 2 <none> 30m\nkubectl get pod -o wide\n# 两个 worker 的场景\n# NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES\n# redis-ds-9r96k 1/1 Running 0 2m52s 10.10.3.2 woker02 <none> <none>\n# redis-ds-hdl28 1/1 Running 0 21m 10.10.1.11 woker01 <none> <none>\n# Master 节点却被排除在外了\n```\n\n污点(taint)作用也是给节点“贴标签”。容忍度(toleration)Pod 能否“容忍”污点。\n\n```bash\nkubectl describe node master\n# Taints: node-role.kubernetes.io/master:NoSchedule\n# 污点会拒绝 Pod 调度到本节点上运行\nkubectl describe node woker01\n# Taints: <none>\n```\n\n```bash\n# - 出掉 master 污点\nkubectl taint node master node-role.kubernetes.io/master:NoSchedule-\n# NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE\n# redis-ds 3 3 3 3 3 <none> 31m\n```\n\n```yaml\n# Pod 添加 tolerations\n# kubectl explain ds.spec.template.spec.tolerations\n# vim redis-ds-t.yml\napiVersion: apps/v1\nkind: DaemonSet\nmetadata:\n name: redis-ds-t\n labels:\n app: redis-ds-t\nspec:\n # 和 deplayment 比没有 replicas\n selector:\n matchLabels:\n name: redis-ds-t\n template:\n metadata:\n labels:\n name: redis-ds-t\n spec:\n containers:\n - image: redis:5-alpine\n name: redis\n ports:\n - containerPort: 6379\n # 容忍 node-role.kubernetes.io/master\n tolerations:\n - key: node-role.kubernetes.io/master\n effect: NoSchedule\n operator: Exists\n```\n\n```bash\nkubectl apply -f redis-ds-t.yml\nkubectl get ds\n# NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE\n# redis-ds 2 2 2 2 2 <none> 41m\n# redis-ds-t 3 3 3 3 3 <none> 6s\n# 差别在 master\nkubectl get pod -o wide\n# NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES\n# redis-ds-9r96k 1/1 Running 0 23m 10.10.3.2 woker02 <none> <none>\n# redis-ds-hdl28 1/1 Running 0 42m 10.10.1.11 woker01 <none> <none>\n# redis-ds-t-4mptv 1/1 Running 0 80s 10.10.3.4 woker02 <none> <none>\n# redis-ds-t-dpcl8 1/1 Running 0 80s 10.10.1.12 woker01 <none> <none>\n# redis-ds-t-kdjmn 1/1 Running 0 80s 10.10.0.6 master <none> <none>\n```\n\n<https://kubernetes.io/zh-cn/docs/concepts/scheduling-eviction/taint-and-toleration/>\n\n静态 Pod:\n\n```bash\nll -a /etc/kubernetes/manifests\n# -rw------- 1 root root 2274 Feb 22 12:47 etcd.yaml\n# -rw------- 1 root root 3358 Feb 22 12:47 kube-apiserver.yaml\n# -rw------- 1 root root 2878 Feb 22 12:47 kube-controller-manager.yaml\n# -rw------- 1 root root 1465 Feb 22 12:47 kube-scheduler.yaml\n```\n\nKubernetes 的 4 个核心组件 apiserver、etcd、scheduler、controller-manager 原来都以静态 Pod 的形式存在的,这也是为什么它们能够先于 Kubernetes 集群启动的原因。\n\nkubelet 会定期检查目录里的文件。\n\n```bash\n# flannel 就是一个 DaemonSet\nkubectl get ns\nkubectl get ds -n kube-flannel\n# NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE\n# kube-flannel-ds 3 3 3 3 3 <none> 3h54m\n```\n\n## 20 Service 服务发现\n\n由 kube-proxy 控制的四层负载均衡,在 TCP/IP 协议栈上转发流量。\n\nPod 的生命周期很短暂,会不停地创建销毁,所以就需要用 Service 来实现负载均衡,它由 Kubernetes 分配固定的 IP 地址,能够屏蔽后端的 Pod 变化。\n\n```bash\nexport out=\"--dry-run=client -o yaml\"\nkubectl expose deploy ngx-dep --port=80 --target-port=80 $ou\n```\n\n<img width=\"600\" alt=\"image\" src=\"https://user-images.githubusercontent.com/9289792/220598245-da0f7319-8c28-4c08-b003-1dd209af1ee6.png\">\n\n```yaml\n# vim ngx-svc.yml\napiVersion: v1\nkind: Service\nmetadata:\n name: ngx-svc\nspec:\n ports:\n - port: 80\n protocol: TCP\n targetPort: 80\n selector:\n app: ngx-dep\nstatus:\n loadBalancer: {}\n\n---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: ngx-conf\ndata:\n default.conf: |\n server {\n listen 80;\n location / {\n default_type text/plain;\n return 200\n 'srv : $server_addr:$server_port\\nhost: $hostname\\nuri : $request_method $host $request_uri\\ndate: $time_iso8601\\n';\n }\n }\n\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: ngx-dep\nspec:\n replicas: 2\n selector:\n matchLabels:\n app: ngx-dep\n template:\n metadata:\n labels:\n app: ngx-dep\n spec:\n volumes:\n - name: ngx-conf-vol\n configMap:\n name: ngx-conf\n containers:\n - image: nginx:alpine\n name: nginx\n ports:\n - containerPort: 80\n volumeMounts:\n - mountPath: /etc/nginx/conf.d\n name: ngx-conf-vol\n```\n\n```bash\nkubectl apply -f ngx-svc.yml\nkubectl get svc\n# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE\n# kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 5h10m\n# ngx-svc ClusterIP 10.109.131.132 <none> 80/TCP 35s\n# 虚地址 10.109.131.132\nkubectl describe svc ngx-svc\n# Name: ngx-svc\n# Namespace: default\n# Labels: <none>\n# Annotations: <none>\n# Selector: app=ngx-dep\n# Type: ClusterIP\n# IP Family Policy: SingleStack\n# IP Families: IPv4\n# IP: 10.109.131.132\n# IPs: 10.109.131.132\n# Port: <unset> 80/TCP\n# TargetPort: 80/TCP\n# Endpoints: 10.10.1.13:80,10.10.3.5:80\n# Session Affinity: None\n# Events: <none>\nkubectl get pod -o wide\nNAME READY STATUS RESTARTS AGE IP NODE\nngx-dep-6796688696-cwm8f 1/1 Running 0 2m4s 10.10.3.5 woker02\nngx-dep-6796688696-khjnv 1/1 Running 0 2m2s 10.10.1.13 woker01\n# same Endpoints\n\n# 因为 Service、 Pod 的 IP 地址都是 Kubernetes 集群的内部网段\n# 所以我们需要用 kubectl exec 进入到 Pod 内部\nkubectl exec -it ngx-dep-6796688696-cwm8f -- sh\ncurl 10.109.131.132\n# srv : 10.10.3.5:80\n# host: ngx-dep-6796688696-cwm8f\n# uri : GET 10.109.131.132 /\n# date: 2023-02-22T10:09:49+00:00\ncurl 10.109.131.132\n# srv : 10.10.1.13:80\n# host: ngx-dep-6796688696-khjnv\n# uri : GET 10.109.131.132 /\n# date: 2023-02-22T10:09:50+00:00\n\n# 测试恢复\nkubectl delete pod ngx-dep-6796688696-khjnv\nkubectl describe svc ngx-svc\n# Endpoints: 10.10.1.14:80,10.10.3.5:80\n# 之前是 10.10.1.13:80,10.10.3.5:80\n\n# 测试扩容\nkubectl scale --replicas=5 deploy ngx-dep\nkubectl describe svc ngx-svc\n# Endpoints: 10.10.1.14:80,10.10.1.15:80,10.10.3.5:80 + 2 more...\n```\n\nService 对象的域名完全形式是 `对象.名字空间.svc.cluster.local`,但很多时候也可以省略后面的部分,直接写 `对象.名字空间` 甚至 `对象名` 就足够了,默认会使用对象所在的名字空间(比如这里就是 default)。\n\n```bash\n# Name: ngx-svc\n# Namespace: default\nkubectl exec -it ngx-dep-6796688696-cwm8f -- sh\ncurl ngx-svc\n# srv : 10.10.3.5:80\n# host: ngx-dep-6796688696-cwm8f\n# uri : GET ngx-svc /\n# date: 2023-02-22T10:19:25+00:00\ncurl ngx-svc.default\n# srv : 10.10.3.6:80\n# host: ngx-dep-6796688696-lpcfs\n# uri : GET ngx-svc.default /\n# date: 2023-02-22T10:19:41+00:00\ncurl ngx-svc.default.svc.cluster.local\n# srv : 10.10.3.5:80\n# host: ngx-dep-6796688696-cwm8f\n# uri : GET ngx-svc.default.svc.cluster.local /\n# date: 2023-02-22T10:20:04+00:00\n```\n\nPod 分配了域名:`IP 地址.名字空间.pod.cluster.local` IP 地址 . 改成 -。\n\n```yaml\n# kubectl explain svc.spec.type\n# vim ngx-svc.yml\n ...\n type: NodePort\n```\n\n```bash\nkubectl apply -f ngx-svc.yml\nkubectl get svc\n# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE\n# kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 5h56m\n# ngx-svc NodePort 10.109.131.132 <none> 80:30916/TCP 46m\n# Service 的默认类型是“ClusterIP”,只能在集群内部访问,\n# 如果改成“NodePort”,就会在节点上开启一个随机端口号,让外界也能够访问内部的服务。\ncurl localhost:30916\n# srv : 10.10.1.15:80\n# host: ngx-dep-6796688696-l6skl\n# uri : GET localhost /\n# date: 2023-02-22T10:48:32+00:00\n```\n\n<img width=\"600\" alt=\"image\" src=\"https://user-images.githubusercontent.com/9289792/220598107-2899f665-3977-4e19-894d-bd273046fbae.png\">\n\n## 21 Ingress 流量总管\n\nService 本身是没有服务能力的,它只是一些 iptables 规则,真正配置、应用这些规则的实际上是节点里的 kube-proxy 组件。\n\nIngress 也只是一些 HTTP 路由规则的集合,相当于一份静态的描述文件,真正要把这些规则在集群里实施运行,还需要有另外一个东西,这就是 Ingress Controller,它的作用就相当于 Service 的 kube-proxy,能够读取、应用 Ingress 规则,处理、调度流量。\n\nIngress Class 是插在 Ingress 和 Ingress Controller 中间,作为流量规则和控制器的协调人,解除了 Ingress 和 Ingress Controller 的强绑定关系。\n\nKubernetes 用户可以转向管理 Ingress Class,用它来定义不同的业务逻辑分组,简化 Ingress 规则的复杂度。\n\n```bash\nexport out=\"--dry-run=client -o yaml\"\nkubectl create ing ngx-ing --rule=\"ngx.test/=ngx-svc:80\" --class=ngx-ink $out\n```\n\n```yaml\n# vim ingress.yml\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n creationTimestamp: null\n name: ngx-ing\nspec:\n ingressClassName: ngx-ink\n rules:\n - host: ngx.test\n http:\n # 路径的匹配方式\n paths:\n - backend:\n service:\n name: ngx-svc\n port:\n number: 80\n path: /\n # 精确匹配(Exact)或前缀匹配(Prefix)\n pathType: Exact\nstatus:\n loadBalancer: {}\n\n---\napiVersion: networking.k8s.io/v1\nkind: IngressClass\nmetadata:\n name: ngx-ink\nspec:\n controller: nginx.org/ingress-controller\n```\n\n```bash\nkubectl apply -f ingress.yml\n# NAME CONTROLLER PARAMETERS AGE\n# ngx-ink nginx.org/ingress-controller <none> 15s\nkubectl get ing\n# NAME CLASS HOSTS ADDRESS PORTS AGE\n# ngx-ing ngx-ink ngx.test 80 84s\nkubectl describe ing ngx-ing\n# Name: ngx-ing\n# Labels: <none>\n# Namespace: default\n# Address:\n# Default backend: default-http-backend:80 (<error: endpoints \"default-http-backend\" not found>)\n# Rules:\n# Host Path Backends\n# ---- ---- --------\n# ngx.test\n# / ngx-svc:80 (10.10.1.14:80,10.10.1.15:80)\n# Annotations: <none>\n# Events: <none>\n```\n\n在 Kubernetes 里使用 Ingress Controller:\n\n<https://docs.nginx.com/nginx-ingress-controller/installation/installation-with-manifests/>\n\n```bash\ngit clone https://github.com/nginxinc/kubernetes-ingress.git --branch v3.0.2\ncd kubernetes-ingress/deployments\n\n# Configure RBAC\n# 为Ingress控制器创建一个命名空间和一个服务账户\nkubectl apply -f common/ns-and-sa.yaml\n# 为服务账户创建一个集群角色和集群角色绑定\nkubectl apply -f rbac/rbac.yaml\n\n# Create Common Resources\n# 为NGINX的默认服务器创建一个带有TLS证书和密钥的秘密\nkubectl apply -f common/default-server-secret.yaml\n# 创建一个 config map,用于定制NGINX配置。\nkubectl apply -f common/nginx-config.yaml\n# 创建一个IngressClass资源\nkubectl apply -f common/ingress-class.yaml\n\n# Create Custom Resources\n# kubectl apply -f common/crds/\nvim deployment/nginx-ingress.yaml\n# args add:\n# -enable-custom-resources=false\n\n# Run the Ingress Controller\nkubectl apply -f deployment/nginx-ingress.yaml\n\n# check\nkubectl get pods --namespace=nginx-ingress\n# NAME READY STATUS RESTARTS AGE\n# nginx-ingress-5f98f8f5f9-nnkv7 1/1 Running 0 3m14s\n\n# Get Access to the Ingress Controller\nkubectl create -f service/nodeport.yaml\nkubectl get service -n nginx-ingress\n# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE\n# nginx-ingress NodePort 10.111.210.52 <none> 80:31754/TCP,443:30188/TCP 5s\n```\n\n```bash\n# debug\nkubectl get IngressClass\nkubectl get ing -n nginx-ingress\nkubectl get deploy -n nginx-ingress\nkubectl get pod -n nginx-ingress -o wide\n\nkubectl describe service -n nginx-ingress -o wide\nkubectl describe pod -n nginx-ingress\n```\n\n```bash\n# 命令kubectl port-forward,它可以直接把本地的端口映射到 Kubernetes 集群的某个 Pod 里\nkubectl port-forward -n nginx-ingress nginx-ingress-5f98f8f5f9-nnkv7 8080:80 &\n```\n\n<img width=\"900\" alt=\"image\" src=\"https://user-images.githubusercontent.com/9289792/221135639-3b31c1be-f624-4abf-8851-cff396e9be45.png\">\n\n## 22 玩转 Kubernetes 2\n\nKubernetes 部署 WordPress:\n\n<img width=\"900\" alt=\"image\" src=\"https://user-images.githubusercontent.com/9289792/221116386-7f08aa66-941c-42ac-a0c3-bb8a111fc42d.png\">\n\n```yaml\n# vim wp-maria.yml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: maria-cm\ndata:\n DATABASE: \"db\"\n USER: \"wp\"\n PASSWORD: \"123\"\n ROOT_PASSWORD: \"123\"\n\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n labels:\n app: maria-dep\n name: maria-dep\nspec:\n replicas: 1\n selector:\n matchLabels:\n app: maria-dep\n template:\n metadata:\n labels:\n app: maria-dep\n spec:\n containers:\n - image: mariadb:10\n name: mariadb\n ports:\n - containerPort: 3306\n envFrom:\n - prefix: \"MARIADB_\"\n configMapRef:\n name: maria-cm\n\n---\napiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: maria-dep\n name: maria-svc\nspec:\n ports:\n - port: 3306\n protocol: TCP\n targetPort: 3306\n selector:\n app: maria-dep\n```\n\n```bash\nkubectl apply -f wp-maria.yml\nkubectl get pod\nkubectl get deploy\nkubectl get svc\n```\n\n```yaml\n# vim wp-app.yml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: wp-cm\ndata:\n # DNS HOST\n HOST: \"maria-svc\"\n USER: \"wp\"\n PASSWORD: \"123\"\n NAME: \"db\"\n\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n labels:\n app: wp-dep\n name: wp-dep\nspec:\n replicas: 2\n selector:\n matchLabels:\n app: wp-dep\n template:\n metadata:\n labels:\n app: wp-dep\n spec:\n containers:\n - image: wordpress:5\n name: wordpress\n ports:\n - containerPort: 80\n envFrom:\n - prefix: \"WORDPRESS_DB_\"\n configMapRef:\n name: wp-cm\n\n---\napiVersion: v1\nkind: Service\nmetadata:\n labels:\n app: wp-dep\n name: wp-svc\nspec:\n ports:\n - name: http80\n port: 80\n protocol: TCP\n targetPort: 80\n # 指定端口\n nodePort: 30088\n selector:\n app: wp-dep\n # NodePort\n type: NodePort\n```\n\n```bash\nkubectl apply -f wp-app.yml\nkubectl get pod\nkubectl get deploy\nkubectl get svc\n\nkubectl port-forward service/wp-svc 80:80 --address 0.0.0.0\n```\n\n```yaml\n# vim wp-ing.yml\napiVersion: networking.k8s.io/v1\nkind: IngressClass\nmetadata:\n name: wp-ink\nspec:\n controller: nginx.org/ingress-controller\n\n---\n# kubectl create ing wp-ing --rule=\"wp.test/=wp-svc:80\" --class=wp-ink $out\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n name: wp-ing\nspec:\n ingressClassName: wp-ink\n rules:\n - host: wp.test\n http:\n paths:\n - path: /\n pathType: Prefix\n backend:\n service:\n name: wp-svc\n port:\n number: 80\n```\n\n```yaml\n# vim wp-kic-dep.yml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: wp-kic-dep\n namespace: nginx-ingress\nspec:\n replicas: 1\n selector:\n matchLabels:\n app: wp-kic-dep\n template:\n metadata:\n labels:\n app: wp-kic-dep\n spec:\n # kubectl explain Deployment.spec.template.spec.serviceAccountName\n serviceAccountName: nginx-ingress\n # kubectl explain Deployment.spec.template.spec.hostNetwork\n hostNetwork: true\n containers:\n - image: nginx/nginx-ingress:3.0.2\n imagePullPolicy: IfNotPresent\n name: nginx-ingress\n ports:\n - name: http\n containerPort: 80\n - name: https\n containerPort: 443\n - name: readiness-port\n containerPort: 8081\n - name: prometheus\n containerPort: 9113\n readinessProbe:\n httpGet:\n path: /nginx-ready\n port: readiness-port\n periodSeconds: 1\n securityContext:\n allowPrivilegeEscalation: true\n runAsUser: 101 #nginx\n runAsNonRoot: true\n capabilities:\n drop:\n - ALL\n add:\n - NET_BIND_SERVICE\n env:\n - name: POD_NAMESPACE\n valueFrom:\n fieldRef:\n fieldPath: metadata.namespace\n - name: POD_NAME\n valueFrom:\n fieldRef:\n fieldPath: metadata.name\n args:\n # 默认是 nginx\n - -ingress-class=wp-ink\n - -enable-custom-resources=false\n - -nginx-configmaps=$(POD_NAMESPACE)/nginx-config\n - -default-server-tls-secret=$(POD_NAMESPACE)/default-server-secret\n\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: wp-kic-svc\n namespace: nginx-ingress\n\nspec:\n ports:\n - port: 80\n protocol: TCP\n targetPort: 80\n nodePort: 30080\n\n selector:\n app: wp-kic-dep\n type: NodePort\n```\n\n```bash\nkubectl apply -f wp-ing.yml -f wp-kic-dep.yml\n\nkubectl get ing\nkubectl get ingressclass\nkubectl get pod -n=nginx-ingress\nkubectl describe pod -n=nginx-ingress\nkubectl get deploy -n=nginx-ingress\nkubectl get svc -n=nginx-ingress\n```\n\n```bash\n# 在服务器上\nkubectl get pod -n=nginx-ingress -o=wide\n# NAME READY STATUS RESTARTS AGE IP NODE\n# wp-kic-dep-68579bc688-d64zs 1/1 Running 0 10m 172.21.0.9 woker01\ncurl 172.21.0.9 -H \"HOST: wp.test\"\n```\n\n```bash\n# 在服务器外\nkubectl port-forward service/wp-kic-svc -n=nginx-ingress 80:80 --address 0.0.0.0\n\nvim /etc/hosts\n[master ip] wp.test\n# 游览器访问 wp.test\n```\n\n<img width=\"900\" alt=\"image\" src=\"https://user-images.githubusercontent.com/9289792/221134023-69087496-f390-4b6a-90ca-9e7dde86bfc1.png\">\n\n## 23 中级篇实操总结\n\n```bash\n# DaemonSet 模板生成\nkubectl create deploy redis-ds --image=redis:5-alpine $out \\\n | sed 's/Deployment/DaemonSet/g' - \\\n | sed -e '/replicas/d' -\n```\n\n## References\n\n- [chronolaw/k8s_study | GitHub](https://github.com/chronolaw/k8s_study)\n- [nginx-ingress-controller Installation with Manifests | nginx.com](https://docs.nginx.com/nginx-ingress-controller/installation/installation-with-manifests/)\n\n-- EOF --\n","tags":["kubernetes"]},{"title":"Kubernetes 入门实战 Part1","url":"/2023/02/15/kubernetes-getting-started-part1/","content":"\n## 01 初识 Docker\n\n```bash\napt install -y docker.io\nservice docker start\nusermod -aG docker ${USER}\n```\n\n```bash\ndocker version\ndocker info\ndocker ps\ndocker pull busybox\n# show all images\ndocker images\n```\n\n<https://docs.docker.com/get-started/overview/>\n\n## 02 被隔离的进程\n\n```bash\ndocker pull alpine\ndocker run -it alpine sh\ncat /etc/os-release\n```\n\n隔离资源,保证系统安全,提高资源的利用率。\n\n资源隔离提供了三种技术:namespace、 cgroup、chroot(pivot_rott)。\n\n## 03 容器化的应用\n\nBuild once, Run anywhere.\n\n应用程序不再直接和操作系统打交道,而是封装成镜像,再交给容器环境去运行。\n\n```bash\n# remove image\ndocker rmi busybox\n# 开启一个交互式操作的 Shell\ndocker run -it busybox\n# 在后台运行\ndocker run -d busybox\n# 为容器起一个名字\ndocker run -d --name xxx busybox\n# 不保存容器,只要运行完毕就自动清除\ndocker run --rm busybox echo \"hello docker\"\n\ndocker stop xxx\n# remove container\ndocker rm xxx\n\n# show all container\ndocker ps -a\ndocker exec xxx echo \"hello docker\"\n```\n\n## 05 镜像仓库\n\n- <https://github.com/docker-library/official-images>\n- <https://hub.docker.com/u/library>\n\n`用户名/应用名:标签` 官方镜像用户名是 `library`。\n\n- slim 经过精简的\n- fat 包含了较多的辅助工具\n- rc 候选版本,release candidate\n\n```bash\ndocker build -t\ndocker tag ngx-app chronolaw/ngx-app:1.0\ndocker push chronolaw/ngx-app:1.0\n```\n\nsave 和 load 这两个镜像归档命令:\n\n```bash\ndocker save ngx-app:latest -o ngx.tar\ndocker load -i ngx.tar\n```\n\n## 06 打破次元壁\n\n```bash\ndocker run -d --rm --name ubu phusion/baseimage:jammy-1.0.1\necho \"hello\" > a.txt\n# a.txt 拷贝到容器 tmp\ndocker cp a.txt ubu:/tmp\ndocker exec -it ubu bash\n# 考出容器\ndocker cp ubu:/tmp/a.txt ./b.txt\n```\n\n容器和主机共享本地目录:\n\n```bash\n# --mount\n# -v /tmp:/tmp:ro 只读\ndocker run -d --rm -v /tmp:/tmp --name ubu phusion/baseimage:jammy-1.0.1\ndocker exec -it ubu bash\n```\n\n```bash\ndocker pull python:alpine\ndocker run -it --rm -v `pwd`:/tmp python:alpine sh\n```\n\n网络模式:`null` `host` `bridge`\n\n```bash\n# host\ndocker run -d --rm --net=host --name=ng nginx:alpine\ndocker exec ng ip addr\ndocker inspect ng | grep IPAddress\ndocker stop ng\n```\n\n```bash\n# bridge 默认模式\ndocker run -d --rm --name=ng nginx:alpine\ndocker inspect ng | grep IPAddress\n# \"IPAddress\": \"172.17.0.3\"\ndocker run -d --rm --name=rd redis\ndocker inspect rd | grep IPAddress\n# \"IPAddress\": \"172.17.0.4\"\n```\n\n分配服务端口号\n\n```bash\ndocker run -d -p 80:80 --rm nginx:alpine\ndocker run -d -p 8080:80 --rm nginx:alpine\n# 分别“映射”到了两个容器里的 80 端口\n```\n\n## 07 玩转 Docker\n\nContainer Image Registry\n\n<https://registry.hub.docker.com/_/registry/>\n\n```bash\ndocker run -d -p 5000:5000 registry\n# 使用 docker tag 命令给镜像打标签再上传\ndocker tag nginx:alpine 127.0.0.1:5000/nginx:alpine\ndocker push 127.0.0.1:5000/nginx:alpine\n# 本次重新拉取测试\ndocker rmi 127.0.0.1:5000/nginx:alpine\ndocker pull 127.0.0.1:5000/nginx:alpine\n```\n\n<https://docs.docker.com/registry/spec/api/>\n\n```bash\n# Listing Repositories\ncurl 127.1:5000/v2/_catalog\n# {\"repositories\":[\"nginx\"]}\ncurl 127.1:5000/v2/nginx/tags/list\n# {\"name\":\"nginx\",\"tags\":[\"alpine\"]}\n```\n\nregistry 默认会把镜像存储在 Docker 内部目录 `/var/lib/registry`。\n\n搭建 WordPress:\n\n```bash\ndocker run -d --rm \\\n --env MARIADB_DATABASE=db \\\n --env MARIADB_USER=wp \\\n --env MARIADB_PASSWORD=123 \\\n --env MARIADB_ROOT_PASSWORD=123 \\\n --name mariadb \\\n mariadb:10\n# 进入 mariadb\ndocker exec -it mariadb mysql -uwp -p123\n# show mariadb ip\ndocker inspect mariadb | grep IPAddress\n# \"IPAddress\": \"172.17.0.2\"\n\ndocker run -d --rm \\\n --env WORDPRESS_DB_HOST=172.17.0.2 \\\n --env WORDPRESS_DB_USER=wp \\\n --env WORDPRESS_DB_PASSWORD=123 \\\n --env WORDPRESS_DB_NAME=db \\\n --name wp \\\n wordpress:5\ndocker inspect wp | grep IPAddress\n# \"IPAddress\": \"172.17.0.4\"\n```\n\n```bash\nvim wp.conf\n\nserver {\n listen 80;\n default_type text/html;\n location / {\n proxy_http_version 1.1;\n proxy_set_header Host $host;\n # wordpress server\n proxy_pass http://172.17.0.4;\n }\n}\n```\n\n```bash\n# 感觉可以直接用 wordpress 的 80,无需再代理一次\ndocker run -d --rm \\\n -p 80:80 \\\n -v `pwd`/wp.conf:/etc/nginx/conf.d/default.conf \\\n --name ng \\\n nginx:alpine\n```\n\n```bash\n# show logs\ndocker logs mariadb\ndocker logs ng\ndocker logs wp\n```\n\n## 08 入门篇总结\n\n```bash\ndocker pull alpine\ndocker run -it alpine sh\nuname -a\n# Linux de3852b4ec42 5.15.49-linuxkit #1 SMP Tue Sep 13 07:51:46 UTC 2022 x86_64 Linux\n\n# Darwin V_YFANZHAO-MB1 19.6.0 Darwin Kernel Version 19.6.0: Tue Jun 21 21:18:39 PDT 2022; root:xnu-6153.141.66~1/RELEASE_X86_64 x86_64\n```\n\n构建自己的镜像:\n\n<https://github.com/chronolaw/k8s_study/blob/master/ch1/Dockerfile>\n\n```Dockerfile\nARG IMAGE_BASE=\"nginx\"\nARG IMAGE_TAG=\"1.21-alpine\"\n\nFROM ${IMAGE_BASE}:${IMAGE_TAG}\n\nENV PATH=$PATH:/tmp\nENV DEBUG=OFF\n\nCOPY ./default.conf /etc/nginx/conf.d/\n\nRUN cd /usr/share/nginx/html \\\n && echo \"hello nginx\" > a.txt\n\nEXPOSE 8081 8082 8083\n\nWORKDIR /etc/nginx\n```\n\n```bash\ndocker build -t ngx-app:1.0 .\ndocker run -it --rm ngx-app:1.0 sh\ndocker save ngx-app:1.0 -o ngx.tar\ndocker load -i ngx.tar\n```\n\n## 09 Kubernetes 环境\n\n容器编排 Container Orchestration\n\nKubernetes 就是一个生产级别的容器编排平台和集群管理系统。\n\n<https://minikube.sigs.k8s.io/docs/start/>\n\n```bash\nminikube version\n# minikube version: v1.29.0\n# commit: ddac20b4b34a9c8c857fc602203b6ba2679794d3\n\n# 统一实验环境\nminikube start --kubernetes-version=v1.23.3\n# 查看状态\nminikube status\nminikube node list\n# minikube 192.168.49.2\n\n# 登录到这个节点上\nminikube ssh\nuname -a\n# Linux minikube 5.15.49-linuxkit #1 SMP Tue Sep 13 07:51:46 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux\nip add\n```\n\n```bash\n# minikube 管理 Kubernetes 集群环境\n# kubectl 操作实际的 Kubernetes 功能\n# install kubectl\nminikube kubectl\n\nminikube kubectl -- version\nalias kubectl=\"minikube kubectl --\"\nkubectl version\n\n# 启动一个镜像\nkubectl run ngx --image=nginx:alpine\n\n# like docker ps\nkubectl get pod\n```\n\n<https://kubernetes.io/zh/>\n\n## 10 Kubernetes 工作机制\n\nKubernetes 采用了现今流行的“控制面 / 数据面”(Control Plane / Data Plane)架构,集群里的计算机被称为“节点”(Node),可以是实机也可以是虚机,少量的节点用作控制面来执行集群的管理维护工作,其他的大部分节点都被划归数据面,用来跑业务应用。Master 节点实现管理控制功能,Worker 节点运行具体业务。\n\n<img width=\"600\" alt=\"image\" src=\"https://user-images.githubusercontent.com/9289792/219293821-a4e1620a-c024-4edf-ba29-a5bc1f8d3f57.png\">\n\n```bash\nkubectl get node\n# NAME STATUS ROLES AGE VERSION\n# minikube Ready control-plane,master 38m v1.23.3\n\n# 查看 master Component pod\nkubectl get pod -n kube-system\n# NAME READY STATUS RESTARTS AGE\n# coredns-65c54cc984-4pckd 1/1 Running 0 40m\n# etcd-minikube 1/1 Running 0 40m\n# kube-apiserver-minikube 1/1 Running 0 40m\n# kube-controller-manager-minikube 1/1 Running 0 40m\n# kube-proxy-88wt2 1/1 Running 0 40m\n# kube-scheduler-minikube 1/1 Running 0 40m\n# storage-provisioner 1/1 Running 3 (6m39s ago) 40m\n```\n\n```bash\n# worker node\nminikube ssh\n# show kube-proxy\ndocker ps |grep kube-proxy\n# show kubelet, not exist docker\nps -ef|grep kubelet\n```\n\n```bash\n# show addons list\nminikube addons list\n\nminikube dashboard\n```\n\n<img width=\"600\" alt=\"image\" src=\"https://user-images.githubusercontent.com/9289792/219294564-12655273-044c-4d56-a99a-d6f6e7cf7525.png\">\n\n## 11 YAML\n\nYAML 是 JSON 的超集。\n\nShell 脚本和 Dockerfile 可以很好地描述“命令式”(Imperative)。“声明式”(Declarative)注重结果。\n\napiserver 采用了 HTTP 协议的 URL 资源理念,API 风格也用 RESTful,被称为是“API 对象”了。\n\n```bash\n# 查看 kubectl api-service 支持的所有对象\nkubectl api-resources\n\n# 显示出详细的命令执行过程\nkubectl get pod --v=9\n# 自带的 API 文档 https://kubernetes.io/docs/reference/kubernetes-api/\nkubectl explain pod\nkubectl explain pod.metadata\n\n# 命令式\nkubectl run ngx --image=nginx:alpine\n# 转换为 YAML 声名式\nkubectl run ngx --image=nginx:alpine --dry-run=client -o yaml > ngx-pod.yml\nkubectl apply -f ngx-pod.yml\nkubectl delete -f ngx-pod.yml\n```\n\n## 12 Pod\n\n为了解决这样多应用联合运行的问题,同时还要不破坏容器的隔离,就需要在容器外面再建立一个“收纳舱”。\n\n```yaml\napiVersion: v1\nkind: Pod\nmetadata:\n creationTimestamp: null\n labels:\n run: ngx\n name: ngx\nspec:\n containers:\n - image: nginx:alpine\n name: ngx\n resources: {}\n dnsPolicy: ClusterFirst\n restartPolicy: Always\nstatus: {}\n```\n\n<img width=\"600\" alt=\"image\" src=\"https://user-images.githubusercontent.com/9289792/219309091-7436a44b-c0e4-4e8f-8dc1-7a208122ae7d.png\">\n\n```bash\nvim ngx-pod.yml\nkubectl apply -f ngx-pod.yml\nkubectl logs ngx\nkubectl get pod\nkubectl describe pod ngx\n\n# cp file to pod\necho 'aaa' > a.txt\nkubectl cp a.txt ngx:/tmp\n\n# exec need --\nkubectl exec -it ngx -- sh\n```\n\n## 13 Job CronJob 离线业务\n\n“单一职责”的意思是对象应该只专注于做好一件事情,不要贪大求全,保持足够小的粒度才更方便复用和管理。\n\n“组合优于继承”的意思是应该尽量让对象在运行时产生联系,保持松耦合,而不要用硬编码的方式固定对象的关系。\n\n```bash\nkubectl create job echo-job --image=busybox --dry-run=client -o yaml > job.yml\n```\n\n```yaml\napiVersion: batch/v1\nkind: Job\nmetadata:\n creationTimestamp: null\n name: echo-job\nspec:\n template:\n metadata:\n creationTimestamp: null\n spec:\n containers:\n - image: busybox\n name: echo-job\n resources: {}\n # 补充输出\n command: [\"/bin/echo\"]\n args: [\"hello\", \"world\"]\n restartPolicy: Never\nstatus: {}\n```\n\n```bash\nkubectl apply -f job.yml\nkubectl get job\n# NAME COMPLETIONS DURATION AGE\n# echo-job 1/1 16s 33s\nkubectl get pod\n# NAME READY STATUS RESTARTS AGE\n# echo-job-g2bf6 0/1 Completed 0 68s\nkubectl logs echo-job\n# hello world\n```\n\n```yaml\napiVersion: batch/v1\nkind: Job\nmetadata:\n creationTimestamp: null\n name: sleep-job\nspec:\n # 设置 Pod 运行的超时时间\n activeDeadlineSeconds: 60\n # 设置 Pod 的失败重试次数\n backoffLimit: 2\n # Job 完成需要运行多少个 Pod,默认是 1\n completions: 4\n # 它与 completions 相关,表示允许并发运行的 Pod 数量,避免过多占用资源\n parallelism: 2\n\n template:\n metadata:\n creationTimestamp: null\n spec:\n containers:\n - image: busybox\n name: echo-job\n resources: {}\n # 随机休眠\n command:\n - sh\n - -c\n - sleep $(($RANDOM % 10 + 1)) && echo done\n restartPolicy: Never\nstatus: {}\n```\n\n```bash\nkubectl apply -f sleep-job.yaml\n\nkubectl get pod -w\n# NAME READY STATUS RESTARTS AGE\n# sleep-job-92m4d 0/1 Completed 0 30s\n# sleep-job-g8pmj 0/1 Completed 0 15s\n# sleep-job-tsncl 0/1 Completed 0 30s\n# sleep-job-x4qlp 0/1 Completed 0 15s\n```\n\n```bash\n# cronjob\nkubectl create cj echo-cj --image=busybox --schedule=\"*/1 * * * *\" --dry-run=client -o yaml > echo-cj.yaml\n```\n\n```yaml\napiVersion: batch/v1\nkind: CronJob\nmetadata:\n creationTimestamp: null\n name: echo-cj\nspec:\n jobTemplate:\n metadata:\n creationTimestamp: null\n name: echo-cj\n spec:\n template:\n metadata:\n creationTimestamp: null\n spec:\n containers:\n - image: busybox\n name: echo-cj\n resources: {}\n # 补充输出\n command: [\"/bin/echo\"]\n args: [\"hello\", \"world\"]\n restartPolicy: OnFailure\n schedule: \"*/1 * * * *\"\nstatus: {}\n```\n\n```bash\nkubectl apply -f echo-cj.yaml\nkubectl get cj\n# NAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGE\n# echo-cj */1 * * * * False 1 5s 8s\nkubectl get pod\n# NAME READY STATUS RESTARTS AGE\n# echo-cj-27942326-qng4j 0/1 Completed 0 93s\n# echo-cj-27942327-vlrvs 0/1 Completed 0 33s\n```\n\n## 14 ConfigMap Secret 管理配置信息\n\n```bash\nkubectl create cm info --from-literal=name=zhao --dry-run=client -o yaml > cm.yml\nkubectl apply -f cm.yml\nkubectl get cm\nkubectl describe cm info\n```\n\n```bash\nkubectl create secret generic user --from-literal=name=root --dry-run=client -o yaml > secret.yml\n\n# -n 去掉字符串里隐含的换行符\necho -n \"root\" | base64\nkubectl apply -f secret.yml\nkubectl get secret\nkubectl describe secret user\n```\n\n```bash\nkubectl explain pod.spec.containers.env.valueFrom\n```\n\n```yaml\napiVersion: v1\nkind: Pod\nmetadata:\n creationTimestamp: null\n labels:\n run: ngx\n name: ngx\nspec:\n containers:\n - image: nginx:alpine\n name: ngx\n resources: {}\n env:\n - name: NAME\n valueFrom:\n configMapKeyRef:\n name: info\n key: name\n - name: SNAME\n valueFrom:\n secretKeyRef:\n name: user\n key: name\n dnsPolicy: ClusterFirst\n restartPolicy: Always\nstatus: {}\n```\n\n```bash\nkubectl apply -f env-pod.yml\nkubectl exec -it ngx -- sh\n\necho $NAME $SNAME\n```\n\n```yaml\n# 以 Volume 的方式使用 ConfigMap/Secret\napiVersion: v1\nkind: Pod\nmetadata:\n name: vol-pod\nspec:\n # Volume 属于 Pod 与 containers 同级\n volumes:\n - name: cm-vol\n configMap:\n name: info\n - name: sec-vol\n secret:\n secretName: user\n containers:\n # 挂载到容器里的某个路径下\n - volumeMounts:\n - mountPath: /tmp/cm-items\n name: cm-vol\n - mountPath: /tmp/sec-items\n name: sec-vol\n image: nginx:alpine\n name: ngx\n resources: {}\n dnsPolicy: ClusterFirst\n restartPolicy: Always\nstatus: {}\n```\n\n```bash\nvim vol-pod.yml\nkubectl apply -f vol-pod.yml\nkubectl get pod\nkubectl exec -it vol-pod -- sh\n\ncat /tmp/cm-items/name\ncat /tmp/sec-items/name\n# ConfigMap 和 Secret 都变成了目录的形式,而它们里面的 Key-Value 变成了一个个的文件,而文件名就是 Key。\n```\n\n## 15 玩转 Kubernetes\n\n<img width=\"600\" alt=\"image\" src=\"https://user-images.githubusercontent.com/9289792/219537942-31e778eb-888d-4331-8d65-03ae4095b003.png\">\n\n搭建 WordPress 环境:\n\n```yaml\n# vim mariadb-pod.yml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: maria-cm\ndata:\n DATABASE: \"db\"\n USER: \"wp\"\n PASSWORD: \"123\"\n ROOT_PASSWORD: \"123\"\n\n---\napiVersion: v1\nkind: Pod\nmetadata:\n name: maria-pod\n labels:\n app: wordpress\n role: database\nspec:\n containers:\n - image: mariadb:10\n name: maria\n imagePullPolicy: IfNotPresent\n ports:\n - containerPort: 3306\n envFrom:\n - prefix: \"MARIADB_\"\n configMapRef:\n name: maria-cm\n```\n\n```bash\nkubectl apply -f mariadb-pod.yml\n# 获取 IP 地址需要加上参数 -o wide\nkubectl get pod -o wide\n# NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES\n# maria-pod 1/1 Running 0 64s 172.17.0.5 minikube <none> <none>\n```\n\n```yaml\n# vim wp-pod.yml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: wp-cm\ndata:\n # MariaDB Pod 的 IP\n HOST: \"172.17.0.5\"\n USER: \"wp\"\n PASSWORD: \"123\"\n NAME: \"db\"\n\n---\napiVersion: v1\nkind: Pod\nmetadata:\n name: wp-pod\n labels:\n app: wordpress\n role: website\nspec:\n containers:\n - image: wordpress:5\n name: wp-pod\n imagePullPolicy: IfNotPresent\n ports:\n - containerPort: 80\n envFrom:\n - prefix: \"WORDPRESS_DB_\"\n configMapRef:\n name: wp-cm\n```\n\n```bash\nkubectl apply -f wp-pod.yml\nkubectl get pod -o wide\n# NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES\n# maria-pod 1/1 Running 0 160m 172.17.0.5 minikube <none> <none>\n# wp-pod 1/1 Running 0 2m47s 172.17.0.6 minikube <none> <none>\n```\n\n```bash\n# 本地的 “8080” 映射到 WordPress Pod 的“80”\nkubectl port-forward wp-pod 8080:80 &\n# Forwarding from 127.0.0.1:8080 -> 80\n# Forwarding from [::1]:8080 -> 80\n# fg\n```\n\n## 16 初级篇总结\n\n```bash\nminikube version\nminikube status\nminikube start --kubernetes-version=v1.23.3\nminikube node list\n\nkubectl version\nkubectl run ngx --image=nginx:alpine\n\n# apiserver 等核心组件是在 kube-system 名字空间\nkubectl get pod -n kube-system\n\nkubectl api-resources\nkubectl explain pod.metadata\n\nexport out=\"--dry-run=client -o yaml\"\nkubectl run ngx --image=nginx:alpine $out > pod.yml\n\nkubectl apply -f ngx-pod.yml\nkubectl get pod\nkubectl logs ngx-pod\nkubectl exec -it ngx-pod -- sh\nkubectl delete -f ngx-pod\n\nkubectl create job echo-job --image=busybox $out\nkubectl apply -f job.yml\nkubectl get job\nkubectl get pod\nkubectl logs echo-job-l52l7\n\nkubectl create cj echo-cj --image=busybox --schedule=\"* * * * *\" $out\nkubectl apply -f cronjob.yml\nkubectl get cj\nkubectl get pod\n\nkubectl create cm info --from-literal=k=v $out\nkubectl get cm\nkubectl describe cm info\n\nkubectl create secret generic user --from-literal=name=root $out\nkubectl get secret\nkubectl describe secret user\n\necho cm9vdA== | base64 -d\n```\n\n## References\n\n- [chronolaw/k8s_study | GitHub](https://github.com/chronolaw/k8s_study)\n\n-- EOF --\n","tags":["kubernetes"]},{"title":"Go 语言第一课","url":"/2023/02/01/go-language-lesson-one/","content":"\n## 02 设计哲学\n\n设计哲学之于编程语言,就好比一个人的价值观之于这个人的行为。\n\n- 简单:Go 生产力的源泉。\n- 显式:Go 希望开发人员 明确知道自己在做什么;显式的基于值比较的错误处理方案。\n- 组合:类型嵌入(Type Embedding)。\n- 并发:面向多核、原生支持并发、用户层轻量级线程 goroutine。\n- 面向工程:将解决工程问题作为 Go 的 设计原则之一,这些问题包括:程序构建慢、依赖管理失控、代码难于理 解、跨语言构建难等。\n\n## 03 配好环境\n\n- <https://go.dev/doc/devel/release>\n- <https://golang.google.cn/dl/>\n\n### 安装多个 Go 版本\n\n```bash\ngo get golang.org/dl/go1.15.13\ngo1.15.13 download\ngo1.15.13 version\n```\n\n### 配置 Go\n\n```bash\ngo env\ngo help environment\n```\n\n## 04 Go 程序的结构\n\n- `import \"fmt\"` 一行中 `fmt` 代表的是包的导入路径(Import),它表示的是标准库下的 fmt 目录,整个 import 声明语句的含义是导入标准库 fmt 目录下的包\n- `fmt.Println` 函数调用一行中的 `fmt` 代表的则是包名。\n- 通常导入路径的最后一个分段名与包名是相同的,这也很容易让人误解 import 声明语句中的 `fmt` 指的是包名,其实并不是这样的。\n\n```php\ngofmt main.go\n```\n\n### Go module\n\n```bash\ngo mod init\ngo mod tidy\n```\n\n## 05 Go 项目的布局标准\n\nloccount 工具\n\n<https://github.com/golang/go>\n\n```bash\ntree -LF 1 .\n```\n\n## 06 解决包依赖管理\n\nGOPATH -> Vendor -> Go Module\n\n### GOPATH\n\n```bash\ngo env\n\nGOPATH=\"/Users/v_yfanzhao/go\"\n\ngo get github.com/sirupsen/logrus\n```\n\n### vendor\n\n- Go 项目必须放在 GOPATH 环境变量配置的路径下,庞大的 vendor 目录需要提交到代码仓库,不仅占用代码仓库空间,减慢仓库下载和更新的速度, 而且还会干扰代码评审,对实施代码统计等开发者效能工具也有比较大影响。\n- 你还需要手工管理 vendor 下面的 Go 依赖包,包括项目依赖包的分析、版本的记 录、依赖包获取和存放,等等,最让开发者头疼的就是这一点。\n\n### Go Module\n\nGo Module 与 go.mod 是一一对应的。go.mod 文件所在的顶层目录也被称为 module 的根目录,module 根目录以及它子目录 下的所有 Go 包均归属于这个 Go Module,这个 module 也被称为 main module。\n\n```go\npackage main\n\nimport \"github.com/sirupsen/logrus\"\n\nfunc main() {\n logrus.Println(\"hello, go module mode\")\n}\n```\n\n```bash\ngo mod init\ngo mod tidy\n```\n\n```go\nmodule go-lesson-one\n\ngo 1.17\n\nrequire github.com/sirupsen/logrus v1.9.0\n\nrequire golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect\n```\n\nmajor.minor.patch\n\nGo 的语义导入版本机制:将包主版本号引入到包导入路径中。v0、v1 时不加入路径。\n\n因此甚至可以同时依赖一个包的两个不兼容版本:\n\n```go\nimport (\n \"github.com/sirupsen/logrus\"\n logv2 \"github.com/sirupsen/logrus/v2\"\n)\n```\n\nGo 会在该项目依赖项的所有版本中,选出符合项目整体要求的“最小版本”。这与 PHP Composer 最新最大 (Latest Greatest) 版本 相反。\n\n## 07 Go Module 操作\n\n```bash\ngo list -m all\n\ngo list -m -versions github.com/sirupsen/logrus\n\n# 指定版本 升降级\ngo get github.com/sirupsen/[email protected]\n\n# 指定版本 升降级\ngo mod edit -require=github.com/sirupsen/[email protected]\ngo mod tidy\n```\n\n### 使用 vendor 机制\n\n```bash\ngo mod vendor\n\ngo build -mod=verdor\n# 顶层目录下存在 vendor 目录,那么 go build 默认也会优先基于 vendor 构建,除非:\ngo build -mod=mod\n```\n\n## 08 Go 程序的执行次序\n\n可执行程序的 main 包必须定义 main 函数,否则 Go 编译器会报错。\n\n除了 main 包外,其他包也可以拥有自己的名为 main 的函数 或方法。\n\n### init 函数\n\n除了前面讲过的 main.main 函数之外,Go 语言还有一个特殊函数,它就是用于进行包初始化的 init 函数了。main 函数之前,常量和变量初 始化之后。每个 init 函数在整个 Go 程序生命周期内仅会被执行一次。Go 包可以拥有不止一个 init 函数。\n\nGo 在进行包初始化的过程中,会采用“深度优先”的原则,递归初始化各个包的 依赖包。\n\n```bash\npackage main\n|- import pkg1\n |- import pkg2\n |- const\n |- var\n |- init()\n |- const\n |- var\n |- init()\n|- const\n|- var\n|- init()\n|- main()\n```\n\n### init 函数的用途\n\n- 重置包级变量值。被用于检查包级变量的初始状态。\n- 实现对包级变量的复杂初始化。\n- 在 init 函数中实现“注册模式”。通过在 init 函数中注册自己的实现的模式,就有效降低了 Go 包对外的直接 暴露,尤其是包级变量的暴露,从而避免了外部通过包级变量对包状态的改动。\n\n## 09 构建一个 Web 服务\n\n```go\npackage main\n\nimport (\n \"net/http\"\n)\n\nfunc main() {\n http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) {\n w.Write([]byte(\"Hello World\"))\n })\n http.ListenAndServe(\":8888\", nil)\n}\n```\n\n```bash\ncurl localhost:8888\nHello World\n```\n\n> <https://github.com/imzyf/go-bookstore>\n\n## 10 变量声明\n\n```go\nvar a int = 10\n// 将变量名放在了类型的前面\n// 修饰关键字 变量名 类型 初值\n\n// 省略类型信息的声明\nvar b = 12\n// 显式赋予变量初值\nvar b = int32(13)\n// 声明多个\nvar a, b, c = 12, 'A', \"hello\"\n\n// 短变量声明\na := 12\nb := 'A'\nc := \"hello\"\n// 声明多个\na, b, c := 12, 'A', \"hello\"\n```\n\n### Go 语言的两类变量\n\n- 包级变量 (package varible)\n- 局部变量 (local varible)\n\n### 包级变量的声明形式\n\n包级变量只能使用带有 var 关键字的变量声明形式,不能使用短变量声明形式,但在形式细节上可以有一定灵活度。\n\n```go\nvar b int32 = 17 // 显式指定类型\nvar f float32 = 3.14 // 显式指定类型\n\nvar a = 13 // 使用默认类型\nvar b = int32(17) // 显式指定类型\nvar f = float32(3.14) // 显式指定类型\n\nvar a int32\nvar f float64\n```\n\n```go\n// 声明聚类\nvar (\n netGo bool\n netCgo bool\n)\n\nvar (\n aLongTimeAgo = time.Unix(1, 0)\n noDeadline = time.Time{}\n noCancel = (chan struct{})(nil)\n)\n\n// 就近原则\n// 尽可能在靠近第一次使用变量的位置声明这个变量\n```\n\n### 局部变量的声明形式\n\n```go\n// 延迟初始化的局部变量\nvar err error\n\n// 显式初始化的局部变量\na := 17\nf := float32(3.14)\ns := []byte(\"hello, gopher!\")\n// 尽量在分支控制时使用短变量声明形式\n```\n\n## 11 代码块 Block 与作用域 Scope\n\n```go\n// 变量遮蔽\nvar a = 11\nfunc foo(n int) {\n a := 1\n a += n\n}\nfunc main() {\n fmt.Println(\"a =\", a) // 11\n foo(5)\n fmt.Println(\"after calling foo, a =\", a) // 11\n}\n```\n\n- 宇宙代码块(Universe Block)\n- 包代码块(Package Block)\n- 文件代码块(File Block)\n- 分支控制语句隐式代码块\n- switch/select 的子句隐式代码块\n\n一个标识符的作用域就是指:这个标识符在被声明后可以被有效使用的源码区域。\n\n导出标识符:\n\n- 声明在包代码块中\n- 它名字第一个字符是一个大写的 Unicode 字符\n\n> <https://github.com/imzyf/go-lesson-one/blob/main/cmd/chapter11/main.go>\n\n## 12 数值类型\n\n### 整型\n\nGo 采用补码(2's complement)作为整型的比特位编码方法。Go 的补码是通过原码逐位取反后再加 1 得到的。\n\n```bash\nunit8 1 0 0 0 0 0 0 1 = 129\nint8 1 0 0 0 0 0 0 1 = -127\n\n0 1 1 1 1 1 1 1 127\n1 0 0 0 0 0 0 0 取反\n1 0 0 0 0 0 0 1 +1 -127\n```\n\n### 整型的溢出问题\n\n> <https://github.com/imzyf/go-lesson-one/blob/main/cmd/chapter12/main.go>\n\n这个问题最容易发生在循环语句的结束条件判断中,因为这也是经常使用整型变量的地方。\n\n### 浮点型\n\n```bash\nIEEE 754\n\n符号位Sign 阶码Exponent 尾数Maintissa\n```\n\n| \\bit 位\\ | 符号位 | 阶码 | 阶码偏移值 | 尾数 |\n| -------------- | ------ | ---- | ---------- | ---- |\n| 单精度 float32 | 1 | 8 | 127 | 23 |\n| 双精度 float64 | 1 | 11 | 1023 | 52 |\n\neg:129.8125\n\n```bash\n步骤一:我们要把这个浮点数值的整数部分和小数部分,分别转换为二进制形式(后缀 d 表示十进制数,后缀 b 表示二进制数):\n\n整数部分:139d => 10001011b;\n\n小数部分:0.8125d => 0.1101b(十进制小数转换为二进制可采用“乘 2 取整”的竖式计算)。\n0.8125 * 2 = 1.625 …… 1\n0.625 * 2 = 1.25 …… 1\n0.25 * 2 = 0.5 …… 0\n0.5 * 2 = 1 …… 1\n\n139.8125d -> 10001011.1101b\n```\n\n```bash\n步骤二:移动小数点,直到整数部分仅有一个 1。\n\n10001011.1101b -> 1.00010111101b\n\n小数点向左移了 7 位,这样 指数就为 `7`,尾数为 `00010111101b`。\n```\n\n```bash\n步骤三:计算阶码。对于 float32 的单精度浮点数而言:\n阶码 = 指数 + 偏移值\n偏移值的计算公式为 2^(e-1)-1,其中 e 为阶码部分的 bit 位数,这里为 8,于是单精度浮点数的阶码偏移 值就为 2^(8-1)-1 = 127。\n\n阶码 = `7` + 127 = 134d = `10000110b`。\n```\n\n```bash\n步骤四:将符号位、阶码和尾数填到各自位置,得到最终浮点数的二进制表示\n\n符号位 0\n阶码 10000110\n尾数 00010111101 不足 23 位补零 `0_0010111101_00_0000000000`\n```\n\n`139.8125` -> `0_10000110_00010111101_000000000000`\n\n- [Go Float32bit() result not expected | stackoverflow](https://stackoverflow.com/questions/75357159/go-float32bit-result-not-expected)\n\n### 复数型\n\n矢量计算。\n\n### 创建自定义的数值类型\n\n```go\ntype MyInt int32\n\nvar m int = 5\nvar n int32 = 6\nvar a MyInt = m // error\nvar a MyInt = n // error\n\nvar a = MyInt(m) // ok\nvar a = MyInt(n) // ok\n```\n\nMyInt 类型的底层类型是 int32,所以它的数值性质与 int32 完全相同,但它 们仍然是完全不同的两种类型。\n\n### 类型别名(Type Alias)\n\n```go\ntype MyInt = int32\n\nvar n int32 = 6\nvar a MyInt = n\n```\n\n通过类型别名语法定义的新类型与原类型别无二致,可以完全相互替代。\n\n## 13 字符串类型\n\nwhy-what-how\n\n非原生字符串:\n\n- 不是原生类型,编译器不会对它进行类型校验,导致类型安全性差;\n- 字符串操作时要时刻考虑结尾的 `\\0`,防止缓冲区溢出;\n- 以字符数组形式定义的“字符串”,它的值是可变的,在并发场景中需要考虑同步问题;\n- 获取一个字符串的长度代价较大,通常是 O(n) 时间复杂度;\n- C 语言没有内置对非 ASCII 字符(如中文字符)的支持。\n\nstring 类型的数据是不可变的,提高了字符串的并发安全性和存储利用率(同一个字符串值分配同一块存储)。\n\n```go\nvar s string = \"hello\"\ns[0] = 'k' // cannot assign to s[0] (value of type byte)\ns = \"gopher\" // ok\n```\n\n没有结尾 `\\0`,而且获取长度的时间复杂度是常数时间,消除了获取字符串长度的开销。\n\n反引号原生支持“所见即所得”的原始字符串,大大降低构造多行字符串时的心智负担。\n\n对非 ASCII 字符提供原生支持,消除了源码在不同环境下显示乱码的可能。Unicode 字符是以 UTF-8 编码格式存储在内存。\n\n通过单引号括起的字符字面值:\n\n<https://github.com/imzyf/go-lesson-one/blob/main/cmd/chapter13/main.go>\n\nUTF-8 编码解决的是 Unicode 码点值在计算机中如何存储和表示(位模式)的问题。UTF-8 方案使用变长度字节,从 1 个到 4 个不等。\n\n一个 rune 存储一个 unicode 码点或 utf-32 的四字节编码;从字节视角,string 对应的底层存储存放的是 utf8 编码。\n\nGo 字符串类型的内部标示\n\n```go\n// StringHeader 是一个 string 的运行时表示\n// string 类型其实是一个“描述符”\ntype StringHeader struct {\n // 一个指向底层存储的指针\n Data uintptr\n // 字符串的长度\n Len int\n}\n```\n\n直接将 string 类型通过函数或方法参数传入也不会带来太多的开销。因为传入的仅仅是一个“描述符”,而不是真正的字符串数据。\n\n### Go 字符串类型的常见操作\n\n下标操作;下标操作,我们获取的是字符串中特定下标上的字节,而不是字符。\n\n字符迭代:\n\n- or 迭代,字节视角的迭代\n- 字符串中 Unicode 字符的码点值,以及该字符在字符串中的偏移值(字节视角)\n\n字符串连接;`+` `+=` `strings.Builder` `strings.Join` `fmt.Sprintf`。\n\n字符串比较;= =、!= 、>=、<=、> 和 <。\n\n字符串转换;string -> `[]rune` `[]byte`\n\n## 14 常量\n\n- 支持无类型常量\n- 支持隐式自动转型\n- 可用于实现枚举\n\n```go\ntype myInt int\n// 无类型常量(Untyped Constant)\nconst n = 13\n\nfunc main() {\n var a myInt = 5\n // 隐式转型\n fmt.Println(a + n)\n}\n\n// 无类型常量 + 隐式转型:使得在 Go 这样的具有强类型系统的语言,在处理表达式混合数据类型运算的时候具有比较大的灵活性,代码编写也得到了一定程度的简化。\n```\n\nGo 的 const 语法提供了“隐式重复前一个非空表达式”的机制。\n\n```go\nconst (\n Apple, Banana = iota, iota + 10 // 0, 10 (iota = 0)\n Strawberry, Grape // 1, 11 (iota = 1)\n Pear, Watermelon // 2, 12 (iota = 2)\n)\nconst (\n _ = iota // 略过 iota = 0\n IPV6_V6ONLY // 1\n SOMAXCONN // 2\n SO_ERROR // 3\n)\n```\n\n## 15 数组与切片\n\n数组是一个固定长度的、由同构类型元素组成的连续序列。不仅是逻辑上的连续序列,而且在实际内存分配时也占据着一整块内存。\n\n切片不定长同构数据类型。切片可以看成是数组的“描述符”(句柄),为数组打开了一个访问与修改的“窗口”。\n\n```go\n// 切片\ntype slice struct {\n array unsafe.Pointer // 指向底层数组的指针\n len int // 切片的长度,即切片中当前元素的个数\n cap int // 底层数组的长度,也是切片的最大容量,cap 值永远大于等于 len 值\n}\n```\n\n```go\nvar sl1 []int // 是声明,未初始化,是nil值,底层没有分配内存空间\nvar sl2 = []int{} // 初始化了,不是nil值,底层分配了内存空间,有地址。\n```\n\n## 16 map 类型\n\n一组无序的键值对。\n\n```go\nmap[key_type]value_type\n```\n\nkey 的类型必须支持“==”和“!=”两种比较操作符。\n\n## References\n\n- [The Go Playground](https://go.dev/play/)\n\n-- EOF ---\n","tags":["go"]},{"title":"回顾 2022","url":"/2022/12/31/review-2022/","content":"\n> 犯错的人不会沉浸在错误里,而是继续战斗。 —— 管泽元 S12 总决赛 DRX vs SKT 2:2\n\n-- EOF --\n","categories":["review"]},{"title":"PHP Migrating to 8.1","url":"/2022/12/21/php-migrating-to-81/","content":"\n- 实验环境:https://onlinephp.io/\n- Polyfill https://github.com/symfony/polyfill/tree/main/src\n\n## PHP8.0 to PHP8.1\n\n> https://www.php.net/manual/en/migration81.php\n>\n> https://php.watch/versions/8.1\n> brings major new features such as Enums, Fibers, never return type, Intersection Types, readonly properties, and more, while ironing out some of its undesired legacy features by deprecating them.\n\n### New Features 8.1\n\n```php\n// 八进制\nvar_dump(0o14);\n// int(12)\n```\n\n```php\n// 可解包字符串为 key 的数组\n$arr1 = [1, 'a' => 'b'];\nvar_dump([...$arr1, 'c' => 'd']);\n```\n\n```php\n// 参数解包后的命名参数\nfunction foo($a, $b, $c) {\n var_dump($a, $b, $c);\n}\nfoo(...[1,2], c: 3);\n```\n\n```php\n// Enumerations 枚举\n// https://www.php.net/manual/en/language.enumerations.examples.php\n```\n\n```php\n// Fibers 纤程\n// https://www.php.net/manual/en/language.fibers.php\n```\n\n```php\n// Intersection Types 交叉类型\n// 由类型 T、U 和 V 组成的交集类型将写为 T&U&V\n```\n\n```php\n// Never type\n```\n\n```php\n// new in Initializers\n// 现在可以使用 new ClassName() 表达式作为参数、静态变量、全局常量初始值设定项和属性参数的默认值\n// 现在也可以将对象传递给 define()\n```\n\n```php\n// Readonly properties\n// Support for readonly has been added.\n```\n\n### New Functions 8.1\n\n```php\n// 如果数组的键由从 0 到 count($array)-1 的连续数字组成,则数组被认为是 list\narray_is_list([]); // true\narray_is_list(['apple', 2, 3]); // true\narray_is_list([0 => 'apple', 'orange']); // true\n\n// The array does not start at 0\narray_is_list([1 => 'apple', 'orange']); // false\n```\n\n-- EOF --\n","tags":["php"]},{"title":"PHP Migrating to 7.4 8.0","url":"/2022/12/14/php-migrating-to-74-80/","content":"\n- 实验环境:<https://onlinephp.io/>\n- Polyfill <https://github.com/symfony/polyfill/tree/main/src>\n\n## PHP7.3 to PHP7.4\n\n> <https://www.php.net/manual/en/migration74.php>\n>\n> <https://php.watch/versions/7.4>\n> PHP 7.4, the final release in the PHP 7.x series. PHP 7.4 brings typed properties, underscore numeric separator, and other minor improvements to PHP.\n\n### New Features 7.4\n\n```php\n// Typed properties\nclass User {\n public int $id; // 会强制要求 $user->id 只能为 int 类型,访问前必须进行处理化,?int 也要进行初始化\n}\n```\n\n```php\n// 箭头函数 Arrow functions\n$factor = 10;\n$nums = array_map(fn($n) => $n * $factor, [1, 2, 3, 4]);\n```\n\n```php\n// 有限的 Limited 返回类型协变和参数类型逆变\nclass A {}\nclass B extends A {}\nclass Producer {\n public function method(): A {}\n}\nclass ChildProducer extends Producer {\n public function method(): B {}\n}\n// PHP74\n// ok\n//\n// PHP73\n// Fatal error: Declaration of ChildProducer::method(): B must be compatible with Producer::method(): A\n```\n\n```php\n// 空合并赋值运算符\n$array['key1'] ??= 1;\n// is roughly equivalent to\nif (!isset($array['key2'])) {\n $array['key2'] = 2;\n}\nvar_dump($array);\n// array(2) {\n// [\"key1\"]=>\n// int(1)\n// [\"key2\"]=>\n// int(2)\n// }\n```\n\n```php\n// Unpacking inside arrays\n// 可以平替 array_merge\n$parts = ['apple', 'pear'];\n$fruits = ['orange', ...$parts, 'watermelon'];\nvar_dump($fruits);\n// array(4) {\n// [0]=>\n// string(6) \"orange\"\n// [1]=>\n// string(5) \"apple\"\n// [2]=>\n// string(4) \"pear\"\n// [3]=>\n// string(10) \"watermelon\"\n// }\n```\n\n```php\n// 数字文字分隔符\n6.674_083e-11; // float\n299_792_458; // decimal\n0xCAFE_F00D; // hexadecimal\n0b0101_1111; // binary\n\nvar_dump((int)\"1_123\");\n// int(1)\n```\n\n```php\n// WeakReference 类\n// https://www.php.net/manual/zh/class.weakreference.php\n$obj = new stdClass;\n$weakref = WeakReference::create($obj);\nvar_dump($weakref->get());\nunset($obj);\nvar_dump($weakref->get());\n// object(stdClass)#1 (0) {\n// }\n// NULL\n```\n\n### Backward Incompatible Changes 7.4\n\n```php\n// 以数组形式访问非数组,将会抛出 notic\n// null, bool, int, float or resource\n$i = 12;\n$i[\"a\"];\n// PHP74\n// Notice: Trying to access array offset on value of type int\n//\n// PHP80\n// Warning: Trying to access array offset on value of type int\n//\n// PHP73 ok\n```\n\n### Deprecated Features 7.4\n\n```php\n// 嵌套的三元运算必须明确地使用括号来指示运算的顺序\nvar_dump(1 ? 2 : 3 ? 4 : 5);\n// PHP74\n// Deprecated: Unparenthesized `a ? b : c ? d : e` is deprecated. Use either `(a ? b : c) ? d : e` or `a ? b : (c ? d : e)`\n// int(4)\n```\n\n## PHP7.4 to PHP8.0\n\n> <https://www.php.net/manual/en/migration80.php>\n\n### New Features 8.0\n\n它为类型系统、语法、错误处理、字符串、面向对象编程等带来了多项新功能。\n\n```php\n// Named Parameters 命名参数\n// https://www.php.net/manual/zh/functions.arguments.php#functions.named-arguments\n// array_fill(int $start_index, int $count, mixed $value): array\nvar_dump(array_fill(value: 50, count: 3, start_index: 0));\n// PHP80\n// array(3) {\n// [0]=>\n// int(50)\n// [1]=>\n// int(50)\n// [2]=>\n// int(50)\n// }\n\n// Argument Unpacking\necho str_replace(...[\n 'replace' => 'Iron Man',\n 'search' => 'Inevitable',\n 'subject' => 'I am Inevitable',\n]);\n\n// Named Parameters with Variadic Parameters\nfunction test($a, $b, ...$args) {\n var_dump($args);\n}\ntest(28, 15, june: 6, september: 15, january: \"01\");\n// array(3) {\n// [\"june\"]=> int(6)\n// [\"september\"]=> int(15)\n// [\"january\"]=> string(2) \"01\"\n// }\n```\n\n```php\n// JIT Just-In-Time Compilation\n// php -i | grep -i opcache\n// php -d opcache.enable_cli=1 -d opcache.jit_buffer_size=256M -d opcache.jit=tracing -i | grep -i jit\n\n// opcache.enable=1\n// opcache.enable_cli=1\n// opcache.jit_buffer_size=256M\n\n<?php\nfunction fibonacci($n) {\n if ($n < 2) return $n;\n return fibonacci($n - 1) + fibonacci($n - 2);\n}\n\n$start = microtime(true);\n\nfibonacci(42);\n\n$end = microtime(true);\necho \"Time taken: \" . ($end - $start) . \" seconds\\n\";\n\n// php -d opcache.enable_cli=1 -d opcache.jit_buffer_size=256M -d opcache.jit=tracing test.php\n```\n\n```php\n// 注解(Attributes)\n// https://www.php.net/manual/zh/language.attributes.php\n// 定义注解 -> 使用注解 -> (通过反射)提取注释\n// 注意以后用词:Attributes 注解,Properties 属性\n#[\\Attribute]\nclass JsonSerialize {\n public function __construct(public ?string $fieldName = null) {\n var_dump(\"JsonSerialize:$fieldName\");\n }\n}\nclass VersionedObject {\n #[JsonSerialize('json-foobar')]\n protected string $myValue = '';\n}\n$object = new VersionedObject();\n$reflection = new ReflectionObject($object);\nforeach ($reflection->getProperties() as $reflectionProp) {\n foreach ($reflectionProp->getAttributes(JsonSerialize::class) as $jsonSerializeAttr) {\n var_dump($jsonSerializeAttr->getName());\n var_dump($jsonSerializeAttr->getArguments());\n var_dump($jsonSerializeAttr->getTarget());\n var_dump($jsonSerializeAttr->isRepeated());\n var_dump($jsonSerializeAttr->newInstance());\n }\n}\n// string(13) \"JsonSerialize\"\n//\n// array(1) {\n// [0]=>\n// string(11) \"json-foobar\"\n// }\n//\n// int(8) Attribute::TARGET_PROPERTY\n//\n// bool(false)\n//\n// string(25) \"JsonSerialize:json-foobar\"\n// object(JsonSerialize)#5 (1) {\n// [\"fieldName\"]=>\n// string(11) \"json-foobar\"\n// }\n```\n\n```php\n// 构造器属性提升\n// 当构造器参数带访问控制(visibility modifier)时,PHP 会同时把它当作对象属性和构造器参数,并赋值到属性\nclass Point {\n public function __construct(protected int $x, protected int $y = 0) {\n }\n}\nvar_dump(new Point(2,4));\n// object(Point)#1 (2) {\n// [\"x\":protected]=>\n// int(2)\n// [\"y\":protected]=>\n// int(4)\n// }\nclass Point {\n public function __construct(int $x, int $y = 0) {\n $this->x = $x;\n $this->y = $y;\n }\n}\nvar_dump(new Point(2,4));\n// object(Point)#1 (2) {\n// [\"x\"]=>\n// int(2)\n// [\"y\"]=>\n// int(4)\n// }\n\nclass Point2 {\n protected int $x;\n protected int $y;\n public function __construct(protected int $x, protected int $y = 0) {}\n}\n// 此类中已经定义了具有相同名称的字段\n// Fatal error: Cannot redeclare Point::$x\n```\n\n```php\n// Union types 联合类型\n// ?T is same T|null\n//\n// - 类型只能出现一次 int|string|INT 否则报错\n// - 使用 mixed 会报错\n// - bool 与 false or true 不能混用\n// - object 与 class 不能混用\n// - iterable 与 array Traversable 不能混用\n// - 使用 self、parent 或 static 都会导致错误\n```\n\n```php\n// match 表达式\n// https://www.php.net/manual/en/control-structures.match.php\n// match 表达式必须是详尽,则会抛出 UnhandledMatchError。\n$food = 'cake';\n$return_value = match ($food) {\n 'apple' => 'This food is an apple',\n 'bar' => 'This food is a bar',\n 'cake' => 'This food is a cake',\n};\nvar_dump($return_value);\n// string(19) \"This food is a cake\"\n```\n\n```php\n// Nullsafe 方法和属性\n// As of PHP 8.0.0, this line:\n$result = $repository?->getUser(5)?->name;\n// Is equivalent to the following code block:\nif (is_null($repository)) {\n $result = null;\n} else {\n $user = $repository->getUser(5);\n if (is_null($user)) {\n $result = null;\n } else {\n $result = $user->name;\n }\n}\n```\n\n```php\n// The WeakMap class has been added, accepts objects as keys, similar SplObjectStorage\n// https://www.php.net/manual/en/class.weakmap.php\n// https://www.php.net/manual/en/class.splobjectstorage.php\n// final class WeakMap implements ArrayAccess, Countable, IteratorAggregate\n$wm = new WeakMap();\n$o = new StdClass;\nclass A {\n public function __destruct() {\n echo \"Dead!\\n\";\n }\n}\n$wm[$o] = new A;\nvar_dump(count($wm));\n// int(1)\nunset($o);\n// Dead!\n// 销毁\nvar_dump(count($wm));\n// int(0)\n\n\n// 对比 SplObjectStorage\n$wm = new SplObjectStorage();\n$o = new StdClass;\nclass A {\n public function __destruct() {\n echo \"Dead!\\n\";\n }\n}\n$wm[$o] = new A;\nvar_dump(count($wm));\n// int(1)\nunset($o);\n// 未销毁\nvar_dump(count($wm));\n// int(1)\n```\n\n```php\n// 只要类型兼容,现在可以将任意数量的函数参数替换为可变参数 variadic argument\nclass A {\n public function method(int $many, string $parameters, $here) {}\n}\nclass B extends A {\n public function method(...$everything) {}\n}\n// OK\nclass C extends A {\n public function method(int ...$everything) {}\n}\n// Fatal error: Declaration of C::method(int ...$everything)\n```\n\n```php\n// static 后期静态绑定,现在可以用作返回类型\nclass Test {\n public function create(): static {\n return new static();\n }\n}\n```\n\n```php\n// get_class($obj) === $obj::class\nclass Test {}\n$obj = new Test;\nvar_dump($obj::class);\nvar_dump(get_class($obj));\n```\n\n```php\n// new instanceof 可以与任意表达式一起使用\n// new (expression)(...$args)\n// $obj instanceof (expression)\nclass A{};\nclass B{};\nnew (1>2 ? \"A\" : \"B\")();\n```\n\n```php\n// 新增 Stringable 接口。如果 class 定义了 __toString(),则自动实现了此接口\n```\n\n```php\n// Traits can define abstract private methods\ntrait T {\n abstract private function a();\n}\n```\n\n```php\n// throw 可以作为一个表达式 as an expression\n$fn = fn() => throw new Exception('Exception in arrow function');\n```\n\n```php\n// 参数列表中现在允许使用可选的尾随逗号\nfunction functionWithLongSignature(\n Type1 $parameter1,\n Type2 $parameter2, // <-- This comma is now allowed.\n) {}\n```\n\n```php\n// 允许 catch (Exception) 无需存储到变量\ntry {}\ncatch (Exception) {}\n```\n\n```php\n// 在父类上声明的私有方法不再对子类的方法强制执行任何继承规则(私有构造函数除外)\nclass ParentClass {\n private function method1() {}\n private function method2() {}\n private static function method3() {}\n}\nabstract class ChildClass extends ParentClass {\n public abstract function method1();\n public static function method2() {}\n public function method3() {}\n}\n// OK\n```\n\n### Backward Incompatible Changes 8.0\n\n```php\n// 字符串与数字比较\n// 0 == \"not-a-number\" is false\n// 将数字转换为字符串,并使用字符串比较\nvar_dump(0 == \"foo\");\nvar_dump(42 == \"42foo\");\nvar_dump(0 == \"\");\n// PHP80\n// bool(false)\n// bool(false)\n// bool(false)\n//\n// PHP74\n// bool(true)\n// bool(true)\n// bool(true)\nvar_dump(\"\" < 0);\nvar_dump(\"\" < -1);\nvar_dump(\"a\" < 0);\nvar_dump(\"b\" < -1);\nvar_dump(\"ab\" > 0);\n// PHP80\n// bool(true)\n// bool(true)\n// bool(false)\n// bool(false)\n// bool(true)\n//\n// PHP74\n// bool(false)\n// bool(false)\n// bool(false)\n// bool(false)\n// bool(false)\n```\n\n```php\n// match is 保留关键字\n```\n\n```php\n// is_callable() 在检查具有 classname 的 non-static 方法时将 false(必须检查对象实例)\nclass Test {\n public function method1() {}\n}\nvar_dump(is_callable([Test::class, 'method1']));\nvar_dump(is_callable([new Test, 'method1']));\n// PHP80\n// bool(false)\n// bool(true)\n//\n// PHP74\n// bool(true)\n// bool(true)\n```\n\n```php\n// __autoload() function has been removed\n```\n\n```php\n// 删除了对 object 使用 array_key_exists() 的能力。isset() or property_exists() may be used instead\n```\n\n-- EOF --\n","tags":["php"]},{"title":"PHP Migrating to 7.2 7.3","url":"/2022/12/12/php-migrating-to-72-73/","content":"\n- 实验环境:https://onlinephp.io/\n- Polyfill https://github.com/symfony/polyfill/tree/main/src\n\n## PHP7.1 to PHP7.2\n\n> https://www.php.net/manual/en/migration72.php\n>\n> Argon2 password hashing support, class constant visibility, object type, and many more.\n\n### New Features 7.2\n\n```php\n// 新的 object 类型\n// 可用于逆变(contravariant)参数输入和协变(covariant)返回任何对象类型\n// https://www.php.net/manual/zh/language.oop5.variance.php\n// 协变使子类比父类方法能返回更具体的类型;逆变使子类比父类方法参数类型能接受更模糊的类型\nfunction test(object $obj) : object\n{\n return new SplQueue();\n}\ntest(new StdClass());\n```\n\n```php\n// 抽象类可以重写被继承的抽象类的抽象方法\nabstract class A\n{\n abstract function test(string $s);\n}\nabstract class B extends A\n{\n // overridden - 仍然保持参数的逆变和返回的逆变\n abstract function test($s): int;\n}\n```\n\n```php\n// 重写方法和接口实现的参数类型可以省略\n// 仍然是符合LSP,这种参数类型是逆变\ninterface A\n{\n public function test(array $input);\n}\nclass B implements A\n{\n // type omitted for $input\n public function test($input){\n return $input;\n }\n}\nvar_dump((new B())->test(1));\n// PHP72\n// int(1)\n//\n// PHP71\n// Fatal error: Declaration of B::test($input) must be compatible with A::test(array $input)\n```\n\n### Backward incompatible changes 7.2\n\n```php\n// 防止 number_format() 返回负零\nvar_dump(number_format(-0.01));\n// PHP72\n// string(1) \"0\"\n//\n// PHP71\n// string(2) \"-0\"\n```\n\n```php\n// 转换对象和数组中的数字键\n$arr = [0 => 1];\n$obj = (object) $arr;\nvar_dump($obj);\nvar_dump($obj->{'0'}, // 新写法\n $obj->{0} // 新写法\n);\n// PHP72\n// object(stdClass)#1 (1) {\n// [\"0\"]=>\n// int(1)\n// }\n// int(1)\n// int(1)\n//\n// PHP71\n// object(stdClass)#1 (1) {\n// [0]=>\n// int(1)\n// }\n// Notice: Undefined property: stdClass::$0\n\n\n$obj = new class {\n public function __construct()\n {\n $this->{1} = \"value\";\n }\n};\n$arr = (array) $obj;\nvar_dump($arr);\nvar_dump($arr[\"1\"]); // 整数 或者 字符串整数 含义相同\nvar_dump($arr[1]); // PHP71\nvar_dump($arr[0]);\n// PHP72\n// array(1) {\n// [1]=>\n// string(2) \"my\"\n// }\n//\n// PHP71 无法取整型字符串 key\n// array(1) {\n// [\"1\"]=>\n// string(2) \"my\"\n// }\n// Notice: Undefined offset: 1\n// Notice: Undefined offset: 1\n// Notice: Undefined offset: 0\n\n$a = [1.3 => \"v1\", 1.4 => \"v2\"];\nvar_dump($a);\n// ALL array key float 会被转换为 int\n// array(1) {\n// [1]=>\n// string(2) \"v2\"\n// }\n```\n\n```php\n// get_class() 函数不再接受 null\nvar_dump(get_class(null));\n// PHP72\n// Warning: get_class() expects parameter 1 to be object\n// bool(false)\n//\n// PHP71\n// Warning: get_class() called without object from outside a class\n// bool(false)\n//\n// PHP80\n// Fatal error: Uncaught TypeError: get_class(): Argument #1 ($object) must be of type object, null given\n```\n\n```php\n// 计算非可数类型(non-countable)时发出警告\nvar_dump(\n count(null), // NULL is not countable\n count(1), // integers are not countable\n count('abc'), // strings are not countable\n count(new stdclass) // objects not implementing the Countable interface are not countable\n);\n// PHP72\n// Warning: count(): Parameter must be an array or an object that implements Countable\n//\n// PHP71\n// 无 Warning\n//\n// PHP80\n// Fatal error: Uncaught TypeError: count(): Argument #1 ($value) must be of type Countable|array\n```\n\n```php\n// 调用未定义的常量,现在会抛出一个 E_WARNING 错误(之前版本中为 E_NOTICE))\n// PHP8 将不会转化成他们自身的字符串,同时抛出 Error 异常\nvar_dump(MY_CONST);\n// PHP72\n// Warning: Use of undefined constant MY_CONST - assumed 'MY_CONST'\n// string(8) \"MY_CONST\"\n//\n// PHP71\n// Notice: Use of undefined constant MY_CONST\n// string(8) \"MY_CONST\"\n//\n// PHP80\n// Fatal error: Uncaught Error: Undefined constant \"MY_CONST\"\n```\n\n```php\n// bcmod 任意精度数字取模,添加新增参数 scale\nvar_dump(bcmod(\"4\", \"3.5\"));\n// PHP72\n// string(1) \"0\"\n//\n// PHP71\n// string(1) \"1\"\nvar_dump(bcmod(\"4\", \"3.5\", 1));\n// PHP72\n// string(3) \"0.5\"\n//\n// PHP71\n// Warning: bcmod() expects exactly 2 parameters, 3 given\n```\n\n```php\n// json_decode associative 允许为 null\n// 当为 true 时,JSON 对象将返回关联 array;当为 false 时,JSON 对象将返回 object。\n// 当为 null 时,JSON 对象将返回关联 array 或 object,这取决于是否在 flags 中设置 JSON_OBJECT_AS_ARRAY\n// https://www.php.net/manual/zh/function.json-decode.php\n$json = '{\"a\":1,\"b\":2}';\nvar_dump(json_decode($json, null, 512, JSON_OBJECT_AS_ARRAY));\n// PHP72\n// array(2) {\n// [\"a\"]=>\n// int(1)\n// [\"b\"]=>\n// int(2)\n// }\n//\n// PHP71\n// object(stdClass)#1 (2) {\n// [\"a\"]=>\n// int(1)\n// [\"b\"]=>\n// int(2)\n// }\n```\n\n## PHP7.2 to PHP7.3\n\n> https://www.php.net/manual/en/migration73.php\n>\n> https://php.watch/versions/7.3\n> Heredoc/nowdoc syntax improvements and a bunch of legacy code deprecations.\n\n### New Features 7.3\n\n```php\n// Heredoc Nowdoc 不再需要后跟分号或换行符\n// 结束标记可以缩进,结束时所引用的标识符必须在该行的第一列\n$values = [<<<END\na\n b\n c\nEND, 'd e f'];\nvar_dump($values);\n// PHP73\n// array(2) {\n// [0]=>\n// string(13) \"a\n// b\n// c\"\n// [1]=>\n// string(5) \"d e f\"\n// }\n//\n// PHP72\n// Parse error: syntax error, unexpected end of file\n\necho <<<END\n a\n b\n c\n END;\n// PHP73\n// a\n// b\n// c\n//\n// PHP72\n// Parse error: syntax error, unexpected end of file, expecting variable (T_VARIABLE) or heredoc end (T_END_HEREDOC) or ${ (T_DOLLAR_OPEN_CURLY_BRACES) or {$ (T_CURLY_OPEN)\n\necho <<<END\n a\n b\nc\n END;\n// PHP73\n// Parse error: Invalid body indentation level\n```\n\n```php\n// 数组解构支持引用赋值\n$arr = [[1], [1]];\n[&$a, $b] = $arr;\n$a[] = 2;\n$b[] = 3;\nvar_dump($arr);\n// PHP73\n// array(2) {\n// [0]=>\n// &array(2) {\n// [0]=>\n// int(1)\n// [1]=>\n// int(2)\n// }\n// [1]=>\n// array(1) {\n// [0]=>\n// int(1)\n// }\n// }\n//\n// PHP72\n// Fatal error: [] and list() assignments cannot be by reference\n```\n\n```php\n// 允许将 literals 作为第一个操作数,always false\nvar_dump(true instanceof stdClass);\n// PHP73\n// bool(false)\n//\n// PHP72\n// Fatal error: instanceof expects an object instance, constant given\n```\n\n```php\n// 调用中允许尾随逗号\nfunction my($v) {\n echo \"php-{$v}\";\n}\nmy(73,);\n// PHP73\n// php-73\n//\n// PHP72\n// Parse error: syntax error, unexpected ')'\n```\n\n### New Functions 7.3\n\n```php\n// Gets the first key of an array\narray_key_first(array $array): int|string|null\n// Gets the last key of an array\narray_key_last(array $array): int|string|null\n// Get the system's high resolution time\n// [seconds, nanoseconds] int(64 位平台)或 float(32 位平台)\nhrtime(bool $as_number = false): array|int|float|false\n// 验证变量的内容是否为 countable 值\n// return is_array($value) || $value instanceof Countable || $value instanceof ResourceBundle || $value instanceof SimpleXmlElement;\nis_countable(mixed $value): bool\n```\n\n### Backward Incompatible Changes 7.3\n\n```php\n// Continue Targeting Switch 问题警告\n$foo = 0;\nwhile ($foo < 10) {\n switch ($foo) {\n case \"baz\":\n continue;\n }\n $foo++;\n}\n// PHP73\n// Warning: \"continue\" targeting switch is equivalent to \"break\". Did you mean to use \"continue 2\"?\n//\n// PHP72\n// ok\n```\n\n```php\n// $obj[\"123\"] 类型的数组访问,其中 $obj 实现 ArrayAccess 且 \"123\" 是整数字符串文字将不再导致隐式转换为整数\n// 数组的行为不会受到任何影响,它们继续将整数字符串键隐式转换为整数\nclass A implements \\ArrayAccess\n{\n public function offsetExists($offset)\n {\n }\n public function offsetGet($offset)\n {\n var_dump($offset);\n }\n public function offsetSet($offset, $value)\n {\n }\n public function offsetUnset($offset)\n {\n }\n}\n$a = new A();\n$a[\"123\"];\n// PHP73\n// string(3) \"123\"\n//\n// PHP72\n// int(123)\n```\n\n-- EOF --\n","tags":["php"]},{"title":"PHP Migrating to 7.0 7.1","url":"/2022/11/28/php-migrating-to-70-71/","content":"\n- 实验环境:https://onlinephp.io/\n- Polyfill https://github.com/symfony/polyfill/tree/main/src\n\n## PHP7.0 to PHP7.1\n\n> https://www.php.net/manual/en/migration71.php\n\n### 7.1 New Features\n\n```php\n// Nullable types 可为空类型\nfunction test(?string $name): ?string\n```\n\n```php\n// Void 函数\nfunction swap(&$left, &$right) : void\n// 获取一个 void 方法的返回值会得到 null,并且不会产生任何警告\n```\n\n```php\n// 对称数组解构\n$data = [\n [1, 'Tom'],\n [2, 'Fred'],\n];\n[$id, $name] = $data[0];\n// 赋值部分\n[, $name1] = $data[1];\n\nforeach ($data as [$id, $name]) {\n // logic here with $id and $name\n}\n\nfunction swap(&$a, &$b): void\n{\n [$a, $b] = [$b, $a];\n}\n```\n\n```php\n// list 支持键名\n$data = [\n [\"id\" => 1, \"name\" => 'Tom'],\n [\"id\" => 2, \"name\" => 'Fred'],\n];\nforeach ($data as [\"name\" => $name, \"id\" => $id]) {\n // logic here with $id and $name\n}\n```\n\n```php\n// 类常量支持可见性\nclass ConstDemo\n{\n public const PUBLIC_CONST_A = 1;\n}\n```\n\n```php\n// iterable 伪类(与 callable 类似)\ninterface Traversable extends iterable {\n // 这个接口没有任何方法,它的作用仅仅是作为所有可遍历类的基本接口\n // Traversable as part of either Iterator or IteratorAggregate,不能直接实现\n}\n\nfunction iterator(iterable $iter)\n{\n foreach ($iter as $val) {\n // ...\n }\n}\n```\n\n```php\n// 多异常捕获处理,可以通过管道字符(|)来实现多个异常的捕获\ntry {\n // some code\n} catch (FirstException | SecondException $e) {\n // handle first and second exceptions\n}\n```\n\n```php\n// 支持为负的字符串偏移量,一个负数的偏移量会被理解为一个从字符串结尾开始的偏移量\n// 所有支持偏移量的字符串操作函数,都支持接受负数作为偏移量\n// 中文操作要小心\nvar_dump(\"abcdef\"[-2]); // e\nvar_dump(strpos(\"aabbcc\", \"b\")); // 2\nvar_dump(strpos(\"aabbcc\", \"b\", -3)); // 3\n```\n\n```php\n// 新增 Closure::fromCallable(),将 callable 转为一个 Closure 对象\n// public static Closure::fromCallable(callable $callback): Closure\nclass Test\n{\n public function exposeFunction()\n {\n return Closure::fromCallable([$this, 'privateFunction']);\n }\n private function privateFunction($param)\n {\n var_dump($param);\n }\n}\n\n$privFunc = (new Test)->exposeFunction();\n$privFunc('some value');\n// string(10) \"some value\"\n```\n\n### 7.1 New Functions\n\n```php\n// 否为可迭代\nis_iterable()\n```\n\n### 7.1 Backward Incompatible Changes\n\n```php\n// 当传递参数过少时将抛出错误\nfunction test($param){}\ntest();\n/*\nPHP71\nFatal error: Uncaught ArgumentCountError: Too few arguments to function test()\n\nPHP70\nWarning: Missing argument 1 for test()\n*/\n```\n\n```php\n// 禁止动态调用作用域自检函数\n// $func() or array_map('extract', ...)\n// function array_map($callback, array $array, array ...$arrays): array\n(function () {\n $func = 'func_num_args';\n $func();\n})();\n/*\nPHP71\nWarning: Cannot call func_num_args() dynamically\n*/\n```\n\n```php\n// 以下名称不能用于命名 class, interface, trait\n// - void\n// - iterable\n```\n\n```php\n// 数字字符串转换 遵循科学记数法\nvar_dump(intval('1e5'));\nvar_dump(is_numeric('1e5'));\nvar_dump(is_numeric('e'));\n/*\nPHP71\nint(100000)\nbool(true)\nbool(false)\n\n\nPHP70\nint(1)\nbool(true)\nbool(false)\n*/\n```\n\n```php\n// call_user_func() 不再支持对传址的函数的调用\nerror_reporting(E_ALL);\nfunction increment(&$var)\n{\n $var++;\n}\n\n$a = 0;\ncall_user_func('increment', $a); // ...expected to be a reference, value given\ncall_user_func_array('increment', [&$a]); // 1\n$increment = 'increment';\n$increment($a); // 2\n```\n\n```php\n// 字符串不再支持空索引运算符\n$str = \"\";\n$str[] = \"abc\";\nvar_dump($str);\n/*\nPHP71\nFatal error: Uncaught Error: [] operator not supported for strings\n\nPHP70\narray(1) {\n [0]=>\n string(3) \"abc\"\n}\n*/\n\n$a = \"\";\n$a[0] = \"hello world\";\nvar_dump($a);\n/*\nPHP71\nstring(1) \"h\"\n\nPHP70\narray(1) {\n [0]=>\n string(11) \"hello world\"\n}\n*/\n```\n\n```php\n// 通过空字符串上的字符串索引访问赋值\n$str = '';\n$str[10] = 'foo';\nvar_dump($str);\n/*\nPHP71\nstring(11) \" f\"\n\nPHP70\narray(1) {\n [10]=>\n string(3) \"foo\"\n}\n*/\n```\n\n```php\n// 当通过引用赋值引用它们自动创建这些元素时,数组中元素的顺序已更改。\n$array = [];\n$array['a'] = &$array['b'];\n$array['b'] = 1;\nvar_dump($array);\n/*\nPHP71\narray(2) {\n [\"b\"]=>\n &int(1)\n [\"a\"]=>\n &int(1)\n}\n\nPHP70\narray(2) {\n [\"a\"]=>\n &int(1)\n [\"b\"]=>\n &int(1)\n}\n*/\n```\n\n```php\n// 词法绑定变量不能重用名称,以下都将 fatal error\n$f = function () use ($_SERVER) {}; // any superglobals\n$f = function () use ($this) {};\n$f = function ($param) use ($param) {};\n```\n\n```php\n// 禁止 \"return;\" 对于已经在编译时键入的返回值\nfunction test(int $i): array\n{\n $i += 1;\n return;\n}\n// PHP Compile Error A function with return type must return a value\n```\n\n```php\nvar_dump('1b' + 'something');\n/*\nPHP71\nNotice: A non well formed numeric value encountered\nWarning: A non-numeric value encountered\nint(1)\n\nPHP70\nint(1)\n*/\n```\n\n## PHP5.6 to PHP7.0\n\n> https://www.php.net/manual/en/migration70.php\n\n### Backward incompatible changes\n\n```php\n// 错误和异常处理相关的变更\n// set_exception_handler() 不再保证收到的一定是 Exception 对象\nfunction handler(Exception $e) { ... }\nset_exception_handler('handler');\n\n// 兼容 PHP 5 和 7\nfunction handler($e) { ... }\n\n// 仅支持 PHP 7\nfunction handler(Throwable $e) { ... }\n\n// 当内部构造器失败的时候,总是抛出异常\n```\n\n```php\n// 间接调用的表达式的新旧解析顺序\n// 现在严格遵循从左到右的顺序来解析\n// 表达式 PHP 5 的解析方式 PHP 7 的解析方式\n$$foo['bar']['baz'] ${$foo['bar']['baz']} ($$foo)['bar']['baz']\n$foo->$bar['baz'] $foo->{$bar['baz']} ($foo->$bar)['baz']\n```\n\n```php\n// foreach 通过值遍历时,操作的值为数组的副本\n$array = [0];\nforeach ($array as $val) {\n var_dump($val);\n $array[1] = 1;\n}\n// int(0)\n\n// foreach通过引用遍历时,有更好的迭代特性\n$array = [0];\nforeach ($array as &$val) {\n var_dump($val);\n $array[1] = 1;\n}\n// PHP 7.0\n// int(0)\n// int(1)\n\n// PHP 5.6\n// int(0)\n```\n\n```php\n// call_user_method() and call_user_method_array() 被移除。\n// 应该使用 call_user_func() 和 call_user_func_array()\n```\n\n```php\n// 在函数中检视参数值会返回 当前 的值\nfunction foo($x) {\n $x++;\n var_dump(func_get_arg(0));\n}\nfoo(1);\n// int(2) PHP 7.0\n// int(1) PHP 5.6\n```\n\n### 7.0 New Features\n\n```php\n// 标量类型声明\n// 返回值类型声明\nfunction arraysSum(array ...$arrays): array\n{\n return array_map(function(array $array): int {\n return array_sum($array);\n }, $arrays);\n}\n```\n\n```php\n// null 合并运算符,对 isset() 的简化\n$username = $_GET['user'] ?? 'nobody';\n\n// 太空船操作符(组合比较符)\necho 1 <=> 1; // 0\necho 1 <=> 2; // -1\necho 2 <=> 1; // 1\n```\n\n```php\n// 通过 define() 定义 常量数组\ndefine('ANIMALS', [\n 'dog',\n 'cat',\n 'bird'\n]);\n```\n\n```php\n// 匿名类 用来替代一些“用后即焚”的完整类定义\n$app = new Application;\n$app->setLogger(new class implements Logger {\n public function log(string $msg) {\n echo $msg;\n }\n});\n```\n\n```php\n// Unicode codepoint 转译语法\n// 这接受一个以 16 进制形式的 Unicode codepoint\necho \"\\u{9999}\"; // 香\n```\n\n```php\n// Closure::call() 新方法,简化绑定一个方法到对象上闭包并调用它\n// PHP 7 之前版本的代码\n$getXCB = function() {return $this->x;};\n// function bindTo($newThis, $newScope = 'static') { }\n// $newScope 将闭包关联到的类作用域\n$getX = $getXCB->bindTo(new A, A::class); // 中间层闭包\necho $getX();\n\n// PHP 7+ 及更高版本的代码\n$getX = function() {return $this->x;};\necho $getX->call(new A);\n```\n\n```php\n// 生成器委托 yield from\nfunction gen()\n{\n yield 1;\n yield 2;\n\n yield from gen2();\n}\nfunction gen2()\n{\n yield 3;\n yield 4;\n}\nforeach (gen() as $val)\n{\n echo $val, PHP_EOL;\n}\n// 1\n// 2\n// 3\n// 4\n```\n\n```php\n// 柯里化\nfunction add($a) {\n return function($b) use ($a) {\n return $a + $b;\n };\n}\n$result = add(10)(15);\nvar_dump($result); // int 25\n```\n\n### Deprecated features in PHP 7.0.x\n\n```php\n// 构造函数(方法名和类名一样)将被弃用,并在将来移除\nclass Foo {\n function foo() {\n echo 'I am the constructor';\n }\n}\n// PHP 7.0: Deprecated: Methods with the same name as their class will not be constructors in a future version of PHP\n// PHP 5.6: OK\n```\n\n### Other Changes\n\n```php\n// 放宽了保留词限制,以前不能用 'new'、'private' 和 'for'\nProject::new('Project Name')->private()->for('purpose here')->with('username here');\n// class 关键词不能用于常量名,否则会和类名解析语法冲突(ClassName::class)\n```\n\n### New Global Constants\n\n```php\nPHP_INT_MIN\n```\n\n-- EOF --\n","tags":["php"]},{"title":"通过 ondrej/php PPA 管理多个 PHP 版本","url":"/2022/11/18/ondrej-multiversion-php/","content":"\nhttps://ostechnix.com/how-to-switch-between-multiple-php-versions-in-ubuntu/\n\n## References\n\n- [Managing Multiple PHP versions via the ondrej/php PPA](https://mwop.net/blog/2019-04-30-ondrej-multiversion-php.html)\n- [update-alternatives(1) — Linux manual page](https://man7.org/linux/man-pages/man1/update-alternatives.1.html)\n","tags":["php"]},{"title":"Docker Code Snippet","url":"/2022/11/17/docker-code-snippet/","content":"\n## Dockerfile\n\n### ARG 构建参数\n\n`ARG <参数名>[=<默认值>]`\n\n该默认值可以在构建命令 docker build 中用 --build-arg <参数名>=<值> 来覆盖。\n\n```dockerfile\nARG LARADOCK_PHP_VERSION\nARG BASE_IMAGE_TAG_PREFIX=latest\nFROM laradock/workspace:${BASE_IMAGE_TAG_PREFIX}-${LARADOCK_PHP_VERSION}\n\nLABEL maintainer=\"Mahmoud Zalt <[email protected]>\"\n\n# ARG 指令有生效范围,如果在 FROM 指令之前指定,那么只能用于 FROM 指令中。\n# 在 FROM 之后须再次指定 ARG\nARG LARADOCK_PHP_VERSION\n```\n\n## Config Setting\n\n```json\n{\n \"registry-mirrors\": [\n \"https://mirror.ccs.tencentyun.com\",\n \"https://reg-mirror.qiniu.com\",\n \"https://hub-mirror.c.163.com\",\n \"https://mirror.baidubce.com\"\n ]\n}\n```\n","categories":["code-snippet"]},{"title":"Git Code Snippet","url":"/2022/10/01/git-code-snippet/","content":"\n## 建立独立的分支\n\n当想在项目中使用一个独立分支进行项目文档的管理时,或者当我们想要发布一个软件的开源版本但又不希望将软件的版本历史暴露给外界时,都可以使用以下的方法建立一个独立分支:\n\n```bash\ngit help checkout\n# git checkout --orphan <new-branch> <start-point>\n```\n\n## 修改提交时间\n\n```bash\n# 指定本次提交时间\ngit commit -m \"fix...\" --date=`date -R`\ngit commit -m \"fix...\" --date=\"$(date -R)\"\ngit commit -m \"fix...\" --date=\"Tue, 11 Jun 2019 17:50:50 +0800\"\n\n# 修改上次提交时间\ngit commit --amend --date=`date -R`\n# 风险操作。会修改 history hash.\n```\n\n## 忽略文件权限修改\n\n```bash\n# Git 默认会记录文件权限的修改,可关闭\ngit config core.filemode false\n```\n\n## git status 中文文件名乱码\n\n```bash\ngit config --global core.quotepath false\n```\n\n当 `core.quotepath` 设置为 `false` 时,Git 将不会对路径名进行 quoting,这意味着路径名中的特殊字符将不被转义。\n\n> [Git 中文显示问题解决](http://xstarcd.github.io/wiki/shell/git_chinese.html)\n\n## 工作区中删除未跟踪的文件\n\n```bash\n# 查看文档\ngit help clean\ngit clean -h\n# git clean [-d] [-f] [-i] [-n] [-q] [-e <pattern>] [-x | -X] [--] [<pathspec>...]\n\n# 【查看】有哪些文件将被删除\ngit clean -n\n\n# 【查看】删除 Git【忽略】的文件与文件夹\ngit clean -Xn\n\n# 【查看】删除【src 路径下】下的【未跟踪】文件以及文件夹\ngit clean -d -- src\n\n# 如果想执行删除 -n 替换为 -f,注意风险。\n```\n\n> [Git 清除未跟踪文件 | github.io](http://glgjing.github.io/blog/2015/01/09/git-qing-chu-wei-gen-zong-wen-jian/)\n\n## 推荐站点\n\n- [Git教程 | 廖雪峰的官方网站](https://www.liaoxuefeng.com/wiki/896043488029600)\n- [git - 简明指南](https://rogerdudler.github.io/git-guide/index.zh.html)\n- [语义化版本 2.0.0 | semver.org](https://semver.org/lang/zh-CN/)\n\n## References\n\n- [圖解 Git 版本控制: Cola Git GUI (1)](http://graphicalgit.blogspot.com/2012/07/git-cola-git-gui-1.html)\n\n-- EOF --\n","tags":["git"],"categories":["code-snippet"]},{"title":"MySQL 实战 45 讲 Part2","url":"/2022/09/14/mysql-course-45-part2/","content":"\n> [MySQL 实战 45 讲 | 林晓斌](http://gk.link/a/11AmM)\n","tags":["mysql"]},{"title":"MySQL 实战 45 讲 Part1","url":"/2022/08/14/mysql-course-45-part1/","content":"\n> [MySQL 实战 45 讲 | 林晓斌](http://gk.link/a/11AmM)\n\n## 01 | 基础架构:一条 SQL 查询语句是如何执行的?\n\n<img alt=\"image\" src=\"https://user-images.githubusercontent.com/9289792/184530380-826038a0-335a-45f7-a56e-cff7b336b4a4.png\" style=\"width: 480px\" />\n\n连接器:跟客户端建立连接、获取权限、维持和管理连接。\n\n```sql\nSHOW PROCESSLIST;\n\n137462239 db 10.135.4.7:41261 db Query 0 starting show processlist\n-- 一个空闲连接\n137462293 db 10.130.128.1:47174 db Sleep 24\n```\n\n```sql\n-- 客户端如果太长时间没动静,连接器就会自动将它断开\nselect @@wait_timeout;\nshow variables like 'wait_timeout';\n-- 28800 8h\n```\n\n通过执行 `mysql_reset_connection` 来重新初始化连接资源,这是个接口函数,不是一个 SQL 语句。\n\n查询缓存:不建议使用。在一个表上有更新的时候,跟这个表有关的查询缓存都会失效。\n\n```sql\nselect @@query_cache_type;\n-- OFF\n```\n\n- [sysvar_query_cache_type | mysql](https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_query_cache_type)\n\n分析器:词法分析、语法分析。进行分词和验证语法规则。解析器和预处理器。\n\n- 解析器处理语法和解析查询,生成一课对应的解析树。\n- 预处理器进一步检查解析树的合法。比如: 数据表和数据列是否存在,别名是否有歧义等。如果通过则生成新的解析树,再提交给优化器。\n\n优化器:决定使用哪个索引,决定各个表的连接顺序。\n\n执行器:有没有执行查询的权限,操作引擎,返回结果。执行器调用的次数(rows_examined)与引擎总共扫描行数可能是不等的,后文有例子。\n\n读写、存取数据在 engine 引擎层,连接、鉴权、计算在 server 服务层。\n\n连接的长短是由客户端来决定的,MySQL 服务端不会主动断开连接,除非到了 waiting_timeout 所设置的时间。\n\n查询一个没用 k 列的表 `1054 - Unknown column 'k' in 'field list'` 是在哪阶段报错的?\n\n- 答案是:分析器。分析器在词法分析阶段,需要知道 SQL 中的每个字段代表什么意思,所以在这个阶段就可以判断表中存不存在 k 这一列。\n- 不是执行器的原因:有人说在执行器时才打开表获取数据,但是表的字段不是数据,是事先定义好的,所以可以直接读取的,不需要打开表。\n\n## 02 | 日志系统:一条 SQL 更新语句是如何执行的?\n\nWAL 的全称是 Write-Ahead Logging,先写日志,再写磁盘。\n\n1. redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用\n2. redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1”。\n3. redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。\n\n- 物理:InnoDB 存储引擎提供接口给执行器调用(操作数据),数据库的数据是存在磁盘上的(或者说硬盘上),那么 redo log 记录存储引擎修改硬盘上的数据的操作就叫做物理操作;物理日志就只有“我”自己能用,别人没有共享我的“物理格式”。\n- 逻辑:binlog 归档日志,有两种模式 1 statement 记录 SQL;2 row 格式记录两条数据,数据修改前的样子,数据修改后的样子。记录的是一种逻辑上的变化 。逻辑日志可以给别的数据库,别的引擎使用,已经大家都讲得通这个“逻辑”。\n\n<img alt=\"image\" src=\"https://user-images.githubusercontent.com/9289792/184621469-ce7bea97-685a-421f-a582-0d571badc226.png\" style=\"width: 480px\" />\n\n把 binlog 夹在 redo log 中间,就是为了保证如果 redo 提交前的任何失败,都会带来回滚,binlog 的写入也应该不会成功,只有这样,才能保证两个一致。\n\n- 如果提交了 binlog,提交事务接口崩溃了,恢复时 redo log 有日志记录,binlog 有日志记录,一致,直接自动提交事务,事务完成确认数据修改成功。\n- 如果提交 binlog 前就崩了,redo log 是 prepare 阶段,binlog 没有记录不一致,事务回滚,事务执行失败。\n\nredo log 负责事务、crash-safe;binlog 负责归档恢复。redo log 是物理的,binlog 是逻辑的。\n\nredo log 是顺序写,数据文件是随机写。\n\nMySQL 的记录是以“页”为单位存取的,默认大小 16K。也就是说,你要访问磁盘中一个记录,不会只读这个记录,而会把它所在的 16K 数据一起读入内存。\n\n```sql\n-- 每个 binlog 文件的大小\nSELECT @@max_binlog_size / 1024 / 1024;\n-- 256.00000000\n\n-- 一页数据大小\nSELECT @@innodb_page_size / 1024;\n-- 16.0000\n```\n\nredo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。\n\n```sql\n-- 保证 crash-safe 能力;每次事务的 redo log 都直接持久化到磁盘\nselect @@innodb_flush_log_at_trx_commit;\n-- 2\n\n-- 每次事务的 binlog 都持久化到磁盘\nselect @@sync_binlog;\n-- 0\n\nSELECT @@binlog_format;\n-- STATEMENT 是记 SQL 语句,但是有风险比如时间函数\n-- ROW 记录行的内容,记两条,更新前和更新后都有\n```\n\n- [彻底搞懂三大 MySQL 日志,Redo Log、Undo Log、Bin Log](https://segmentfault.com/a/1190000042041728)\n- [彻底搞懂 mysql 日志系统 binlog,redolog,undolog](https://blog.51cto.com/u_3664660/3212550)\n\n## 03 | 事务隔离:为什么你改了我还看不见?\n\n脏读(dirty read)读到其他事务未提交的数据,仅发生在读未提交的的隔离级别下:\n\n```sql\n-- session1 当数据库中一个事务A正在修改一个数据但是还未提交或者回滚\nBEGIN;\nSELECT k FROM T1 WHERE id=1;\n-- 1\nUPDATE T1 SET k=2 WHERE id=1;\n\n-- session2\nSET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;\nSELECT k FROM T1 WHERE id=1;\n-- 2 session2 出现脏读\n```\n\n不可重复读(non-repeatable read)前后读取的记录内容不一致,发生在读未提交、读提交的隔离级别:\n\n```sql\n-- session1 在一个事务A中多次操作数据,在事务操作过程中(未最终提交)\nSET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;\nBEGIN;\nSELECT k FROM T1 WHERE id=1;\n-- 1\n\n-- session2 修改改值\nUPDATE T1 SET k=2 WHERE id=1;\n\n-- session1\nSELECT k FROM T1 WHERE id=1;\n-- 2 session1 出现不可重复读\n```\n\n幻读(phantom read)前后读取的记录数量不一致,发生在读未提交、读提交的隔离级别,InnoDB RR 不发生幻读:\n\n```sql\n-- session1 获取当前行数量\nSET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;\nBEGIN;\nSELECT * FROM T1 WHERE k=1;\n-- 1\n\n-- session2 插入一行数\nINSERT INTO T1 (k) VALUES (1);\n\n-- session1\nSELECT count(*) FROM T1;\n-- 2\n```\n\n```sql\n-- session1 表数据\nBEGIN;\nSELECT * FROM T1;\n-- id,k 1,1 只有一行数据\n\n-- session2 更新 id 主键\nUPDATE T1 SET id=5 WHERE id=1;\n\n-- session1\nSELECT * FROM T1;\n-- id,k 1,1\nUPDATE T1 SET id=id+1;\n-- ok\nSELECT * FROM T1;\n-- 竟然看到了两行 1,1 6,1\n-- 1,1 MVCC 保证了 1,1 的存在\n-- 因为主键的 UPDATE 在 MySQL 里是以 insert+delete 方式执行的。这个 6 和 1 在 MySQL 看来已经不是同一行数据了,1 的 delete version 是在事务 1 的可见范围,所以才能看得到。如果是非主键就只用一行,就是下面的例子.\n-- 这个例子不能被归类为幻读,只不过是当前最新读带来的问题。\n```\n\n```sql\n-- session1 获取 id=1 的 k\nBEGIN;\nSELECT k FROM T1;\n-- 1 一行数据\n\n-- session2 更新\nUPDATE T1 SET k=2;\n\n-- session1\nSELECT k FROM T1;\n-- 1\nUPDATE T1 SET k=k+1;\n-- ok\nSELECT k FROM T1;\n-- 3\n```\n\n- [关于幻读,可重复读的真实用例是什么?| zhihu](https://www.zhihu.com/question/47007926)\n\n不可重复读和幻读区别:由于在 InnoDB RR 下模拟不出幻读的场景,退回到 RC 隔离级别的话就容易把幻读和不可重复读搞混淆。理论上 RR 级别是无法解决幻读的问题, 但是由于 InnoDB 引擎的 RR 级别还使用了 MVCC,所以也就避免了幻读的出现。\n\n后文有讲到 FOR UPDATE 下的幻读。\n\n事务隔离级别:\n\n- 读未提交(READ UNCOMMITTED)一个事务还没提交时,它做的变更就能被别的事务看到。直接返回记录上的最新值,没有视图概念。\n- 读提交(READ COMMITTED)一个事务提交之后,它做的变更才会被其他事务看到。这个视图是在每个 SQL 语句开始执行的时候创建的。\n- 可重复读(REPEATABLE READ)一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的 read-view。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。这个视图是在事务启动时(第一个 SQL 执行时)创建的,整个事务存在期间都用这个视图。\n- 串行化(SERIALIZABLE)对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。用加锁的方式来避免并行访问。\n\n```sql\nshow variables like '%tx_isolation%';\nshow variables like 'transaction_isolation';\n-- tx_isolation REPEATABLE-READ\n\nselect @@tx_isolation;\n-- REPEATABLE-READ\n\nSET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;\n-- 修改 session 事务隔离级别\n```\n\n> [SET TRANSACTION Statement | mysql](https://dev.mysql.com/doc/refman/8.0/en/set-transaction.html)\n\n多版本并发控制(MVCC)\n\n`set autocommit=0`,这个命令会将这个线程的自动提交关掉。\n\n```sql\n-- 持续时间超过 60s 的事务\nselect * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60;\n```\n\n```sql\ncommit work and chain;\n-- 等价于 commit; begin;\n```\n\n如何避免长事务对业务的影响?\n\n- 确认是否使用了 set autocommit=0,如果没有,则可以使用 set autocommit=1 来避免长事务对业务的影响。\n- 确认是否有不必要的只读事务。有些框架会习惯不管什么语句先用 begin/commit 框起来。\n- 业务连接数据库的时候,根据业务本身的预估,通过 SET MAX_EXECUTION_TIME 命令,来控制每个语句执行的最长时间,避免单个语句意外执行太长时间。\n- 监控 information_schema.Innodb_trx 表,设置长事务阈值。\n- 开发测试阶段输出所用 general_log,分析日志提前发现问题。\n\n## 04 | 深入浅出索引(上)\n\n- 哈希表:这种结构适用于只有等值查询的场景。\n- 有序数组:有序数组在等值查询和范围查询场景中的性能就都非常优秀。但有序数组索引只适用于静态存储引擎\n- 搜索树:平衡二叉树是 O(log(N)) 的查询复杂度;N 叉树,以 InnoDB 整数字段索引为例,这个 N 差不多是 1200。\n\n> MySQL 默认一个节点的长度为 16K,一个整数(bigint)字段索引的长度为 8B,另外每个索引还跟着 6B 的指向其子树的指针;所以 16K/14B ≈ 1170\n\ninnodb B+树主键索引的叶子节点存的是什么:\n\n回复 1:\n\n> InnoDB 磁盘管理的最小单位就是“页”,也就是说无论是叶子节点、非叶子节点和行数据,都是存放在页当中。页组成结构有头部数据、主体数据和尾部数据。头部数据主要存的是页相关数据,例如上一页、下一页、当前页号等。是一个双向链表结构。主体数据主要关注索引和数据的存储,也就是我们常说的索引和数据的存储位置。主体数据当中有一个“User Records”的概念,用来存储索引和数据,是一个单链表结构。\n> User Records 根据节点的不同,User Records 又分为四种不同类型:主键索引树叶子节点和非叶子节点,二级索引树叶子节点和非叶子节点。\n> 有了页和 User Records 的认识,其实说叶子节点存的是页是一种笼统的回答,基于我的理解,我认为叶子节点(主键索引树叶子节点)存放的是行数据更为贴切。\n\n回复 2:\n\n> B+树的叶子节点是 page(页),一个页里面可以存多个行。 B+树的结点跟 innoDB 的“页”都属于一种抽象逻辑概念。如果你要问“存”的是什么?我觉得回答行数据没毛病。因为存的不可能是“页”。这一逻辑概念,只能说这个叶结点大小等于 innoDB 里设置的页大小,或者说这个叶结点其实就是“页”。但存的是什么?那当然是数据,什么数据?当然是表中的行数据。\n\n索引类型分为主键索引和非主键索引。\n\n- 主键索引也被称为聚簇索引(clustered index)\n- 非主键索引也被称为二级索引(secondary index)\n\n基于非主键索引的查询需要多扫描一棵索引树,也就是主键索引树,也就是回表操作。\n\n自增主键的意义:\n\n- 性能方面:每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂(页分裂)。\n- 存储空间方面:主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。\n\n有没有什么场景适合用业务字段直接做主键的呢?\n\n- 只有一个索引\n- 该索引必须是唯一索引\n\n即典型的 KV 场景。\n\n非聚集索引上为啥叶子节点的 value 为什么不是地址,这样可以直接定位到整条数据,而不用再次对整棵树进行查询?\n\n这个叫作“堆组织表”,MyISAM 就是这样的,各有利弊。你想一下如果修改了数据的位置的情况,InnoDB 这种模式是不是就方便些。主键索引页分裂的场景,就可能会导致主键记录的地址发生变化,这时候需要更新每一个索引上面对主键记录地址的引用。\n\n## 05 | 深入浅出索引(下)\n\n覆盖索引:在查询里索引已经覆盖了查询需要的列。覆盖索引可以减少树的搜索次数,显著提升查询性能,是一个常用的性能优化手段。\n\n```sql\nselect * from T where k between 3 and 5;\n```\n\n表中 k 的值是:1,2,3,5,6,7。引擎内部使用覆盖索引在索引 k 上其实读了三个记录(第一次 3,第二次 3 的下一个 5,第三次 5 的下一个 6 不满足 ),但是对于 MySQL 的 Server 层来说,它只是找引擎拿到了两条记录,因此 MySQL 认为扫描行数是 2。这就是引擎实际扫描条数不等于 MySQL explain 语句中的 rows 字段的原因。\n\n最左前缀原则:B+树这种索引结构,可以利用索引的“最左前缀”,来定位记录。\n\n第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。\n\n如果需要常查人的名字与年龄。可以建立索引 (name,age) 与 (age),而不是 (age,name) 与 (age),考虑的原则就是空间。\n\n索引下推优化(index condition pushdown):\n\n联合索引(name,age)为例,名字第一个字是张,而且年龄是 10 岁的所有男孩:\n\n```sql\nselect * from tuser where name like '张%' and age=10 and ismale=1;\n```\n\n这个语句在搜索索引树的时候,只能用 “张”。\n\n- 在 MySQL 5.6 之前,只能从符合 '张%' 开始一个个回表。到主键索引上找出数据行,再对比字段值。\n- 而 MySQL 5.6 引入的索引下推优化(index condition pushdown),可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。\n\n<img alt=\"image\" src=\"https://user-images.githubusercontent.com/9289792/185831784-b6f62f71-a0f7-49c8-afe4-dda0f2dc335a.png\" style=\"width: 420px\" />\n\nInnoDB 在(name,age)索引内部就判断了 age 是否等于 10,对于不等于 10 的记录,直接判断并跳过。\n\n联合索引中是按索引列的顺序,来排序的。(name,age)先排 name 列的值,再排 age 列的值。\n\n在满足语句需求的情况下,尽量少地访问资源是数据库设计的重要原则之一。\n\n## 06 | 全局锁和表锁:给表加个字段怎么有这么多阻碍?\n\n全局锁:\n\n```sql\n-- 加全局读锁 FTWRL,整个库处于只读状态;做全库逻辑备份\nFLUSH TABLES WITH READ LOCK;\n\n-- 插入一行数据\n\nSHOW PROCESSLIST;\n-- Waiting for global read lock\n\n-- 释放锁\nUNLOCK TABLES;\n```\n\n但是让整库都只读,听上去就很危险:\n\n- 如果你在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆。\n- 如果你在从库上备份,那么备份期间从库不能执行主库同步过来的 binlog,会导致主从延迟。\n\n在可重复读隔离级别下开启一个事务,来确保拿到一致性视图。mysqldump 使用参数 `-single-transaction` 就是如此执行的。而由于 MVCC 的支持,这个过程中数据是可以正常更新的。\n\n有了可重复读的事务隔离级别却还需要 FTWRL 的原因是:引擎不都支持这个事务隔离级别。\n\n既然要全库只读,为什么不使用 `set global readonly=true` 的方式\n\n- 在有些系统中,readonly 的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。修改 global 变量的方式影响面更大。\n- 在异常处理机制上有差异。如果执行 FTWRL 命令之后由于客户端发生异常断开,那么 MySQL 会自动释放这个全局锁,整个库回到可以正常更新的状态。而将\n 整个库设置为 readonly 之后,如果客户端发生异常,则数据库就会一直保持 readonly 状态,这样会导致整个库长时间处于不可写状态,风险较高。\n\n表级锁:\n\n第一类表锁:[lock-tables | mysql](https://dev.mysql.com/doc/refman/5.7/en/lock-tables.html)\n\n```sql\nLOCK TABLES T1 READ, T2 WRITE;\n-- 其他线程 写 T1、读写 T2 的语句都会被阻塞\n-- 当前线程也只能执行 读 T1、读写 T2 的操作,其他表都不能访问\n\n-- 当前线程读 T2\n-- Waiting for table metadata lock\n\n-- 当前线程读 T3\n-- 1100 - Table 'T3' was not locked with LOCK TABLES\n```\n\n```sql\n-- 查看表锁\nSHOW OPEN TABLES WHERE In_use > 0;\n```\n\n第二类 MDL(metadata lock):\n\nMDL 不需要显式使用:\n\n- 当对一个表做增删改查操作的时候,加 MDL 读锁。读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。共享锁。\n- 当要对表做结构变更操作的时候,加 MDL 写锁。读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。排他锁。\n\n给一个小表加个字段,导致整个库挂了:\n\n```sql\n-- session1\nSET autocommit = 0;\n-- 关闭自动提交\n\nBEGIN;\n-- 开启使用\n\nSELECT * FROM T;\n-- 查询 T 表,一切 ok\n-- 获取 MDL 读锁 ok,因为在事务中,查询结束后没有释放读锁。\n\n-- 事务中的 MDL 锁,在语句执行开始时申请,但是语句结束后并不会马上释放,而会等到整个事务提交后再释放。\n\nSELECT * FROM information_schema.innodb_trx;\n-- 查询事务有个 RUNNING\n```\n\n```sql\n-- session2\nSELECT * FROM T;\n-- 查询 T 表,一切 ok\n-- 获取 MDL 读锁 ok,查询结束后释放读锁。\n```\n\n```sql\n-- session3\nALTER TABLE `T` ADD COLUMN `c2` varchar(255) NULL;\n-- 修改字段,卡住\n-- blocked 原因:是因为 session1 的 MDL 读锁还没有释放,而 session3 需要 MDL 写锁,因此只能被阻塞。\n\nSHOW OPEN TABLES WHERE In_use > 0;\n-- In_use 1\n```\n\n```sql\n-- session4\nSELECT * FROM T;\n-- 查询 T 表,卡住\n-- blocked 原因:对表的增删改查操作都需要先申请 MDL 读锁,都被 session3 阻塞。\n```\n\n<img alt=\"image\" src=\"https://user-images.githubusercontent.com/9289792/185884836-f36feff9-1668-4f37-ad06-1fff9d9c18ef.png\" style=\"width: 420px\" />\n\n这里自己想了一个问题:session4 卡住后,session1 COMMIT 后,session4 的 SELECT 执行结果是否有 session3 的新列呢?\n\n从结果上看是没有的。开始以为是事务级别,但是调整为 READ UNCOMMITTED,session4 仍然没有新列。后来了解到 online DDL 意识到应该有关系。测试后得出 session3 在获取写锁后,在做 DDL 前会释放写锁加读锁,这时 session4 就可以执行了,但是这时 DDL 还没有执行,所以 session4 的执行结果没有新列。\n\n如果 session3 执行语句是:\n\n```sql\nALTER TABLE `T` ADD COLUMN `c2` varchar(255) NULL, LOCK=EXCLUSIVE;\n-- NONE:允许并发查询和DML操作;\n-- SHARED:允许并发查询,但不允许DML操作;\n-- DEFAULT:允许尽可能多的并发查询或DML操作(或两者都允许),没指定LOCK选项默认就为DEFAULT;\n-- EXCLUSIVE:不允许并发查询和DML操作。\n```\n\nEXCLUSIVE 时写锁会一直持有,一直等到 DDL 完毕 session4 才开始执行,所以 session4 的执行结果有新列。\n\n衍生问题:session3 如果没有 `LOCK=EXCLUSIVE`,session4 查询前 `BEGIN;` 开启事务那结果是如何呢?实验结果:session1 提交后,session3 session4 依然阻塞。\n\n衍生问题:session3 添加 `LOCK=EXCLUSIVE`,session4 查询前 `BEGIN;` 开启事务那结果是如何呢?实验结果:ssession1 提交后,session3 session4 正常结束。\n\n分析猜测 session3 阻塞的原因是:session1 提交后释放了读锁,session3 session4 拿到读锁,session3 DDL 操作完成后又要拿写锁,但是 session4 的读锁未释放,所以 session3 阻塞。那 session4 为啥阻塞呢?session4 可以获取读锁就应该可以执行完毕,遗留问题。\n\n如何安全地给小表加字段?\n\n- 解决长事务,事务不提交,就会一直占着 MDL 锁。\n- 在 ALTER TABLE 语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到 MDL 写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。\n\n```sql\nALTER TABLE tbl_name NOWAIT add column ...\nALTER TABLE tbl_name WAIT N add column ...\n```\n\n实测:MySQL 5.7.30 不支持。腾讯云 MySQL 5.7.18-txsql-log 支持。\n\n- [支持 NOWAIT 语法 | cloud.tencent](https://cloud.tencent.com/document/product/236/48838)\n\n## 07 | 行锁功过:怎么减少行锁对性能的影响?\n\n在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。\n\n如果事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。\n\n举例:\n\n1. 从顾客 A 账户余额中扣除电影票价;UPDATE\n2. 给影院 B 的账户余额增加这张电影票价;UPDATE 最容易发送锁等待的地方\n3. 记录一条交易日志。INSERT\n\n以 3、1、2 顺序执行可以最大程度地减少了事务之间的锁等待,提升了并发度。\n\n当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。\n\n<img alt=\"image\" src=\"https://user-images.githubusercontent.com/9289792/186377931-53a1b737-3149-4985-820f-87ffd1319158.png\" style=\"width: 420px\" />\n\n上图 session1 在等 session2 id=2 的锁,session2 在等 session1 id=1 的锁,进入死锁状态。\n\n```sql\n-- 所等待超时时间 s\nSELECT @@innodb_lock_wait_timeout;\n-- 7200 2h\n\n-- 死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务\nSELECT @@innodb_deadlock_detect;\n-- 1213 - Deadlock found when trying to get lock; try restarting transaction\n```\n\n主动死锁检测在发生死锁的时候,是能够快速发现并进行处理的,但是它也是有额外负担的:\n\n每当一个事务被锁的时候,就要看看它所依赖的线程有没有被别人锁住,如此循环,最后判断是否出现了循环等待,也就是死锁。\n\n每个新来的被堵住的线程,都要判断会不会由于自己的加入导致了死锁,这是一个时间复杂度是 O(n)的操作。假设有 1000 个并发线程要同时更新同一行,那么死锁检测操作就是 100 万这个量级的。这期间要消耗大量的 CPU 资源。即:热点行更新导致的性能问题。\n\n可以通过尝试将热点数据拆分多行进行处理,提高并发处理。\n\n## 08 | 事务到底是隔离的还是不隔离的?\n\n`begin/start transaction` 命令并不是一个事务的起点,在执行到它们之后的第一个操作 InnoDB 表的语句,事务才真正启动。如果你想要马上启动一个事务:\n\n```sql\nSTART TRANSACTION WITH CONSISTENT SNAPSHOT;\n```\n\n有两个“视图”的概念:\n\n- 一个是 view。它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建视图的语法是 create view ...,而它的查询方法与表一样。\n- 另一个是 InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。\n\n在可重复读隔离级别下,事务在启动的时候就“拍了个快照”。注意,这个快照是基于整库的。\n\nInnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。\n\n---\n","tags":["mysql"]},{"title":"Shell Code Snippet","url":"/2022/07/28/shell-code-snippet/","content":"\n## pkgs.org\n\n<https://pkgs.org/download/vim-common>\n\n```bash\n# rpm redhat package manager 查看安装的包版本\nrpm -qa | grep docker\n```\n\n## 命令后台运行\n\n```bash\n# 后台运行,关掉终端会停止运行\ncmd &\n# 后台运行,关掉终端不会停止运行\nnohup cmd &\n```\n\n- ctrl + z 将正在前台执行的命令放到后台,且让命令处于暂停状态。?\n- jobs:查看当前有多少在后台运行的命令,-l 选项可显示所有任务的 PID。\n- fg:将后台命令放回前台运行(可以跟 jobid )。\n- bg:将嵌套命令放到后台运行(可以跟 jobid )。\n\n## lookup CNAME records\n\n```bash\ndig zyf.im cname\n# ;; ANSWER SECTION:\n# zyf.im. 300 IN CNAME imzyf.github.io.\n```\n\n## 查找大文件\n\n```bash\ndu -s ./*|sort -nr|head -3\n```\n\n一般说来不会出现删除文件后空间不释放的情况,但是也存在例外,比如文件被进程锁定,或者有进程一直在向这个文件写数据等等,要理解这个问题,就需要知道 Linux 下文件的存储机制和存储结构。\n\n一个文件在文件系统中的存放分为两个部分:数据部分和指针部分,指针位于文件系统的 meta-data 中,数据被删除后,这个指针就从 meta-data 中清除了,而数据部分存储在磁盘中,数据对应的指针从 meta-data 中清除后,文件数据部分占用的空间就可以被覆盖并写入新的内容,之所以出现删除 access_log 文件后,空间还没释放,就是因为 httpd 进程还在一直向这个文件写入内容,导致虽然删除了 access_log 文件,但文件对应的指针部分由于进程锁定,并未从 meta-data 中清除,而由于指针并未被删除,那么系统内核就认为文件并未被删除,因此通过 df 命令查询空间并未释放也就不足为奇了。\n\n```bash\n# 获取一个已经被删除但仍然被应用程序占用的文件列表\nlsof | grep delete\n```\n\n通过这种方法,磁盘空间不但可以马上释放,也可保障进程继续向文件写入日志,这种方法经常用于在线清理 Apache、Tomcat、Nginx 等 Web 服务产生的日志文件:\n\n```bash\necho \" \" > /tmp/acess.log\n```\n\ndocker image 可能占用大量空间。\n\n## grep 遍历文件夹查找文本内容\n\n有时候我们需要在某一个包含很多子目录的目录中搜索查找包含某个文本内容的文本,我们可以在 grep 中加上 -r 选项让 grep 命令迭代进入子目录查找。同时在命令最后加上代表文件通配符的\\*号,不然 grep 会一直等待输入。\n\n```bash\ngrep -r \"查找文本内容\" *\n```\n\n## set\n\n- [The Set Builtin | gnu](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html)\n- [Bash 脚本 set 命令教程 | ruanyifeng](http://www.ruanyifeng.com/blog/2017/11/bash-set.html)\n\n```bash\n#!/usr/bin/env bash\n\nset -o errexit #等价 set -e。只要发生错误,就终止执行。认为非0就是错误。\nset +o nounset # 等价 set +u。遇到不存在的变量不报错。默认如此。\nset -o pipefail # 只要一个子命令失败,整个管道命令就失败,脚本就会终止执行。\nset -o xtrace # 等价 set -x。在运行结果之前,先输出执行的那一行命令,调试复杂的脚本是很有用。\n```\n\n```bash\nset -o errexit\nset -o nounset\nset -o pipefail\nset -o xtrace\n\n# 4合1\nset -euxo pipefail\n## 写法二\nset -eux\nset -o pipefail\n\n# 或者执行命令时\nbash -euxo pipefail script.sh\n```\n\n如果脚本里面有运行失败的命令(返回值非 0),Bash 默认会继续执行后面的命令。\n\n实际开发中,如果某个命令失败,往往需要脚本停止执行,防止错误累积。这时,一般采用下面的写法:\n\n```bash\ncommand || exit 1\n```\n\n上面的写法表示只要 command 有非零返回值,脚本就会停止执行。\n\n如果停止执行之前需要完成多个操作,就要采用下面三种写法:\n\n```bash\n# 写法一\ncommand || { echo \"command failed\"; exit 1; }\n\n# 写法二\nif ! command; then echo \"command failed\"; exit 1; fi\n\n# 写法三\ncommand\nif [ \"$?\" -ne 0 ]; then echo \"command failed\"; exit 1; fi\n```\n\n如果两个命令有继承关系,只有第一个命令成功了,才能继续执行第二个命令,那么就要采用下面的写法:\n\n```bash\ncommand1 && command2\n```\n\n## CDPATH\n\n```bash\n# Unset CDPATH so that path interpolation can work correctly\nunset CDPATH\n```\n\n```bash\nexport CDPATH=/etc\ncd mail\n/etc/mail\n```\n\n## Ubuntu 命令行下设置时区\n\n- Ubuntu 16.04\n\n```bash\nsudo dpkg-reconfigure tzdata\n```\n\n按提示进行选择完成。\n\n设置完成后发现在 `crontab` 仍然是按 UTC 执行的,应该是需要重启下系统或者:\n\n```bash\n/etc/init.d/rsyslog restart\n```\n\n## Ubuntu 命令行下打开 PDF\n\n一个文件夹中存放了大量的文件后,在窗口打开中被打开时是非常耗时的。当我们已经明确知道文件名时,可以直接在 Terminal 中使用命令,调用应用打开文件:\n\n```bash\nevince filename.pdf\n```\n\n## References\n\n- [setting timezone from terminal - Ask Ubuntu](http://askubuntu.com/questions/323131/setting-timezone-from-terminal)\n- [cron - How do you set the timezone for crontab? - Ask Ubuntu](https://askubuntu.com/questions/54364/how-do-you-set-the-timezone-for-crontab)\n- [Ubuntu 命令行打开 PDF 文件 - bigmarco 的专栏](http://blog.csdn.net/bigmarco/article/details/6555582)\n- [Linux 文件删除,但是 df 之后磁盘空间没有释放 | cnblogs](https://www.cnblogs.com/xd502djj/p/6668632.html)\n- [How to lookup CNAME records | nslookup.io](https://www.nslookup.io/cname-lookup/)\n- [Linux 命令后台运行](https://juejin.cn/post/6985727600320593951)\n\n-- EOF --\n","categories":["code-snippet"]},{"title":"Front-end Code Snippet","url":"/2022/07/26/front-end-code-snippet/","content":"\n<!-- more -->\n\n## 库推荐\n\n### nrm\n\n[Pana/nrm](https://github.com/Pana/nrm)\n\nnrm can help you easy and fast switch between different npm registries, now include: npm, cnpm, taobao, nj(nodejitsu).\n\n```bash\nnpm install -g nrm\n\nnrm ls\n\nnrm use tencent\n```\n\n## Website\n\n- [W3Techs - World Wide Web Technology Surveys](https://w3techs.com/)\n- [nodejs release schedule](https://github.com/nodejs/release#release-schedule)\n\n## References\n\n-- EOF --\n","categories":["code-snippet"]},{"title":"【JavaScript 教程 | 网道】笔记","url":"/2022/06/09/javascript-wangdoc-reading-notes/","content":"\n> [JavaScript 教程 | 网道(WangDoc.com)](https://wangdoc.com/javascript/index.html)\n\n<!-- more -->\n\n## JavaScript 语言的历史\n\nECMAScript 只用来标准化 JavaScript 这种语言的基本语法结构,与部署环境相关的标准都由其他标准规定,比如 DOM 的标准就是由 W3C 组织(World Wide Web Consortium)制定的。\n\n2011 年 6 月,ECMAScript 5.1 版发布,并且成为 ISO 国际标准(ISO/IEC 16262:2011)。到了 2012 年底,所有主要浏览器都支持 ECMAScript 5.1 版的全部功能。\n\n2015 年 6 月,ECMAScript 6 正式发布,并且更名为“ECMAScript 2015”。这是因为 TC39 委员会计划,以后每年发布一个 ECMAScript 的版本,下一个版本在 2016 年发布,称为“ECMAScript 2016”,2017 年发布“ECMAScript 2017”,以此类推。\n\n## JavaScript 的基本语法\n\n如果只是声明变量而没有赋值,则该变量的值是 undefined。undefined 是一个特殊的值,表示“无定义”。\n\n```js\nvar a;\na; // undefined\n```\n\nJavaScript 是一种动态类型语言,也就是说,变量的类型没有限制,变量可以随时更改类型。\n\n```js\nvar a = 1;\na = \"hello\";\n```\n\n如果使用 var 重新声明一个已经存在的变量,是无效的。\n\n```js\nvar x = 1;\nvar x;\nx; // 1\n```\n\nJavaScript 引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升(hoisting)。\n\n```js\nconsole.log(a);\nvar a = 1;\n\n// 在控制台(console)显示变量a的值。这时变量a还没有声明和赋值,所以这是一种错误的做法,但是实际上不会报错。因为存在变量提升,真正运行的是下面的代码。\n\nvar a;\nconsole.log(a);\na = 1;\n```\n\n标识符命名规则如下:\n\n- 第一个字符,可以是任意 Unicode 字母(包括英文字母和其他语言的字母),以及美元符号($)和下划线(\\_)。\n- 第二个字符及后面的字符,除了 Unicode 字母、美元符号和下划线,还可以用数字 0-9。\n\n对于 var 命令来说,JavaScript 的区块不构成单独的作用域(scope)。\n\n```js\n{\n var a = 1;\n}\n\na; // 1\n```\n\nelse 代码块总是与离自己最近的那个 if 语句配对。\n\n```js\nvar m = 1;\nvar n = 2;\n\nif (m !== 1)\n if (n === 2) console.log(\"hello\");\n else console.log(\"world\");\n\n// 相当于\n\nif (m !== 1) {\n if (n === 2) {\n console.log(\"hello\");\n } else {\n console.log(\"world\");\n }\n}\n```\n\nswitch 语句后面的表达式,与 case 语句后面的表示式比较运行结果时,采用的是严格相等运算符(===),而不是相等运算符(==),这意味着比较时不会发生类型转换。\n\n```js\nvar x = 1;\n\nswitch (x) {\n case true:\n console.log(\"x 发生类型转换\");\n break;\n default:\n console.log(\"x 没有发生类型转换\");\n}\n// x 没有发生类型转换\n```\n\nfor 语句的三个部分(initialize、test、increment),可以省略任何一个,也可以全部省略。\n\n```js\nfor (;;) {\n console.log(\"Hello World\");\n}\n```\n\n上面代码省略了 for 语句表达式的三个部分,结果就导致了一个无限循环。\n\nJavaScript 语言允许,语句的前面有标签(label),相当于定位符,用于跳转到程序的任意位置,标签的格式如下。\n\n标签通常与 break 语句和 continue 语句配合使用,跳出特定的循环。\n\n```js\ntop: for (var i = 0; i < 3; i++) {\n for (var j = 0; j < 3; j++) {\n if (i === 1 && j === 1) break top;\n console.log(\"i=\" + i + \", j=\" + j);\n }\n}\n// i=0, j=0\n// i=0, j=1\n// i=0, j=2\n// i=1, j=0\n\ntop: for (var i = 0; i < 3; i++) {\n for (var j = 0; j < 3; j++) {\n if (i === 1 && j === 1) continue top;\n console.log(\"i=\" + i + \", j=\" + j);\n }\n}\n// i=0, j=0\n// i=0, j=1\n// i=0, j=2\n// i=1, j=0\n// i=2, j=0\n// i=2, j=1\n// i=2, j=2\n```\n\n## 数据类型概述\n\nJavaScript 语言的每一个值,都属于某一种数据类型。JavaScript 的数据类型,共有六种。(ES6 又新增了第七种 Symbol 类型的值,本教程不涉及。)\n\n- 数值(number):整数和小数(比如 1 和 3.14)。\n- 字符串(string):文本(比如 Hello World)。\n- 布尔值(boolean):表示真伪的两个特殊值,即 true(真)和 false(假)。\n- undefined:表示“未定义”或不存在,即由于目前没有定义,所以此处暂时没有任何值。\n- null:表示空值,即此处的值为空。\n- 对象(object):各种值组成的集合。\n\n通常,数值、字符串、布尔值这三种类型,合称为原始类型(primitive type)的值,即它们是最基本的数据类型,不能再细分了。\n\n对象则称为合成类型(complex type)的值,因为一个对象往往是多个原始类型的值的合成,可以看作是一个存放各种值的容器。\n\n至于 undefined 和 null,一般将它们看成两个特殊值。\n\n对象是最复杂的数据类型,又可以分成三个子类型。\n\n- 狭义的对象(object)\n- 数组(array)\n- 函数(function)\n\n函数其实是处理数据的方法,JavaScript 把它当成一种数据类型,可以赋值给变量,这为编程带来了很大的灵活性,也为 JavaScript 的“函数式编程”奠定了基础。\n\nJavaScript 有三种方法,可以确定一个值到底是什么类型。\n\n- typeof 运算符\n- instanceof 运算符\n- Object.prototype.toString 方法\n\n```js\ntypeof 123; // \"number\"\ntypeof \"123\"; // \"string\"\ntypeof false; // \"boolean\"\n\nfunction f() {}\ntypeof f;\n// \"function\"\n\ntypeof undefined;\n// \"undefined\"\n```\n\n利用这一点,typeof 可以用来检查一个没有声明的变量,而不报错。\n\n```js\nv;\n// ReferenceError: v is not defined\n\ntypeof v;\n// \"undefined\"\n\n// 错误的写法\nif (v) {\n // ...\n}\n// ReferenceError: v is not defined\n\n// 正确的写法\nif (typeof v === \"undefined\") {\n // ...\n}\n```\n\n```js\ntypeof window; // \"object\"\ntypeof {}; // \"object\"\ntypeof []; // \"object\"\n```\n\n上面代码中,空数组([])的类型也是 object,这表示在 JavaScript 内部,数组本质上只是一种特殊的对象。instanceof 运算符可以区分数组和对象。\n\n```js\nvar o = {};\nvar a = [];\n\no instanceof Array; // false\no instanceof Object; // true\na instanceof Array; // true\na instanceof Object; // false\n```\n\nnull 的类型是 object,这是由于历史原因造成的。1995 年的 JavaScript 语言第一版,只设计了五种数据类型(对象、整数、浮点数、字符串和布尔值),没考虑 null,只把它当作 object 的一种特殊值。后来 null 独立出来,作为一种单独的数据类型,为了兼容以前的代码,typeof null 返回 object 就没法改变了。\n\n```js\ntypeof null; // \"object\"\n```\n\n## null, undefined 和布尔值\n\nnull 与 undefined 都可以表示“没有”,含义非常相似。将一个变量赋值为 undefined 或 null,语法效果几乎没区别。\n\n```js\nif (!undefined) {\n console.log(\"undefined is false\");\n}\n// undefined is false\n\nif (!null) {\n console.log(\"null is false\");\n}\n// null is false\n\nundefined == null;\n// true\n```\n\n1995 年 JavaScript 诞生时,最初像 Java 一样,只设置了 null 表示\"无\"。根据 C 语言的传统,null 可以自动转为 0。\n\n```js\nNumber(null); // 0\n5 + null; // 5\n```\n\n但是,JavaScript 的设计者 Brendan Eich,觉得这样做还不够。首先,第一版的 JavaScript 里面,null 就像在 Java 里一样,被当成一个对象,Brendan Eich 觉得表示“无”的值最好不是对象。其次,那时的 JavaScript 不包括错误处理机制,Brendan Eich 觉得,如果 null 自动转为 0,很不容易发现错误。\n\n因此,他又设计了一个 undefined。区别是这样的:null 是一个表示“空”的对象,转为数值时为 0;undefined 是一个表示\"此处无定义\"的原始值,转为数值时为 NaN。\n\n```js\nNumber(undefined); // NaN\n5 + undefined; // NaN\n```\n\n- null 表示空值,即该处的值现在为空。调用函数时,某个参数未设置任何值,这时就可以传入 null,表示该参数为空。比如,某个函数接受引擎抛出的错误作为参数,如果运行过程中未出错,那么这个参数就会传入 null,表示未发生错误。\n- undefined 表示“未定义”,下面是返回 undefined 的典型场景。\n\n```js\n// 变量声明了,但没有赋值\nvar i;\ni; // undefined\n\n// 调用函数时,应该提供的参数没有提供,该参数等于 undefined\nfunction f(x) {\n return x;\n}\nf(); // undefined\n\n// 对象没有赋值的属性\nvar o = new Object();\no.p; // undefined\n\n// 函数没有返回值时,默认返回 undefined\nfunction f() {}\nf(); // undefined\n```\n\n如果 JavaScript 预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。转换规则是除了下面六个值被转为 false,其他值都视为 true。\n\n- undefined\n- null\n- false\n- 0\n- NaN\n- \"\"或''(空字符串)\n\n注意,空数组([])和空对象({})对应的布尔值,都是 true。\n\n## 数值\n\nJavaScript 内部,所有数字都是以 64 位浮点数形式储存,即使整数也是如此。所以,1 与 1.0 是相同的,是同一个数。\n\n这就是说,JavaScript 语言的底层根本没有整数,所有数字都是小数(64 位浮点数)。\n\n> 容易造成混淆的是,某些运算只有整数才能完成,此时 JavaScript 会自动把 64 位浮点数,转成 32 位整数,然后再进行运算。\n\n```js\n1 === 1.0; // true\n```\n\n由于浮点数不是精确的值,所以涉及小数的比较和运算要特别小心。\n\n```js\n0.1 + 0.2 === 0.3;\n// false\n\n0.3 /\n 0.1(\n // 2.9999999999999996\n\n 0.3 - 0.2\n ) ===\n 0.2 - 0.1;\n// false\n```\n\n根据国际标准 IEEE 754,JavaScript 浮点数的 64 个二进制位,从最左边开始,是这样组成的。\n\n- 第 1 位:符号位,0 表示正数,1 表示负数\n- 第 2 位到第 12 位(共 11 位):指数部分\n- 第 13 位到第 64 位(共 52 位):小数部分(即有效数字)\n\n符号位决定了一个数的正负,指数部分决定了数值的大小,小数部分决定了数值的精度。\n\n```js\nMath.pow(2, 53);\n// 9007199254740992\n\n// 多出的三个有效数字,将无法保存\n9007199254740992111;\n// 9007199254740992000\n```\n\n```js\nMath.pow(2, 1024); // Infinity “正向溢出”\n\nMath.pow(2, -1075); // 0\n\nNumber.MAX_VALUE; // 1.7976931348623157e+308\nNumber.MIN_VALUE; // 5e-324\n\n123e3; // 123000\n123e-3; // 0.123\n-3.1e12;\n0.1e-23;\n\n// 小数点前的数字多于21位。\n1234567890123456789012;\n// 1.2345678901234568e+21\n\n123456789012345678901;\n// 123456789012345680000\n\n// 小数点后的零多于5个。\n// 小数点后紧跟5个以上的零,就自动转为科学计数法\n0.0000003; // 3e-7\n\n// 否则,就保持原来的字面形式\n0.000003; // 0.000003\n```\n\n- 十进制:没有前导 0 的数值。\n- 八进制:有前缀 0o 或 0O 的数值,或者有前导 0、且只用到 0-7 的八个阿拉伯数字的数值。\n- 十六进制:有前缀 0x 或 0X 的数值。\n- 二进制:有前缀 0b 或 0B 的数值。\n\n有前导 0 的数值会被视为八进制,但是如果前导 0 后面有数字 8 和 9,则该数值被视为十进制。\n\n```js\n0888; // 888\n0777; // 511\n```\n\nJavaScript 的 64 位浮点数之中,有一个二进制位是符号位。这意味着,任何一个数都有一个对应的负值,就连 0 也不例外。\n\nJavaScript 内部实际上存在 2 个 0:一个是 +0,一个是 -0,区别就是 64 位浮点数表示法的符号位不同。它们是等价的。\n\n```js\n-0 === +0; // true\n0 === -0; // true\n0 === +0; // true\n\n+0; // 0\n-0; // 0\n(-0).toString(); // '0'\n(+0).toString(); // '0'\n```\n\n```js\n1 / +0 === 1 / -0; // false\n\n// +Infinity !== -Infinity\n```\n\nNaN 是 JavaScript 的特殊值,表示“非数字” Not a Number,主要出现在将字符串解析成数字出错的场合。\n\n```js\n5 - \"x\"; // NaN\n\nMath.acos(2); // NaN\nMath.log(-1); // NaN\nMath.sqrt(-1); // NaN\n\n0 / 0; // NaN\n```\n\nNaN 不是独立的数据类型,而是一个特殊数值,它的数据类型依然属于 Number。\n\n```js\ntypeof NaN; // 'number'\n```\n\n```js\nNaN === NaN; // false\n\n[NaN].indexOf(NaN); // -1\n\nBoolean(NaN); // false\n\nNaN + 32; // NaN\nNaN - 32; // NaN\nNaN * 32; // NaN\nNaN / 32; // NaN\n```\n\n```js\n// 场景一\nMath.pow(2, 1024);\n// Infinity\n\n// 场景二\n0 / 0; // NaN\n1 / 0; // Infinity\n\nInfinity === -Infinity; // false\n\n1 / -0; // -Infinity\n1 / -0; // Infinity\n\nInfinity > 1000; // true\n-Infinity < -1000; // true\n\nInfinity > NaN; // false\n-Infinity > NaN; // false\n\nInfinity < NaN; // false\n-Infinity < NaN; // false\n\n5 * Infinity; // Infinity\n5 - Infinity; // -Infinity\nInfinity / 5; // Infinity\n5 / Infinity; // 0\n\n0 * Infinity; // NaN\n0 / Infinity; // 0\nInfinity / 0; // Infinity\n\nInfinity + Infinity; // Infinity\nInfinity * Infinity; // Infinity\n\nInfinity - Infinity; // NaN\nInfinity / Infinity; // NaN\n\nnull * Infinity; // NaN\nnull / Infinity; // 0\nInfinity / null; // Infinity\n\nundefined + Infinity; // NaN\nundefined - Infinity; // NaN\nundefined * Infinity; // NaN\nundefined / Infinity; // NaN\nInfinity / undefined; // NaN\n```\n\n### 与数值相关的全局方法\n\n```js\nparseInt(\"123\"); // 123\n\n// 如果字符串头部有空格,空格会被自动去除。\n\nparseInt(\" 81\"); // 81\n\n// 如果parseInt的参数不是字符串,则会先转为字符串再转换。\n\nparseInt(1.23); // 1\n// 等同于\nparseInt(\"1.23\"); // 1\n\n// 字符串转为整数的时候,是一个个字符依次转换,如果遇到不能转为数字的字符,就不再进行下去,返回已经转好的部分。\nparseInt(\"8a\"); // 8\nparseInt(\"12**\"); // 12\nparseInt(\"12.34\"); // 12\nparseInt(\"15e2\"); // 15\nparseInt(\"15px\"); // 15\n\n// 如果字符串的第一个字符不能转化为数字(后面跟着数字的正负号除外),返回NaN。\nparseInt(\"abc\"); // NaN\nparseInt(\".3\"); // NaN\nparseInt(\"\"); // NaN\nparseInt(\"+\"); // NaN\nparseInt(\"+1\"); // 1\n\n// parseInt的返回值只有两种可能,要么是一个十进制整数,要么是NaN。\n\nparseInt(\"0x10\"); // 16\n\nparseInt(\"011\"); // 11\n```\n\n```js\n// 对于那些会自动转为科学计数法的数字,parseInt会将科学计数法的表示方法视为字符串,因此导致一些奇怪的结果。\nparseInt(1000000000000000000000.5); // 1\n// 等同于\nparseInt(\"1e+21\"); // 1\n\nparseInt(0.0000008); // 8\n// 等同于\nparseInt(\"8e-7\"); // 8\n```\n\n```js\nparseInt(\"1000\", 2); // 8\nparseInt(\"1000\", 6); // 216\nparseInt(\"1000\", 8); // 512\n\n// 第二个参数是0、undefined和null,则直接忽略。\nparseInt(\"10\", 37); // NaN\nparseInt(\"10\", 1); // NaN\nparseInt(\"10\", 0); // 10\nparseInt(\"10\", null); // 10\nparseInt(\"10\", undefined); // 10\n\nparseInt(\"1546\", 2); // 1\nparseInt(\"546\", 2); // NaN\n\n// 如果parseInt的第一个参数不是字符串,会被先转为字符串。这会导致一些令人意外的结果。\nparseInt(0x11, 36); // 43\nparseInt(0x11, 2); // 1\n\n// 等同于\nparseInt(String(0x11), 36); // 43\nparseInt(String(0x11), 2); // 1\n\n// 等同于\nparseInt(\"17\", 36); // 43\nparseInt(\"17\", 2); // 1\n// 上面代码中,十六进制的0x11会被先转为十进制的17,再转为字符串。然后,再用36进制或二进制解读字符串17,最后返回结果43和1\n\nparseInt(011, 2); // NaN\n\n// 等同于\nparseInt(String(011), 2); // NaN\n\n// 等同于\nparseInt(String(9), 2); // NaN\n```\n","tags":["javascript"]},{"title":"GROUP_CONCAT 截断","url":"/2022/01/10/group-concat-max-len/","content":"\n今日在生产环境发生了:因为 `GROUP_CONCAT` 聚合的数据列发生截断导致异常,原来 `GROUP_CONCAT` 是有大小限制的。\n\n```sql\nSELECT @@GROUP_CONCAT_MAX_LEN;\n\n-- 1024 bytes\n```\n\n## References\n\n> [group_concat_max_len | mysql](https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_group_concat_max_len)\n\n-- EOF --\n","tags":["mysql"]},{"title":"回顾 2021","url":"/2021/12/31/review-2021/","content":"\n[直到对的人来 · 追星族乐队](https://music.163.com/#/song?id=25638375)\n\n> 我想跟他说:那家餐厅太美了,我一定要和你去一次。但是那个菜其实很一般的,那个老板娘戴的耳环特别漂亮。\n\n-- EOF --\n","categories":["review"]},{"title":"PHP 安装 gRPC","url":"/2021/10/13/php-install-grpc/","content":"\n## ENV\n\n```bash\ncat /etc/redhat-release\n\nCentOS Linux release 7.2 (Final)\n\nuname -a\nLinux xxx-xxx 3.10.107-1-tlinux2_kvm_guest-0052 #1 SMP Wed Jan 15 18:42:19 CST 2020 x86_64 x86_64 x86_64 GNU/Linux\n```\n\n## PECL\n\n```bash\npecl install grpc\n```\n\n如果遇到:\n\n```bash\nConnection to `ssl://pecl.php.net:443′ failed:\n```\n\n参考:\n\n[PHP Swoft 框架环境配置 | ZYF.IM](/2020/08/07/deploy-swoft-framework/)\n\n## Build\n\n### 通用手动安装 PECL 扩展\n\nFor this to work, you’ll need to have root access to your server, and you’ll most probably need developer tools installed as well.\n\n> <https://pecl.php.net/>\n\n```bash\n# 创建临时目录\nmkdir /tmp/download/ && cd /tmp/download/\n# 下载\nwget http://pecl.php.net/get/[extension].tgz\n# 解压\ntar -xvf [extension].tgz\ncd [extension]\n# 配置\nphpize\n# 查看参数\n./configure --help\n# 设置参考 以实际情况为准\n./configure --enable-[extension]\n# 编译\nmake && make test\n# 安装\nmake install\n# 查看 ini 路径\nphp --ini\n# 开启\nvi [php-ini].ini\nextension=[extension].so\n```\n\n### gcc is currently not installed\n\n```bash\nyum install gcc\n\n...already installed and latest version...\n\ngcc\n\n...gcc is currently not installed.\n\nyum reinstall gcc\n# 重新安装后解决\n```\n\n### GrpcLb::TokenAndClientStatsAttribute::ToString() const\n\n原因:gcc 版本过低。\n\n```bash\nyum install centos-release-scl\n\nyum install devtoolset-8-gcc*\n\nscl enable devtoolset-8 bash\n\nsource /opt/rh/devtoolset-8/enable\n```\n\n## References\n\n- [安装适用于 PHP 的 gRPC | cloud.google](https://cloud.google.com/php/grpc)\n- [PHP 安装 grpc 扩展报错 | jianshu](https://www.jianshu.com/p/387b7a46d9fd)\n\n-- EOF --\n","tags":["php"]},{"title":"分组聚合 OVER PARTITION BY","url":"/2021/09/14/sql-over-partition-by/","content":"\n## 在 HIVE 中\n\n最近在使用 HIVE,需要统计 `当年累计和` 这样的指标,请教同事后发现了 `OVER(PARTITION BY)` 开窗函数。\n\n分析函数用于计算基于组的某种聚合值,它和聚合函数的不同之处是:对于每个组返回多行,而聚合函数对于每个组只返回一行。\n\n开窗函数指定了分析函数工作的数据窗口大小,这个数据窗口大小可能会随着行的变化而变化。\n\n测试语句:\n\n```sql\nCREATE TABLE default.test_over_partition (\n `fdate` Date,\n `year` Int,\n `month` Int,\n `category1` String,\n `category2` String,\n `income` Double\n);\n\nINSERT INTO hdp_fin_dash_ods.test_over_partition (`fdate`,`year`,`month`,`category1`,`category2`,`income`) VALUES\n('2020-01-01',2020,1,'3C','电脑','1010'),\n('2020-01-01',2020,1,'3C','手机','1011'),\n('2020-02-01',2020,2,'3C','电脑','1012'),\n('2020-02-01',2020,2,'3C','手机','1013'),\n('2020-03-01',2020,3,'3C','电脑','1014'),\n('2020-03-01',2020,3,'3C','手机','1015'),\n('2021-04-01',2021,4,'3C','电脑','1016'),\n('2021-04-01',2021,4,'3C','手机','1017'),\n('2021-05-01',2021,5,'3C','电脑','1018'),\n('2021-05-01',2021,5,'3C','手机','1019');\n\n-- 查询每年每 category2 日累计 income\n\nSELECT `fdate`,`year`,`month`,`category1`,`category2`,`income`\n,SUM(income) OVER (PARTITION BY `year`,`category1`,`category2` ORDER BY fdate) AS ttl_year_income\nFROM hdp_fin_dash_ods.test_over_partition;\n```\n\n<img src=\"https://user-images.githubusercontent.com/9289792/133225004-be0d2c88-e713-407b-b41d-da44e16ac960.png\" />\n\n## 在 MySQL 中\n\n> Before MySQL 8.0 you can't use window functions like ROW_NUMBER.\n\n## References\n\n- [Mysql 分组聚合实现 over partition by 功能 | cnblogs](https://www.cnblogs.com/zhwbqd/p/4205821.html)\n- [Emulating PARTITION OVER with MySQL 5.7 | stackoverflow](https://stackoverflow.com/questions/58645949/emulating-partition-over-with-mysql-5-7)\n\n-- EOF --\n","tags":["mysql","hive"]},{"title":"PHP __invoke 使用","url":"/2021/07/20/php-invoke-is-anybody-using-it/","content":"\nwhy they are magic? Because they are magically called by PHP when specific actions happen.\n\nThe `__invoke()` method is called when a script tries to call an object as a function.\n\n```php\n<?php\nclass CallableClass\n{\n public function __invoke($x)\n {\n var_dump($x);\n }\n}\n$obj = new CallableClass;\n$obj(5);\nvar_dump(is_callable($obj));\n```\n\n```bash\nint(5)\nbool(true)\n```\n\n## 使用明显的操作方法初始化\n\n例如,当我们有一个提供者时,就会发生这种情况。\n\n[aws-sdk-php/src/Endpoint/PatternEndpointProvider.php](https://github.com/aws/aws-sdk-php/blob/master/src/Endpoint/PatternEndpointProvider.php)\n\n```php\npublic function __invoke(array $args = [])\n{\n $service = isset($args['service']) ? $args['service'] : '';\n $region = isset($args['region']) ? $args['region'] : '';\n $keys = [\"{$region}/{$service}\", \"{$region}/*\", \"*/{$service}\", \"*/*\"];\n\n foreach ($keys as $key) {\n if (isset($this->patterns[$key])) {\n return $this->expand(\n $this->patterns[$key],\n isset($args['scheme']) ? $args['scheme'] : 'https',\n $service,\n $region\n );\n }\n }\n\n return null;\n}\n```\n\n它使用 invoke 使用一些参数提供端点。我们如何使用这个类?\n\n```php\npublic function testReturnsNullWhenUnresolved()\n{\n $e = new PatternEndpointProvider(['foo' => ['rules' => []]]);\n $this->assertNull($e(['service' => 'foo', 'region' => 'bar']));\n}\n```\n\n## 尝试使用单动作控制器?\n\n控制器应该大而广泛?他们不应该。我们应该有瘦控制器和胖服务。\n\n在这里,invoke 可以帮助我们,因为我们可以定义一个只处理单个动作的控制器,并在其上放置单个 invoke 方法。\n\n这也有助于我们实现单一职责原则,即 SOLID 中的 S,这是前五个面向对象设计 (OOD) 原则的首字母缩写词。\n\n> A class should have one and only one reason to change, meaning that a class should have only one job.\n\n在 Laravel 中的例子:[Single Action Controllers | laravel](https://laravel.com/docs/5.7/controllers#single-action-controllers):\n\n```php\n<?php\n\nnamespace App\\Http\\Controllers;\n\nuse App\\User;\nuse App\\Http\\Controllers\\Controller;\n\nclass ShowProfile extends Controller\n{\n /**\n * Show the profile for the given user.\n *\n * @param int $id\n * @return View\n */\n public function __invoke($id)\n {\n return view('user.profile', ['user' => User::findOrFail($id)]);\n }\n}\n```\n\n然后,在注册路由时,我们不需要指定方法名称。只有类名。\n\n```php\n<?php\nRoute::get('user/{id}', 'ShowProfile');\n```\n\nThis way we can have Single Action Controllers.\n\n## 实现回调函数 by GPT\n\n```php\nclass Sorter\n{\n public function __invoke($a, $b)\n {\n return $a <=> $b; // 比较函数\n }\n}\n\n$numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5];\nusort($numbers, new Sorter());\nprint_r($numbers); // 输出排序后的数组\n```\n\n## 实现函数式编程 by GPT\n\n```php\nclass Adder\n{\n private $value;\n\n public function __construct($value)\n {\n $this->value = $value;\n }\n\n public function __invoke($x)\n {\n return $this->value + $x;\n }\n}\n\n$add5 = new Adder(5);\n$result = $add5(3); // 结果为 8\n```\n\n## References\n\n- [PHP invoke: is anybody using it? | medium](https://luis-barros-nobrega.medium.com/php-invoke-is-anybody-using-it-1933c64d17f1)\n- [\\_\\_invoke() | php](https://www.php.net/manual/en/language.oop5.magic.php#object.invoke)\n\n-- EOF --\n","tags":["php"]},{"title":"PHP strict_types 严格模式","url":"/2021/07/19/php-strict-types/","content":"\n- 严格模式的声明 _必须_ 放在文件的顶部。\n- 严格模式不仅作用于函数参数的类型声明,也作用于函数的返回值类型。\n\n声明 PHP 文件作为严格模式的一个好事是,实际上只适用于当前文件。这确保了这个文件是严格类型,但是他没有影响到整个项目中的其他文件。这允许你一步一步的迁移非严格模式的代码。\n\n使用提示类型没有 strict_types 可能导致微妙的错误。\n\n严格类型之前,`int $x` 意味着 `$x must have a value coercible to an int`。\n\n- a float (example: 13.1459 -> 13)\n- a bool (example: true -> 1)\n- a null (example: null -> 0)\n- a string with leading digits (example: “15 Trees” -> 15)\n\n设置严格模式后,you tell the engine that `int $x` means `$x must only be an int proper, no type coercion allowed`。\n\n谁给更关心 `strict_type` 这行?is more for the reader than for the writer. Why? Bacause it will explicitly tell the reader:\n\nThe types in this current scope are treated strictly.\n\n## 对比\n\n```php\n<?php\nfunction add(int $a, int $b): int\n{\n return $a + $b;\n}\n\nvar_dump(add(1.0, 2.0));\n```\n\n运行输出 `int(3)`。\n\n```php\n<?php\ndeclare(strict_types=1);\nfunction add(int $a, int $b): int\n{\n return $a + $b;\n}\n\nvar_dump(add(1.0, 2.0));\n```\n\n运行输出:\n\n```bash\nPHP Fatal error: Uncaught TypeError: Argument 1 passed to add() must be of the type int, float given, ...\n```\n\n## 声明位置\n\n必须在脚本最前。不能写在脚本的中间,如下写法是错误的:\n\n```php\n<?php\nfunction add(int $a, int $b): int\n{\n return $a + $b;\n}\ndeclare(strict_types=1);\n\nvar_dump(add(1.0, 2.0));\n```\n\n运行后报错:\n\n```bash\nPHP Fatal error: strict_types declaration must be the very first statement in the script in ...\n```\n\n不得使用 block mode 进行声明:\n\n```php\n<?php\ndeclare(strict_types=1) {\n var_dump(1);\n}\n```\n\n运行后报错:\n\n```bash\nPHP Fatal error: strict_types declaration must not use block mode in ...\n```\n\n## 多文件场景\n\n### 例子 1\n\n`A.php`\n\n```php\n<?php\n// 严格模式\ndeclare(strict_types=1);\nfunction add(int $a, int $b): int\n{\n return $a + $b;\n}\n```\n\n`B.php`\n\n```php\n<?php\n// 非严格模式\nrequire 'A.php';\n// 违反了 A 的定义\nvar_dump(add(1.0, 2.0));\n```\n\n运行\n\n```bash\nphp B.php\n\nint(3)\n```\n\n### 例子 2\n\n`A.php`\n\n```php\n<?php\n// 非严格模式\nfunction add(int $a, int $b): int\n{\n return $a + $b;\n}\n```\n\n`B.php`\n\n```php\n<?php\n// 严格模式\ndeclare(strict_types=1);\nrequire 'A.php';\n// 违反了 A 的定义\nvar_dump(add(1.0, 2.0));\n```\n\n运行\n\n```bash\nphp B.php\n\nPHP Fatal error: Uncaught TypeError: Argument 1 passed to add() must be of the type int, float given, called in ...\n```\n\n### 小结\n\n- 函数定义时的严格模式,行为并不会出现什么不同。\n- 函数执行时的,严格模式会出现差异。\n- `declare(strict_types=1);` 的声明本身在 A.php 文件中完成。被 B.php 文件 require,而 B.php 并没有定义严格模式,那么执行 require 的 B.php 文件不会变成严格模式。\n\n## 总结\n\n只有在写 declare 的文件的执行部分才会执行严格模式,该文件中调用的其它函数(其它文件中的函数)也会被影响。\n\n若果想完全使用严格模式,比较简单的方法是在所有 php 文件都写上 `declare(strict_types=1);`。\n\n## 工具\n\n推荐自动格式化工具:[symplify/easy-coding-standard](https://github.com/symplify/easy-coding-standard)。\n\n## References\n\n- [关于 declare(strict_types=1) 的有效范围 | segmentfault](https://segmentfault.com/a/1190000018389227)\n- [Strict Types in PHP | medium](https://chemaclass.medium.com/strict-types-in-php-d4166bd25394)\n- [Scalar type declarations | php](https://www.php.net/manual/en/migration70.new-features.php#migration70.new-features.scalar-type-declarations)\n- [Strict typing | php](https://www.php.net/manual/en/language.types.declarations.php#language.types.declarations.strict)\n\n-- EOF --\n","tags":["php"]},{"title":"Git and GitHub Secrets","url":"/2021/07/12/git-and-github-secrets/","content":"\n## 记住密码\n\nGit 记住密码配置后,不用每次 pull、push 都需要输入密码:\n\n```bash\ngit config --global credential.helper store\n```\n\n会在 `cat ~/.gitconfig` 看到:\n\n```bash\n[credential]\n helper = store\n```\n\n## 快速检出上一个分支\n\n```bash\ngit checkout -\n```\n\n## 提交空改动\n\n```bash\ngit commit -m \"empty commit\" --allow-empty\n```\n\n在如下几种情况下是有意义:\n\n- 标记一批工作或一个新功能的开始。\n- 记录你对项目进行了跟代码无关的改动。\n- 跟使用你仓库的其他人交流。\n- 作为仓库的第一次提交,因为第一次提交日后是不能被 rebase 的:`git commit -m \"init repo\" --allow-empty`。\n\n## 更直观的 status\n\n```bash\ngit status -sb\n```\n\n## 更直观的 log\n\n```bash\ngit log --all --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an %ae>%Creset' --abbrev-commit --date=relative\n```\n\n## 提交信息查询\n\n找到其中和搜索条件相匹配的最近的一条。query (区别大小写)是你想要搜索的词语。\n\n```bash\ngit show :/query\n```\n\n## 分支合并\n\n显示所有已经合并到你当前分支的分支列表:\n\n```bash\ngit branch --merged\n```\n\n相反地:\n\n```bash\ngit branch --no-merged\n```\n\n## .gitconfig\n\n打开编辑:\n\n```bash\nvim ~/.gitconfig\n```\n\n命令修改:\n\n```bash\ngit config --global alias.co 'checkout'\ngit config --global alias.ac 'add -A . && commit'\ngit config --global alias.lg \"log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an %ae>%Creset' --abbrev-commit\"\n```\n\n## GitHub\n\n### 整行高亮\n\n多行高亮也可以,比如用 #L53-L60 选择范围,或者按住 shift 键,然后再点击选择的两行。\n\n```bash\nhttps://github.com/rails/rails/blob/master/activemodel/lib/active_model.rb#L53-L60\n```\n\n### 用 commit 信息关闭 issue\n\n如果某个提交修复了一个 Issue,当提交到 master 分支时,提交信息里可以使用 fix/fixes/fixed , close/closes/closed 或者 resolve/resolves/resolved 等关键词,后面再跟上 Issue 号,这样就会关闭这个 Issue。\n\n```bash\ngit commit -m \"Fix screwup, fixes #12\"\n```\n\n### 链接其他仓库的 Issue\n\n如果你想引用到同一个仓库中的一个 Issue,只需使用井号 # 加上 Issue 号,这样就会自动创建到此 Issue 的链接。\n\n要链接到其他仓库的 Issue,就使用 `user_name/repo_name#ISSUE_NUMBER` 的方式,例如 `tiimgreen/toc#12`。\n\n## Reference\n\n- [Git and GitHub Secrets | speakerdeck](https://speakerdeck.com/holman/git-and-github-secrets)\n- [More Git and GitHub Secrets | speakerdeck](https://speakerdeck.com/holman/more-git-and-github-secrets)\n\n-- EOF --\n","tags":["git"]},{"title":"如何做好技术协同及管理","url":"/2021/07/08/how-to-do-technical-management/","content":"\n《如何做好技术协同及管理 —— 合作伙伴篇》一次沙龙后的笔记。\n\n## 招人\n\n- 注重招人环节;在这一步要卡严,因为入职后再折腾就更费时费力了。\n- 要关注的点:基础水平(工程能力)、离职原因(是否能长久)、个人性格(气场是否相合)。\n\n## 入职\n\n- 主动沟通是重要的特质。还有责任心、可塑性、执行力。\n- 前紧后松;前期要做 code review,养成好习惯。\n- 关注测试同学以及其他同学对其的反馈。了解工作状态、质量。\n- 在突破底线后应该当机立断,无需心存幻想。\n\n## 心态\n\n- 没有归宿感;owner 意识。\n- 每一位都是组内的一员。\n\n## 管理\n\n- 早会;任务安排,跟踪进度。不能不闻不问,最后容易抓瞎。\n- 超过 30min 解决不了的问题,直接沟通,避免团队时间的浪费。\n- 读书会,组内分享;打造一个好的技术氛围,这里不只是工作,还能成长。\n- 要对任务的工作量有判断;并且明确开发规模,并且进行核对,防止 “大事小做,小题大做”。\n- 通过先编写好单元测试、框架结构、interface,控制需求实现、代码质量。\n- 在无法进行横向对比的情况下,可以对比加入前后是否释放自身生产力来进行判断。\n- 三个卡点:需求评审、技术实现评审、测试用例评审,把关质量。\n\n-- EOF --\n","tags":["thinking"]},{"title":"设计模式之美 Part2","url":"/2021/02/06/the-beauty-of-design-patterns-part2/","content":"\n## 11\n\n领域驱动设计(Domain Driven Design,简称 DDD)。\n\n### 什么是基于贫血模型的传统开发模式?\n\nUserEntity 和 UserRepository 组成了数据访问层,UserBo 和 UserService 组成了业务逻辑层,UserVo 和 UserController 在这里属于接口层。\n\nService 层的数据和业务逻辑,被分割为 BO 和 Service 两个类中。像 UserBo 这样,只包含数据,不包含业务逻辑的类,就叫作贫血模型(Anemic Domain Model)。\n\n这种贫血模型将数据与操作分离,破坏了面向对象的封装特性,是一种典型的面向过程的编程风格。\n\n### 什么是基于充血模型的 DDD 开发模式?\n\n领域驱动设计,即 DDD,主要是用来指导如何解耦业务系统,划分业务模块,定义业务领域模型及其交互。\n\n基于贫血模型的传统的开发模式,重 Service 轻 BO;基于充血模型的 DDD 开发模式,轻 Service 重 Domain。\n\n### 为什么基于贫血模型的传统开发模式如此受欢迎?\n\n- 系统业务可能都比较简单,简单到就是基于 SQL 的 CRUD 操作\n- 充血模型的设计要比贫血模型更加有难度。我们从一开始就要设计好针对数据要暴露哪些操作,定义哪些业务逻辑。而不是像贫血模型那样,我们只需要定义数据,之后有什么功能开发需求,我们就在 Service 层定义什么操作,不需要事先做太多设计。\n- 思维已固化,转型有成本。\n\n### 什么项目应该考虑使用基于充血模型的 DDD 开发模式?\n\n适合业务复杂的系统开发。比如,包含各种利息计算模型、还款模型等复杂业务的金融系统。\n\n两种不同的开发模式会导致不同的开发流程。基于充血模型的 DDD 开发模式的开发流程,在应对复杂业务系统的开发的时候更加有优势。\n\nDDD 这种开发模式下,我们需要事先理清楚所有的业务,定义领域模型所包含的属性和方法。领域模型相当于可复用的业务中间层。新功能需求的开发,都基于之前定义好的这些领域模型来完成。\n\n## 12\n\n### 一个虚拟钱包系统\n\n充值、提现、支付、查询余额、查询交易流水。甚至还有冻结、透支、转赠等。\n\n整个钱包系统一部分单纯跟应用内的虚拟钱包账户打交道,另一部分单纯跟银行账户打交道。我们基于这样一个业务划分,给系统解耦,将整个钱包系统拆分为两个子系统:虚拟钱包系统和三方支付系统。\n\n![image](https://user-images.githubusercontent.com/9289792/107135323-ba50b780-6934-11eb-8b85-1738440f42bc.png)\n\n不保证数据的强一致性,只实现数据的最终一致性。\n\n```java\npublic class VirtualWalletService {\n // 通过构造函数或者 IOC 框架注入\n private VirtualWalletRepository walletRepo;\n private VirtualWalletTransactionRepository transactionRepo;\n\n public VirtualWallet getVirtualWallet(Long walletId ) {\n VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);\n VirtualWallet wallet = convert(walletEntity);\n return wallet;\n }\n\n public BigDecimal getBalance(Long walletId) {\n return virtualWalletRepo.getBalance(walletId);\n }\n\n public void debit(Long walletId, BigDecimal amount) {\n VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);\n\n // 贫血型\n // BigDecimal balance = walletEntity.getBalance();\n // if (balance.compareTo(amount) < 0) {\n // throw new NoSufficientBalanceException(...);\n // }\n // walletRepo.updateBalance(walletId, balance.subtract(amount));\n\n // DDD\n VirtualWallet wallet = convert(walletEntity);\n wallet.debit(amount);\n walletRepo.updateBalance(walletId, wallet.balance());\n }\n\n public void credit(Long walletId, BigDecimal amount) {\n VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);\n\n // 贫血型\n // BigDecimal balance = walletEntity.getBalance();\n // walletRepo.updateBalance(walletId, balance.add(amount));\n\n // DDD\n VirtualWallet wallet = convert(walletEntity);\n wallet.credit(amount);\n walletRepo.updateBalance(walletId, wallet.balance());\n }\n\n public void transfer(Long fromWalletId, Long toWalletId, BigDecimal amount) {\n VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactionEntity();\n transactionEntity.setAmount(amount);\n transactionEntity.setCreateTime(System.currentTimeMillis());\n transactionEntity.setFromWalletId(fromWalletId);\n transactionEntity.setToWalletId(toWalletId);\n transactionEntity.setStatus(Status.TO_BE_EXECUTED);\n Long transactionId = transactionRepo.saveTransaction(transactionEntity);\n try {\n debit(fromWalletId, amount);\n credit(toWalletId, amount);\n } catch (InsufficientBalanceException e) {\n transactionRepo.updateStatus(transactionId, Status.CLOSED);\n // ...rethrow exception e...\n } catch (Exception e) {\n transactionRepo.updateStatus(transactionId, Status.FAILED);\n // ...rethrow exception e...\n }\n transactionRepo.updateStatus(transactionId, Status.EXECUTED);\n }\n}\n```\n\n领域模型 VirtualWallet 类很单薄,包含的业务逻辑很简单。相对于原来的贫血模型的设计思路,这种充血模型的设计思路,貌似并没有太大优势。这也是大部分业务系统都使用基于贫血模型开发的原因。不过,如果虚拟钱包系统需要支持更复杂的业务逻辑,那充血模型的优势就显现出来了。比如,我们要支持透支一定额度和冻结部分余额的功能。这个时候,我们重新来看一下 VirtualWallet 类的实现代码。\n\n```java\npublic class VirtualWallet {\n private Long id;\n private Long createTime = System.currentTimeMillis();\n private BigDecimal balance = BigDecimal.ZERO;\n private boolean isAllowedOverdraft = true;\n private BigDecimal overdraftAmount = BigDecimal.ZERO;\n private BigDecimal frozenAmount = BigDecimal.ZERO;\n\n public VirtualWallet(Long preAllocatedId) {\n this.id = preAllocatedId;\n }\n\n public BigDecimal balance() {\n return this.balance;\n }\n\n public BigDecimal getAvaliableBalance() {\n BigDecimal totalAvaliableBalance = this.balance.subtract(this.frozenAmount);\n if (isAllowedOverdraft) {\n totalAvaliableBalance += this.overdraftAmount;\n }\n return totalAvaliableBalance;\n }\n\n public void debit(BigDecimal amount) {}\n public void credit(BigDecimal amount) {}\n\n public void freeze(BigDecimal amount) { ... }\n public void unfreeze(BigDecimal amount) { ...}\n public void increaseOverdraftAmount(BigDecimal amount) { ... }\n public void decreaseOverdraftAmount(BigDecimal amount) { ... }\n public void closeOverdraft() { ... }\n public void openOverdraft() { ... }\n}\n```\n\n如果功能继续演进,我们可以增加更加细化的冻结策略、透支策略、支持钱包账号(VirtualWallet id 字段)自动生成的逻辑(不是通过构造函数经外部传入 ID,而是通过分布式 ID 生成算法来自动生成 ID)等等。\n\n### 辩证思考与灵活应用\n\n两种代码设计与实现中,并没有完全将 Service 类去掉,这是为什么?或者说,Service 类在这种情况下担当的职责是什么?哪些功能逻辑会放到 Service 类中?\n\n1. Service 类负责与 Repository 交流。获取数据库中的数据,转化成领域模型 VirtualWallet,然后由领域模型 VirtualWallet 来完成业务逻辑,最后调用 Repository 类的方法,将数据存回数据库。之所以让 VirtualWalletService 类与 Repository 打交道,而不是让领域模型 VirtualWallet 与 Repository 打交道,那是因为我们想保持领域模型的独立性,不与任何其他层的代码(Repository 层的代码)或开发框架(比如 Spring、MyBatis)耦合在一起,将流程性的代码逻辑(比如从 DB 中取数据、映射数据)与领域模型的业务逻辑解耦,让领域模型更加可复用。\n2. Service 类负责跨领域模型的业务聚合功能。VirtualWalletService 类中的 transfer() 转账函数会涉及两个钱包的操作,因此这部分业务逻辑无法放到 VirtualWallet 类中,所以,我们暂且把转账业务放到 VirtualWalletService 类中了。当然,虽然功能演进,使得转账业务变得复杂起来之后,我们也可以将转账业务抽取出来,设计成一个独立的领域模型。\n3. Service 类负责一些非功能性及与三方系统交互的工作。比如幂等、事务、发邮件、发消息、记录日志、调用其他系统的 RPC 接口等,都可以放到 Service 类中。\n\n### 小结\n\n基于充血模型的 DDD 开发模式跟基于贫血模型的传统开发模式相比,主要区别在 Service 层。在基于充血模型的开发模式下,我们将部分原来在 Service 类中的业务逻辑移动到了一个充血的 Domain 领域模型中,让 Service 类的实现依赖这个 Domain 类。\n\n在基于充血模型的 DDD 开发模式下,Service 类并不会完全移除,而是负责一些不适合放在 Domain 类中的功能。比如,负责与 Repository 层打交道、跨领域模型的业务聚合功能、幂等事务等非功能性的工作。\n\n基于充血模型的 DDD 开发模式跟基于贫血模型的传统开发模式相比,Controller 层和 Repository 层的代码基本上相同。这是因为,Repository 层的 Entity 生命周期有限,Controller 层的 VO 只是单纯作为一种 DTO。两部分的业务逻辑都不会太复杂。业务逻辑主要集中在 Service 层。所以,Repository 层和 Controller 层继续沿用贫血模型的设计思路是没有问题的。\n\n遗留问题:Entity 与 Domain 的转换应该放在哪里?\n\n## 13 如何对接口鉴权这样一个功能开发做面向对象分析?\n\n面向对象分析(OOA)、面向对象设计(OOD)、面向对象编程(OOP),是面向对象开发的三个主要环节。\n\n我们需要通过沟通、挖掘、分析、假设、梳理,搞清楚具体的需求有哪些,哪些是现在要做的,哪些是未来可能要做的,哪些是不用考虑做的。\n\n> 加密之后的密码及 AppID,可能被 **重放攻击**。\n\n调用方将请求接口的 URL 跟 AppID、密码拼接在一起,然后进行加密,生成一个 token。\n\n![image](https://user-images.githubusercontent.com/9289792/107141538-3ca4a000-6964-11eb-903f-404295e31ebf.png)\n\n这样的设计仍然存在重放攻击的风险。\n\n为了解决这个问题,我们可以进一步优化 token 生成算法,引入一个随机变量,让每次接口请求生成的 token 都不一样。\n\n微服务端在收到这些数据之后,会验证当前时间戳跟传递过来的时间戳,是否在一定的时间窗口内(比如一分钟)。\n\n![image](https://user-images.githubusercontent.com/9289792/107141588-ba68ab80-6964-11eb-8cc4-c84ddf91a6d4.png)\n\n## 14\n\n1. 把 URL、AppID、密码、时间戳拼接为一个字符串;\n2. 对字符串通过加密算法加密生成 token;\n3. 将 token、AppID、时间戳拼接到 URL 中,形成新的 URL;\n4. 解析 URL,得到 token、AppID、时间戳等信息;\n5. 从存储中取出 AppID 和对应的密码;\n6. 根据时间戳判断 token 是否过期失效;\n7. 验证两个 token 是否匹配;\n\n1、2、6、7 都是跟 token 有关,负责 token 的生成、验证;3、4 都是在处理 URL,负责 URL 的拼接、解析;5 是操作 AppID 和密码,负责从存储中读取 AppID 和密码。AuthToken、Url、CredentialStorage。\n\n## 15\n","tags":["design-pattern"]},{"title":"设计模式之美 Part1","url":"/2021/02/05/the-beauty-of-design-patterns-part1/","content":"\n## 00\n\nKISS 原则(Keep It Simple and Stupid),这个原则理解起来很简单,一看貌似就懂了,那我问你,怎样的代码才算是足够简单呢?怎样才算不够简单需要优化呢?\n\n“Talk is cheap, show me the code.”\n\n## 01\n\n为什么要学习设计模式:应对面试中的设计模式相关问题;告别写被人吐槽的烂代码;提高复杂代码的设计和开发能力;让读源码、学框架事半功倍;为你的职场发展做铺垫。\n\n## 02\n\n灵活性(flexibility)、可扩展性(extensibility)、可维护性(maintainability)、可读性(readability)、可理解性(understandability)、易修改性(changeability)、可复用(reusability)、可测试性(testability)、模块化(modularity)、高内聚低耦合(high cohesion loose coupling)、高效(high effciency)、高性能(highperformance)、安全性(security)、兼容性(compatibility)、易用性(usability)、整洁(clean)、清晰(clarity)、简单(simple)、直接(straightforward)、少即是多(less code is more)、文档详尽(well-documented)、分层清晰(well-layered)、正确性(correctness、bug free)、健壮性(robustness)、鲁棒性(robustness)、可用性(reliability)、可伸缩性(scalability)、稳定性(stability)、优雅(elegant)、好(good)、坏(bad)\n\n我们并不能通过单一的维度去评价一段代码写的好坏。比如,即使一段代码的可扩展性很好,但可读性很差,那我们也不能说这段代码质量高。\n\n如果用数字来量化代码的可读性的话,它应该是一个连续的区间值,而非 0、1 这样的离散值。\n\n代码质量的评价有很强的主观性。\n\n有些词语过于笼统、抽象,比较偏向对于整体的描述,比如优雅、好、坏、整洁、清晰等;有些过于细节、偏重方法论,比如模块化、高内聚低耦合、文档详尽、分层清晰等;有些可能并不仅仅局限于编码,跟架构设计等也有关系,比如可伸缩性、可用性、稳定性等。\n\n### 可维护性(maintainability)\n\n破坏原有代码设计、不引入新的 bug 的情况下,能够快速地修改或者添加代码。与之相反,修改或者添加代码需要冒着极大的引入新 bug 的风险,并且需要花费很长的时间才能完成。\n\n码分层清晰、模块化好、高内聚低耦合、遵从基于接口而非实现编程的设计原则等等,那就可能意味着代码易维护。\n\n### 可读性(readability)\n\n“任何傻瓜都会编写计算机能理解的代码。好的程序员能够编写人能够理解的代码。”\n\n是否符合编码规范、命名是否达意、注释是否详尽、函数是否长短合适、模块划分是否清晰、是否符合高内聚低耦合等等。\n\ncode review 是一个很好的测验代码可读性的手段。如果你的同事可以轻松地读懂你写的代码,那说明你的代码可读性很好;如果同事在读你的代码时,有很多疑问,那就说明你的代码可读性有待提高了。\n\n### 可扩展性(extensibility)\n\n我们在不修改或少量修改原有代码的情况下,通过扩展的方式添加新的功能代码。说直白点就是,代码预留了一些功能扩展点,你可以把新功能代码,直接插到扩展点上,而不需要因为要添加一个功能而大动干戈,改动大量的原始代码。\n\n“对修改关闭,对扩展开放”。\n\n### 灵活性(flexibility)\n\n如果一段代码易扩展、易复用或者易用,我们都可以称这段代码写得比较灵活。\n\n- 当我们添加一个新的功能代码的时候,原有的代码已经预留好了扩展点,我们不需要修改原有的代码,只要在扩展点上添加新的代码即可。这个时候,我们除了可以说代码易扩展,还可以说代码写得好灵活。\n- 当我们要实现一个功能的时候,发现原有代码中,已经抽象出了很多底层可以复用的模块、类等代码,我们可以拿来直接使用。这个时候,我们除了可以说代码易复用之外,还可以说代码写得好灵活。\n- 当我们使用某组接口的时候,如果这组接口可以应对各种使用场景,满足各种不同的需求,我们除了可以说接口易用之外,还可以说这个接口设计得好灵活或者代码写得好灵活。\n\n### 简洁性(simplicity)\n\n尽量保持代码简单。代码简单、逻辑清晰,也就意味着易读、易维护。我们在编写代码的时候,往往也会把简单、清晰放到首位。\n\nKISS 原则,思从深而行从简,真正的高手能云淡风轻地用最简单的方法解决最复杂的问题。这也是一个编程老手跟编程新手的本质区别之一。\n\n### 可复用性(reusability)\n\n尽量减少重复代码的编写,复用已有的代码。\n\n当讲到面向对象特性的时候,我们会讲到继承、多态存在的目的之一,就是为了提高代码的可复用性;当讲到设计原则的时候,我们会讲到单一职责原则也跟代码的可复用性相关;当讲到重构技巧的时候,我们会讲到解耦、高内聚、模块化等都能提高代码的可复用性。可见,可复用性也是一个非常重要的代码评价标准,是很多设计原则、思想、模式等所要达到的最终效果。\n\nDRY(Don’t Repeat Yourself)设计原则。\n\n### 可测试性(testability)\n\n代码可测试性的好坏,能从侧面上非常准确地反应代码质量的好坏。代码的可测试性差,比较难写单元测试,那基本上就能说明代码设计得有问题。\n\n## 03\n\n### 面向对象\n\n- 面向对象的四大特性:封装、抽象、继承、多态面\n- 向对象编程与面向过程编程的区别和联系\n- 面向对象分析、面向对象设计、面向对象编程\n- 接口和抽象类的区别以及各自的应用场景\n- 基于接口而非实现编程的设计思想\n- 多用组合少用继承的设计思想\n- 面向过程的贫血模型和面向对象的充血模型\n\n### 设计原则\n\n指导我们代码设计的一些经验总结。\n\n- SOLID 原则 SRP 单一职责原则\n- SOLID 原则 OCP 开闭原则\n- SOLID 原则 SP 里式替换原则\n- SOLID 原则 ISP 接口隔离原则\n- SOLID 原则 DIP 依赖倒置原则\n- DRY 原则、KISS 原则、YAGNI 原则、LOD 法则\n\n### 设计模式\n\n针对软件开发中经常遇到的一些设计问题,总结出来的一套解决方案或者设计思路。\n\n大部分设计模式要解决的都是代码的可扩展性问题。\n\n1. 创建型 常用的有:单例模式、工厂模式(工厂方法和抽象工厂)、建造者模式。不常用的有:原型模式。\n2. 结构型 常用的有:代理模式、桥接模式、装饰者模式、适配器模式。不常用的有:门面模式、组合模式、享元模式。\n3. 行为型 常用的有:观察者模式、模板模式、策略模式、职责链模式、迭代器模式、状态模式。不常用的有:访问者模式、备忘录模式、命令模式、解释器模式、中介模式。\n\n### 编程规范\n\n主要解决的是代码的可读性问题。\n\n### 代码重构\n\n在软件开发中,只要软件在不停地迭代,就没有一劳永逸的设计。\n\n在开发初期,除非特别必须,我们一定不要过度设计,应用复杂的设计模式。而是当代码出现问题的时候,我们再针对问题,应用原则和模式进行重构。这样就能有效避免前期的过度设计。\n\n- 重构的目的(why)、对象(what)、时机(when)、方法(how);\n- 保证重构不出错的技术手段:单元测试和代码的可测试性;\n- 两种不同规模的重构:大重构(大规模高层次)和小重构(小规模低层次)。\n\n### 五者联系\n\n面向对象编程因为其具有丰富的特性(封装、抽象、继承、多态),可以实现很多复杂的设计思路,是很多设计原则、设计模式等编码实现的基础。\n\n设计原则是指导我们代码设计的一些经验总结,对于某些场景下,是否应该应用某种设计模式,具有指导意义。比如,“开闭原则”是很多设计模式(策略、模板等)的指导原则。\n\n设计模式是针对软件开发中经常遇到的一些设计问题,总结出来的一套解决方案或者设计思路。应用设计模式的主要目的是提高代码的可扩展性。从抽象程度上来讲,设计原则比设计模式更抽象。设计模式更加具体、更加可执行。\n\n编程规范主要解决的是代码的可读性问题。编码规范相对于设计原则、设计模式,更加具体、更加偏重代码细节、更加能落地。持续的小重构依赖的理论基础主要就是编程规范。\n\n重构作为保持代码质量不下降的有效手段,利用的就是面向对象、设计原则、设计模式、编码规范这些理论。\n\n![image](https://user-images.githubusercontent.com/9289792/106997160-59eb3a00-67bd-11eb-8f71-db5336d3e77f.png)\n\n## 04\n\n面向对象编程是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石。\n\n## 05\n\n### 封装(Encapsulation)\n\n封装也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方式来访问内部信息或者数据。\n\n封装特性存在的意义,一方面是保护数据不被随意修改,提高代码的可维护性;另一方面是仅暴露有限的必要接口,提高类的易用性。\n\n### 抽象(Abstraction)\n\n抽象可以通过接口类或者抽象类来实现,但也并不需要特殊的语法机制来支持。\n\n抽象存在的意义,一方面是提高代码的可扩展性、维护性,修改实现不需要改变定义,减少代码的改动范围;另一方面,它也是处理复杂系统的有效手段,能有效地过滤掉不必要关注的信息。\n\n提供“函数”这一非常基础的语法机制,就可以实现抽象特性、所以,它没有很强的“特异性”,有时候并不被看作面向对象编程的特性之一。\n\n在定义(或者叫命名)类的方法的时候,也要有抽象思维,不要在方法定义中,暴露太多的实现细节,以保证在某个时间点需要改变方法的实现逻辑的时候,不用去修改其定义。getPictureUrl 好于 getAliyunPictureUrl。\n\n### 继承(Inheritance)\n\n继承是用来表示类之间的 is-a 关系。继承主要是用来解决代码复用的问题。\n\n多用组合少用继承。\n\n### 多态(Polymorphism)\n\n多态是指子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。\n\n多态这种特性也需要编程语言提供特殊的语法机制来实现,比如继承、接口类、duck-typing。多态可以提高代码的扩展性和复用性,是很多设计模式、设计原则、编程技巧的代码实现基础。\n\n只要两个类具有相同的方法,就可以实现多态,并不要求两个类之间有任何关系,这就是所谓的 duck-typing,是一些动态语言所特有的语法机制。\n\n## 06\n\n相较于面向对象编程以类为组织代码的基本单元,面向过程编程则是以过程(或方法)作为组织代码的基本单元。它最主要的特点就是数据和方法相分离。相较于面向对象编程语言,面向过程编程语言最大的特点就是不支持丰富的面向对象编程特性,比如继承、多态、封装。\n\n面向对象编程相比面向过程编程有哪些优势?\n\n- 对于大规模复杂程序的开发,程序的处理流程并非单一的一条主线,而是错综复杂的网状结构。\n- 面向对象编程比起面向过程编程,更能应对这种复杂类型的程序开发。面向对象编程相比面向过程编程,具有更加丰富的特性(封装、抽象、继承、多态)。利用这些特性编写出来的代码,更加易扩展、易复用、易维护。\n- 从编程语言跟机器打交道的方式的演进规律中,我们可以总结出:面向对象编程语言比起面向过程编程语言,更加人性化、更加高级、更加智能。\n\n## 07\n\n### 滥用 getter、setter 方法\n\n尽管 getter 方法相对 setter 方法要安全些,但是如果返回的是集合容器(比如例子中的 List 容器),也要防范集合内部数据被修改的危险。\n\n```java\npublic class ShoppingCart {\n // ... 省略其他代码...\n public List<ShoppingCartItem> getItems() {\n return Collections.unmodifiableList(this.items);\n }\n}\npublic class UnmodifiableList<E> extends UnmodifiableCollection<E> implements List<E> {\n public boolean add(E e) {\n throw new UnsupportedOperationException();\n }\n public void clear() {\n throw new UnsupportedOperationException();\n }\n // ... 省略其他代码...\n}\n```\n\n### 滥用全局变量和全局方法\n\nConstants 类、Utils 类的设计尽量能做到职责单一,定义一些细化的小类。\n\n静态成员变量归属于类上的数据,被所有的实例化对象所共享,也相当于一定程度上的全局变量。\n\n静态方法将方法与数据分离,破坏了封装特性,是典型的面向过程风格。\n\n只包含静态方法不包含任何属性的 Utils 类,是彻彻底底的面向过程的编程风格。要尽量避免滥用,不要不加思考地随意去定义 Utils 类。\n\n### 定义数据和方法分离的类\n\nController 层负责暴露接口给前端调用,Service 层负责核心业务逻辑,Repository 层负责数据读写。\n\n而在每一层中,我们又会定义相应的 VO(View Object)、BO(Business Object)、Entity。一般情况下,VO、BO、Entity 中只会定义数据,不会定义方法,所有操作这些数据的业务逻辑都定义在对应的 Controller 类、Service 类、Repository 类中。这就是典型的面向过程的编程风格。\n\n> 实际上,这种开发模式叫作基于 `贫血模型的开发模式`,也是我们现在非常常用的一种 Web 项目的开发模式。看到这里,你内心里应该有很多疑惑吧?既然这种开发模式明显违背面向对象的编程风格,为什么大部分 Web 项目都是基于这种开发模式来开发呢?\n\n### 在面向对象编程中,为什么容易写出面向过程风格的代码?\n\n面向过程编程风格恰恰符合人的这种流程化思维方式。而面向对象编程风格正好相反。它是一种自底向上的思考方式。\n\n它不是先去按照执行流程来分解任务,而是将任务翻译成一个一个的小的模块(也就是类),设计类之间的交互,最后按照流程将类组装起来,完成整个任务。\n\n在面向对象编程中,类的设计还是挺需要技巧,挺需要一定设计经验的。你要去思考如何封装合适的数据和方法到一个类里,如何设计类之间的关系,如何设计类之间的交互等等诸多设计问题。\n\n### 面向过程编程及面向过程编程语言就真的无用武之地了吗?\n\n如果我们开发的是微小程序,或者是一个数据处理相关的代码,以算法为主,数据为辅,那脚本式的面向过程的编程风格就更适合一些。\n\n## 08\n\n### 抽象类和接口的语法特性\n\n抽象类不允许被实例化,只能被继承。它可以包含属性和方法。方法既可以包含代码实现,也可以不包含代码实现。不包含代码实现的方法叫作抽象方法。子类继承抽象类,必须实现抽象类中的所有抽象方法。接口不能包含属性,只能声明方法,方法不能包含代码实现。类实现接口的时候,必须实现接口中声明的所有方法。\n\n### 抽象类和接口存在的意义\n\n抽象类是对成员变量和方法的抽象,是一种 is-a 关系,是为了解决代码复用问题。接口仅仅是对方法的抽象,是一种 has-a 关系,表示具有某一组行为特性,是为了解决解耦问题,隔离接口和具体的实现,提高代码的扩展性。\n\n从类的继承层次上来看,抽象类是一种自下而上的设计思路,先有子类的代码重复,然后再抽象成上层的父类(也就是抽象类)。而接口正好相反,它是一种自上而下的设计思路。\n\n## 09\n\n“Program to an interface, not animplementation”。“基于抽象而非实现编程”。\n\n越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。好的代码设计,不仅能应对当下的需求,而且在将来需求发生变化的时候,仍然能够在不破坏原有代码设计的情况下灵活应对。\n\n“细节是魔鬼”。\n\n1. 函数的命名不能暴露任何实现细节。\n2. 封装具体的实现细节。\n3. 为实现类定义抽象的接口。\n\n抽象意识、封装意识、接口意识。\n\n## 10\n\n继承层次过深、过复杂,也会影响到代码的可维护性。\n\n鸟 -> 会飞、不会飞、会叫、不会叫、会下蛋、不会下蛋。\n\n利用组合(composition)、接口、委托(delegation 解决。\n\n```java\n// 接口\npublic interface Flyable {\n void fly();\n}\n\n// 提高复用性\npublic class FlyAbility implements Flyable {\n @Override\n public void fly() {\n //...\n }\n}\n\npublic class Sparrow implements Flyable {\n // 组合\n private FlyAbility flyAbility = new FlyAbility();\n @Override\n public void fly() {\n // 委托\n flyAbility.fly();\n }\n}\n```\n\n继承改写成组合意味着要做更细粒度的类的拆分。这也就意味着,我们要定义更多的类和接口。类和接口的增多也就或多或少地增加代码的复杂程度和维护成本。\n\n组合并不完美,继承也不是一无是处。\n\n如果类之间的继承结构稳定,层次比较浅,关系不复杂,我们就可以大胆地使用继承。反之,我们就尽量使用组合来替代继承。除此之外,还有一些设计模式、特殊的应用场景,会固定使用继承(模板模式(template pattern))或者组合(装饰者模式(decoratorpattern)、策略模式(strategy pattern)、组合模式(composite pattern))。\n\n-- EOF --\n","tags":["design-pattern"]},{"title":"MySQL Code Snippet","url":"/2021/01/29/mysql-code-snippet/","content":"\n## Tips SQL\n\n```sql\n-- 追踪优化器 Trace 功能\n-- optimizer_trace_enabled=1\n-- optimizer_trace_file=optimizer_trace.log\nSELECT @@optimizer_trace;\nSET optimizer_trace = 'enabled=on';\n-- <your query>;\nSET optimizer_trace = 'enabled=off';\nselect * from INFORMATION_SCHEMA.OPTIMIZER_TRACE;\n\n-- 查看优化后的 SQL\n-- 在联表查询时比较有效果\nEXPLAIN <你的 SQL>;\nSHOW WARNINGS;\n\n-- 查看处理\nSHOW PROCESSLIST;\n\n-- 查看结构\nDESC user;\nSHOW COLUMNS FROM user;\nDESCRIBE user;\n```\n\n## 视图更新\n\n```sql\nCREATE OR REPLACE VIEW 视图名 AS SELECT[...] FROM [...]\n```\n\n## 字符集转换\n\n```sql\nLEFT JOIN code_value b ON a.cost_type = CONVERT ( b.`code` USING utf8mb4 ) COLLATE utf8mb4_unicode_ci\n```\n\n## Function\n\n```sql\n-- https://www.w3schools.com/sql/func_mysql_find_in_set.asp\nselect FIND_IN_SET('bb', 'aa,bb,cc');\nselect FIND_IN_SET(null, '0');\n\n-- COALESCE 函数用于返回参数列表中第一个非 NULL 值。如果所有参数都为 NULL,则返回 NULL\nCOALESCE ( `code_value`.`name`, '' ) AS cost_type_name,\n```\n\n## ON vs USING\n\n[MySQL ON vs USING? | stackoverflow](https://stackoverflow.com/questions/11366006/mysql-on-vs-using)\n\n## 删除重复数据\n\n```sql\nDELETE\nFROM\n student\nWHERE\n id NOT IN (\n SELECT\n id\nFROM\n ( SELECT MIN( id ) AS id FROM student GROUP BY `name` ) tmp)\n```\n\n要多加一层 tmp 包装,否则会遇到:`1093 - You can't specify target table 'student' for update in FROM clause`\n\n## 备份表\n\n```sql\n-- 创建同结构备份表\ncreate table zzz_my_table_220727 like my_table;\n-- 将需要数据写入备份表\ninsert into zzz_my_table_220727 select * from my_table ORDER BY id desc LIMIT 1000;\n-- 情况原表\ntruncate table my_table;\n```\n\n## SQL AND OR 执行优先级\n\n```sql\nselect id from table01 where condition1 or condition2 and condition3;\n\n-- 等价于:\nselect id from table01 where condition1 or (condition2 and condition3);\n-- 而非:\nselect id from table01 where (condition1 or condition2) and condition3;\n```\n\n`and` 级别高于 `or`。相当于可以把 `and` 看成 `乘号 *`,把 `or` 看成 `加号 +`。\n\n## 事务\n\n```sql\n//对读取的记录加共享锁\nselect ... lock in share mode;\n\n-- //对读取的记录加独占锁\nselect ... for update;\n```\n\n## References\n\n- [编程小技巧(3):查看优化后的 SQL](https://www.standbyside.com/2019/06/19/tips-of-coding-3/)\n\n-- EOF --\n","tags":["mysql"],"categories":["code-snippet"]},{"title":"PHP 月份加减问题","url":"/2021/01/27/php-strtotime-month/","content":"\n## 看现象\n\n```php\nvar_dump(date(\"Y-m-d\", strtotime(\"+1 month\", strtotime(\"2020-07-31\"))));\n// string(10) \"2020-08-31\" 符合预期\n\nvar_dump(date(\"Y-m-d\", strtotime(\"+1 month\", strtotime(\"2020-05-31\"))));\n// string(10) \"2020-07-01\" 不符合预期,预期 2020-06-30\n\nvar_dump(date(\"Y-m-d\", strtotime(\"-1 month\", strtotime(\"2020-02-29\"))));\n// string(10) \"2020-01-29\" 符合预期\n\nvar_dump(date(\"Y-m-d\", strtotime(\"-1 month\", strtotime(\"2020-03-31\"))));\n// string(10) \"2020-03-02\" 不符合预期,预期 2020-02-29\n\n\n// Carbon\\Carbon\nCarbon::parse(\"2020-07-31\")->addMonth()->toDateString();\n// \"2020-08-31\"\nCarbon::parse(\"2020-05-31\")->addMonth()->toDateString();\n// \"2020-07-01\"\nCarbon::parse(\"2020-02-29\")->subMonth()->toDateString();\n// \"2020-01-29\"\nCarbon::parse(\"2020-03-31\")->subMonth()->toDateString();\n// \"2020-03-02\"\n\n// 结果与 strtotime 一致。\n```\n\n## 原因\n\n```php\nvar_dump(date(\"Y-m-d\", strtotime(\"+1 month\", strtotime(\"2020-05-31\"))));\n// string(10) \"2020-07-01\"\n```\n\ndate 内部的处理逻辑:\n\n1. `2020-05-31` 做 `+1 month` 也就是 `2020-06-31`。\n2. 再做日期规范化,因为没有 `06-31`,所以 `06-31` 就等于了 `07-01`。\n\n```php\nvar_dump(date(\"Y-m-d\", strtotime(\"2020-06-31\")));\n// string(10) \"2017-07-01\"\n\nvar_dump(date(\"Y-m-d\", strtotime(\"next month\", strtotime(\"2017-01-31\"))));\n// string(10) \"2017-03-03\"\n\nvar_dump(date(\"Y-m-d\", strtotime(\"last month\", strtotime(\"2017-03-31\"))));\n// string(10) \"2017-03-03\"\n```\n\n## 解决方案\n\n```php\nvar_dump(date(\"Y-m-d\", strtotime(\"last day of -1 month\", strtotime(\"2017-03-31\"))));\n// string(10) \"2017-02-28\"\n\nvar_dump(date(\"Y-m-d\", strtotime(\"first day of +1 month\", strtotime(\"2017-08-31\"))));\n// string(10) \"2017-09-01\"\n\n// 但要注意短语的含义:\nvar_dump(date(\"Y-m-d\", strtotime(\"last day of -1 month\", strtotime(\"2017-03-01\"))));\n// string(10) \"2017-02-28\"\n```\n\n如果使用 `Carbon\\Carbon` 可以用 `subMonthNoOverflow` 与 `addMonthNoOverflow` 防止进位:\n\n```php\nCarbon::parse('2020-03-31')->subMonthNoOverflow()->toDateString();\n// \"2020-02-29\"\n\nCarbon::parse(\"2020-05-31\")->addMonthNoOverflow()->toDateString();\n// \"2020-06-30\"\n```\n\n## Ym 类似问题\n\n在当日是 31 号场景下:\n\n```php\nCarbon::createFromFormat('Ym', '202206')->format('Y-m');\n// 结果是 2022-07 不符合本意\n```\n\n- [DateTimeImmutable::createFromFormat | php](https://www.php.net/manual/en/datetimeimmutable.createfromformat.php)\n\n`!` Resets all fields (year, month, day, hour, minute, second, fraction and timezone information) to zero-like values ( 0 for hour, minute, second and fraction, 1 for month and day, 1970 for year and UTC for timezone information)\n\nWithout !, all fields will be set to the current date and time.\n\n如果 format 包含字符 !,则未在 format 中提供的生成日期/时间部分以及 ! 左侧的值将设置为 Unix 纪元的相应值。\n\nThe Unix epoch is `1970-01-01 00:00:00` UTC.\n\n```php\nCarbon::createFromFormat('!Ym', '202206')->format('Y-m');\n// 结果是 2022-06\nCarbon::createFromFormat('Ym|', '202206')->format('Y-m');\n// 结果是 2022-06\n\n$format = 'Y-m-!d H:i:s';\n$date = DateTimeImmutable::createFromFormat($format, '2009-02-15 15:16:17');\necho \"Format: $format; \" . $date->format('Y-m-d H:i:s') . \"\\n\";\n// Format: Y-m-!d H:i:s; 1970-01-15 15:16:17\n```\n\n## References\n\n- [Why does subMonth not work correctly? | github](https://github.com/briannesbitt/Carbon/issues/428)\n- [令人困惑的 strtotime | laruence](https://www.laruence.com/2018/07/31/3207.html)\n\n-- EOF --\n","tags":["php"]},{"title":"Yii2 Code Snippet","url":"/2021/01/19/yii2-code-snippet/","content":"\n## gii CLI\n\n```bash\nphp yii help gii/mode\n\nphp yii gii/model --generateLabelsFromComments=1 --overwrite=1 --standardizeCapitals=1 --ns='app\\models\\gii' --tableName=\"*\"\n\n# 多数据库\nphp yii gii/model --generateLabelsFromComments=1 --overwrite=1 --standardizeCapitals=1 --db=\"hub_db\" --ns='app\\models\\hub\\gii' --tableName=\"*\"\n```\n\n## 连接数据库时设置时区\n\n```php\n 'class' => 'yii\\db\\Connection',\n 'dsn' => 'mysql:host=mysql;port=3306;dbname=hub',\n 'username' => 'root',\n 'password' => 'root',\n 'charset' => 'utf8',\n\n // 关闭日志记录,防止被 logs 平台拿走\n 'enableLogging' => YII_DEBUG ? true : false,\n 'enableProfiling' => YII_DEBUG ? true : false,\n\n // 设置时区\n 'on afterOpen' => static function ($event) {\n // set 'Asia/Bangkok' timezone\n $event->sender->createCommand(\"SET time_zone='+08:00';\")->execute();\n },\n```\n\n## ActiveRecord one\n\n> `yii\\db\\ActiveRecord::findOne()` 和 `yii\\db\\ActiveQuery::one()` 都不会添加 LIMIT 1 到 生成的 SQL 语句中。如果你的查询会返回很多行的数据, 你明确的应该加上 `limit(1)` 来提高性能,比如 `Customer::find()->limit(1)->one()`。\n\n## DB where\n\n- 字符串格式,例如:`'status=1'`\n- 哈希格式,例如: `['status' => 1, 'type' => 2]`\n- 操作符格式,例如:`['like', 'name', 'test']`\n- 对象格式,例如:`new LikeCondition('name', 'LIKE', 'test')`\n\n### 简单条件\n\n```php\n// SQL: (type = 1) AND (status = 2).\n$cond = ['type' => 1, 'status' => 2]\n\n// SQL: (id IN (1, 2, 3)) AND (status = 2)\n$cond = ['id' => [1, 2, 3], 'status' => 2]\n\n// SQL: status IS NULL\n$cond = ['status' => null]\n```\n\n### AND OR\n\n```php\n// SQL: `id=1 AND id=2`\n$cond = ['and', 'id=1', 'id=2']\n\n// SQL: `type=1 AND (id=1 OR id=2)`\n$cond = ['and', 'type=1', ['or', 'id=1', 'id=2']]\n\n// SQL: `type=1 AND (id=1 OR id=2)`\n// 此写法 '=' 可以换成其他操作符,例:in like != >= 等\n$cond = [\n 'and',\n ['=', 'type', 1],\n [\n 'or',\n ['=', 'id', '1'],\n ['=', 'id', '2'],\n ]\n]\n```\n\n### NOT\n\n```php\n// SQL: `NOT (attribute IS NULL)`\n$cond = ['not', ['attribute' => null]]\n```\n\n### BETWEEN\n\n```php\n// not between 用法相同\n// SQL: `id BETWEEN 1 AND 10`\n$cond = ['between', 'id', 1, 10]\n```\n\n### IN\n\n```php\n// not in 用法相同\n// SQL: `id IN (1, 2, 3)`\n$cond = ['between', 'id', 1, 10]\n$cond = ['id' => [1, 2, 3]]\n\n// IN 条件也适用于多字段\n// SQL: (`id`, `name`) IN ((1, 'foo'), (2, 'bar'))\n$cond = ['in', ['id', 'name'], [['id' => 1, 'name' => 'foo'], ['id' => 2, 'name' => 'bar']]]\n\n// 也适用于内嵌 SQL 语句\n$cond = ['in', 'user_id', (new Query())->select('id')->from('users')->where(['active' => 1])]\n```\n\n### LIKE\n\n```php\n// SQL: `name LIKE '%tester%'`\n$cond = ['like', 'name', 'tester']\n\n// SQL: `name LIKE '%test%' AND name LIKE '%sample%'`\n$cond = ['like', 'name', ['test', 'sample']]\n\n// SQL: `name LIKE '%tester'`\n$cond = ['like', 'name', '%tester', false]\n```\n\n### EXIST\n\n```php\n// not exists用法类似\n// SQL: EXISTS (SELECT \"id\" FROM \"users\" WHERE \"active\"=1)\n$cond = ['exists', (new Query())->select('id')->from('users')->where(['active' => 1])]\n```\n\n## References\n\n- [查询构建器 | yiiframework](https://www.yiiframework.com/doc/guide/2.0/zh-cn/db-query-builder)\n- [YII where 条件 | csdn](https://blog.csdn.net/u013697959/article/details/79687746)\n\n-- EOF --\n","tags":["php","yii2"],"categories":["code-snippet"]},{"title":"Yii2 Vue 跨域问题","url":"/2021/01/10/yii2-vue-cors/","content":"\n## 旧的解决办法\n\n在控制器的 behaviors 方法中增加下面的代码:\n\n```php\npublic function behaviors()\n{\n $behaviors = parent::behaviors();\n if (YII_ENV_DEV) {\n // add CORS filter\n $behaviors['corsFilter'] = [\n 'class' => Cors::class,\n 'cors' => [\n 'Origin' => ['http://localhost:8080'],\n 'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],\n 'Access-Control-Request-Headers' => ['*'],\n 'Access-Control-Allow-Credentials' => true,\n 'Access-Control-Max-Age' => 86400,\n 'Access-Control-Expose-Headers' => [],\n ],\n ];\n }\n\n return $behaviors;\n}\n```\n\n上边配置的解释:\n\n- `Origin` 即 `Access-Control-Allow-Origin` 表示:响应头指定了该响应的资源是否被允许与给定的 origin 共享。\n- `Access-Control-Request-Method` 用于通知服务器在真正的请求中会采用哪种 HTTP 方法。因为预检请求所使用的方法总是 `OPTIONS` ,与实际请求所使用的方法不一样,所以这个请求头是必要的。\n- `Access-Control-Request-Headers` 用于通知服务器在真正的请求中会采用哪些请求头。\n- `Access-Control-Allow-Credentials` 表示是否可以将对请求的响应暴露给页面。返回 true 则可以,其他值均不可以。\n- `Access-Control-Max-Age` 表示返回结果(即 `Access-Control-Allow-Methods` 和 `Access-Control-Allow-Headers` 提供的信息) 可以被缓存多久。\n- `Access-Control-Expose-Headers` 列出了哪些首部可以作为响应的一部分暴露给外部。\n\n注意:\n\n如果设置 `Origin` 为 `['*']`,即所有的前端跨域请求可以接受,同时把 `Access-Control-Allow-Credentials` 设置为 `true`,Yii 会直接报错:**Allowing credentials for wildcard origins is insecure. Please specify more restrictive origins or set 'credentials' to false in your CORS configuration.**。\n\n告诉你使用通配符的凭证是不安全的,让你设置更严格的 `Origin` 或者把 `Access-Control-Allow-Credentials` 设置为 `false`。\n\n也就是说不能 `Access-Control-Allow-Credentials` 为 `true` 并且 `Access-Control-Allow-Origin` 为 `*`。\n\n因为 `Access-Control-Allow-Credentials` 的意思就是允许跨域请求在请求头中携带凭证,比如 `cookie`,做身份识别,但是你又把 `Access-Control-Allow-Origin` 设置为 `*`,这是说不通的,是相悖的。\n\n具体可参考:[Reason: Credential is not supported if the CORS header 'Access-Control-Allow-Origin' is '\\*' | mozilla](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS/Errors/CORSNotSupportingCredentials)\n\n## 新的问题\n\n上面的理论上解决了跨域问题,但是新版 Chrome 根据 Cookie 的 SameSite 属性,仍然会阻止 Cookie 的发送 `network` `show filtered out request cookies`。\n\n参考:\n\n- [What’s New In DevTools (Chrome 79) | medium](https://medium.com/faun/whats-new-in-devtools-chrome-79-1b2df6cdd759)\n- [Cookie 的 SameSite 属性 | ruanyifeng](https://www.ruanyifeng.com/blog/2019/09/cookie-samesite.html)\n\n在开发环境根治跨域问题,使用 `webpack-dev-server` 代理。\n\n前端:abc.test\n后端:abc-api.test\n代理:`abc.test/web-api` -> `abc-api.test`\n\n```js\n proxy: {\n '/web-api/': {\n target: 'http://abc-api.test/',\n pathRewrite: { '^/web-api': '' },\n changeOrigin: true, // 默认情况下,代理时会保留主机头的来源,您可以将 changeOrigin 设置为true 来覆盖此行为。\n secure: false, // 接受在 HTTPS 上运行带有无效证书的后端服务器。\n },\n }\n```\n\n参考:[dev-server devserverproxy | webpack](https://webpack.js.org/configuration/dev-server/#devserverproxy)\n\n> 是否可以 jwt 来解决待研究。\n\n## References\n\n- [解决 Yii2 + Vue 前后台分离时跨域的问题 | shiqidu](https://www.shiqidu.com/d/846)\n- [跨源资源共享(CORS) | mozilla](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS)\n\n-- EOF --\n","tags":["php","vue","yii2"]},{"title":"回顾 2020","url":"/2020/12/31/review-2020/","content":"\n## 过年就在石家庄\n\n## lwl 走学\n\n## 肺炎在家办公\n\n## PR 剪辑\n\n## 表情包制作\n\n## 自己染发剪发\n\n-- EOF --\n","categories":["review"]},{"title":"MySQL 空格问题","url":"/2020/11/28/mysql-blank-space/","content":"\n## 看现象\n\n创建一个测试数据库表,插入测试数据:\n\n```sql\nCREATE TABLE `blank_space` (\n `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n `uid` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',\n `desc` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',\n PRIMARY KEY (`id`),\n UNIQUE KEY `uniq_key` (`uid`) USING BTREE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n\nINSERT INTO `blank_space`(`id`, `uid`, `desc`) VALUES (1, 'abc ', '末尾1个');\nINSERT INTO `blank_space`(`id`, `uid`, `desc`) VALUES (2, ' abc', '开头1个');\nINSERT INTO `blank_space`(`id`, `uid`, `desc`) VALUES (3, ' abc', '开头2个');\n```\n\n| id | uid | desc |\n| --- | ------- | --------- |\n| 1 | abc\\_ | 末尾 1 个 |\n| 2 | \\_abc | 开头 1 个 |\n| 3 | \\_\\_abc | 开头 2 个 |\n\n> uid 实际上没有 `_`,这样写是为了看到空格。\n\n执行操作:\n\n```sql\nSELECT * FROM blank_space WHERE uid = 'abc';\n\nSELECT * FROM blank_space WHERE uid = 'abc ';\n\nSELECT * FROM blank_space WHERE uid = 'abc ';\n```\n\n都可以查询出:\n\n| id | uid | desc |\n| --- | ----- | --------- |\n| 1 | abc\\_ | 末尾 1 个 |\n\n执行操作:\n\n```sql\nINSERT INTO `blank_space`(`uid`, `desc`) VALUES ('abc', '无空格');\n-- 1062 - Duplicate entry 'abc' for key 'uniq_key', Time: 0.322000s\n\nINSERT INTO `blank_space`(`uid`, `desc`) VALUES ('abc ', '末位两个');\n-- 1062 - Duplicate entry 'abc' for key 'uniq_key', Time: 0.322000s\n```\n\n## 原因\n\nMySQL 校对规则属于 PADSPACE,会忽略尾部空格。针对的是 varchar char text 等文本类的数据类型。此为 SQL 标准化行为。无需要设置也无法改变。\n\n## 解决方案\n\n```sql\nSELECT * FROM blank_space WHERE uid = BINARY 'abc';\n-- 0 records\n\nSELECT * FROM blank_space WHERE uid = BINARY 'abc ';\n-- 1 records\n\nSELECT * FROM blank_space WHERE uid = BINARY 'abc ';\n-- 0 records\n\n\nSELECT * FROM blank_space WHERE uid like 'abc';\n-- 0 records\n\nSELECT * FROM blank_space WHERE uid like 'abc ';\n-- 1 records\n\nSELECT * FROM blank_space WHERE uid like 'abc ';\n-- 0 records\n```\n\n> BINARY 不是函数,是类型转换运算符,它用来强制它后面的字符串为一个二进制字符串,可以理解成精确匹配。\n\n## 约束攻击\n\n**需要在非严格模式下。**\n\n```sql\nSET sql_mode = '';\n\nCREATE TABLE `blank_space_attack` (\n `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n `uid` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',\n `pwd` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',\n PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n\nINSERT INTO `blank_space_attack`(`id`, `uid`, `pwd`) VALUES (1, 'admin', '123');\nINSERT INTO `blank_space_attack`(`id`, `uid`, `pwd`) VALUES (2, 'tim', '234');\n```\n\n攻击:\n\n```sql\nINSERT INTO `blank_space_attack`(`uid`, `pwd`) VALUES ('admin 1', 'easy');\n```\n\n结果:\n\n```sql\n\nselect * from blank_space_attack where uid = \"admin\" and pwd = \"easy\";\n```\n\n| id | uid | desc |\n| --- | --------------- | ---- |\n| 3 | admin\\_\\_\\_\\_\\_ | easy |\n\n### 限制条件\n\n- 服务端没有对用户名长度进行限制。如果服务端限制了用户名长度就不能导致数据库截断,也就没有利用条件。\n- 登陆验证的 SQL 语句必须是用户名和密码一起验证。如果是验证流程是先根据用户名查找出对应的密码,然后再比对密码的话,那么也不能进行利用。因为当使用 admin 为用户名来查询密码的话,数据库此时就会返回两条记录,而一般取第一条则是目标用户的记录,那么你传输的密码肯定是和目标用户密码匹配不上的。\n- 验证成功后返回的必须是用户传递进来的用户名,而不是从数据库取出的用户名。因为当我们以用户 admin 和密码 easy 登陆时,其实数据库返回的是我们自己的用户信息,而我们的用户名其实是 `admin_____`,如果此后的业务逻辑以该用户名为准,那么就不能达到越权的目的了。\n\n## References\n\n- [SQL 约束攻击 | v0n](https://www.v0n.top/2019/08/05/SQL%E7%BA%A6%E6%9D%9F%E6%94%BB%E5%87%BB/)\n- [记一次数据库空格问题 | iluoy](https://iluoy.com/articles/295)\n- [Mysql 查询条件中字符串尾部有空格也能匹配上的问题 | xjnotxj](https://www.cnblogs.com/xjnotxj/p/9019866.html)\n\n-- EOF --\n","tags":["mysql"]},{"title":"Composer 文档笔记","url":"/2020/10/28/composer-document-note/","content":"\n## Book\n\n> <https://getcomposer.org/doc/00-intro.md>\n\nComposer 最新版本需要 PHP 7.2.5 才能运行。长期支持版本 (2.2.x) 仍然提供对 PHP 5.3.2+ 的支持。\n\n> <https://getcomposer.org/doc/01-basic-usage.md>\n\n```bash\n# get a list of your locally available platform packages.\n# php | ext-<name> | lib-<name>\ncomposer show --platform\n```\n\n> <https://getcomposer.org/doc/02-libraries.md>\n\nLight-weight distribution packages 轻量级分发包。使用 `.gitattributes` 来防止不需要的文件使 zip 分发包膨胀。\n\n```txt\n// .gitattributes\n/demo export-ignore\nphpunit.xml.dist export-ignore\n/.github/ export-ignore\n```\n\n通过检查手动生成的压缩文件进行测试:\n\n```bash\ngit archive branchName --format zip -o file.zip\n```\n\n> <https://getcomposer.org/doc/03-cli.md>\n\n由于 Composer 使用 symfony/console,因此如果命令名称不含混,可以用简称来称呼命令。\n\n```bash\ncomposer dump\n# calls\ncomposer dump-autoload\n```\n\nGlobal Options:\n\n- `--profile` 显示计时和内存使用信息\n\ninstall / i:\n\n-\n\n-- EOF --\n","tags":["php","composer"]},{"title":"在 Laravel 之外使用 illuminate 组件","url":"/2020/09/11/use-illuminate-components-without-laravel/","content":"\n当代框架基本都是有组件构成,这使得框架变得更加灵活。[The Laravel Components | github](https://github.com/illuminate) Laravel 中有不少优质组件,那如何在 Laravel 之外使用 illuminate 组件呢?\n\n## illuminate/validation\n\n以 [illuminate/validation](https://github.com/illuminate/validation) 为例,validation 有丰富的数据验证功能。\n\n在项目的 `composer.json` 文件中添加:\n\n```json\n...\n \"require\": {\n ...\n \"illuminate/validation\": \"^5.8\",\n...\n```\n\n从 [Laravel-Lang/lang](https://github.com/Laravel-Lang/lang/tree/master/src/zh_CN) 项目中复制需要的语言文件放到自己的项目中。\n\n例如:在 Yii2 项目中,复制对应语言文件到项目中的 `assets/lang/zh-CN/validation.php`。\n\n创建 `common/Validator.php`:\n\n```php\n<?php\n\nnamespace app\\common;\n\nuse Illuminate\\Filesystem\\Filesystem;\nuse Illuminate\\Translation\\FileLoader;\nuse Illuminate\\Translation\\Translator;\nuse Illuminate\\Validation\\Factory;\n\nclass Validator\n{\n private static $instance = null;\n\n private function __construct()\n {\n }\n\n public static function getInstance(): Factory\n {\n if (null === static::$instance) {\n $translationPath = get_alias('@assets/lang');\n $translationLocale = 'zh-CN';\n $transFileLoader = new FileLoader(new Filesystem(), $translationPath);\n $translator = new Translator($transFileLoader, $translationLocale);\n static::$instance = new Factory($translator);\n }\n\n return static::$instance;\n }\n}\n```\n\n在全局函数文件添加:\n\n```php\n// https://learnku.com/docs/laravel/5.8/validation/3899#manually-creating-validators\n// $rules = [\n// 'name' => 'required|string|min:2|max:5',\n// 'code' => 'required|string|min:2|max:5',\n// ];\nfunction validator(array $data, array $rules, array $messages = [], array $customAttributes = [])\n{\n return \\app\\common\\Validator::getInstance()->make($data, $rules, $messages, $customAttributes);\n}\n```\n\n测试使用:\n\n```php\n$rules = ['name' => 'required|numeric'];\n$customAttributes = ['name' => 'My name'];\n$messages = ['name.required' => 'A name is required',];\n\n$validator = validator($data, $rules, $customAttributes, $messages);\nif ($validator->fails()) {\n $errors = $validator->errors()->all();\n Response::error(Errors::ParamsInvalid, implode(',', $errors), $errors);\n}\n```\n\n-- EOF --\n","tags":["php"]},{"title":"Redis 正则批量删除 key","url":"/2020/08/17/redis-matching-a-pattern-delete-keys/","content":"\n```lua\nEVAL \"return redis.call('del', 'defaultKey', unpack(redis.call('keys', ARGV[1])))\" 0 prefix:*\n```\n\n循环删除:\n\n```lua\nEVAL \"local keys = redis.call('keys', ARGV[1]) \\n for i=1,#keys,5000 do \\n redis.call('del', unpack(keys, i, math.min(i+4999, #keys))) \\n end \\n return keys\" 0 prefix:*\n```\n\n## References\n\n- [How to atomically delete keys matching a pattern using Redis | stackoverflow](https://stackoverflow.com/questions/4006324/how-to-atomically-delete-keys-matching-a-pattern-using-redis)\n\n-- EOF --\n","tags":["redis"]},{"title":"MySQL JSON 数据类型","url":"/2020/08/14/mysql-json-data-type/","content":"\n[The JSON Data Type | mysql](https://dev.mysql.com/doc/refman/5.7/en/json.html)\n\n> As of MySQL 5.7.8, MySQL supports a native JSON data type\n\n[JSON Function Reference | mysql](https://dev.mysql.com/doc/refman/5.7/en/json-function-reference.html)\n\nA JSON column cannot have a non-NULL default value.\n\n## 索引\n\n设置虚拟列 -> 虚拟列建立索引\n\n在 MySQL 5.7 中,支持两种 Generated Column,即 Virtual Generated Column 和 Stored Generated Column,前者只将 Generated Column 保存在数据字典中(表的元数据),并不会将这一列数据持久化到磁盘上;后者会将 Generated Column 持久化到磁盘上,而不是每次读取的时候计算所得。很明显,后者存放了可以通过已有数据计算而得的数据,需要更多的磁盘空间,与 Virtual Column 相比并没有优势,因此,MySQL 5.7 中,不指定 Generated Column 的类型,默认是 Virtual Column。\n\n如果需要 Stored Generated Golumn 的话,可能在 Virtual Generated Column 上建立索引更加合适,一般情况下,都使用 Virtual Generated Column,这也是 MySQL 默认的方式。\n\n```json\n{\n \"id\": 1,\n \"name\": \"Sally\",\n \"games_played\": {\n \"Battlefield\": {\n \"weapon\": \"sniper rifle\",\n \"rank\": \"Sergeant V\",\n \"level\": 20\n }\n }\n}\n```\n\n```sql\nCREATE TABLE `players` (\n `id` INT UNSIGNED NOT NULL,\n `player_and_games` JSON NOT NULL,\n `names_virtual` VARCHAR(20) GENERATED ALWAYS AS (`player_and_games` ->> '$.name') NOT NULL,\n PRIMARY KEY (`id`)\n);\n```\n\n## 在 Yii2 中的使用\n\n```php\n$query = static::find()\n ->andWhere(['=', new Expression(\"`json_value` -> '$.source'\"), new JsonExpression($array_param)]);\n```\n\n## References\n\n- [MySQL 5.7 新特性 JSON 的创建,插入,查询,更新](https://www.lnmp.cn/mysql-57-new-features-json.html)\n- [MySQL · 最佳实践 · 如何索引 JSON 字段](http://mysql.taobao.org/monthly/2017/12/09/)\n- [MySQL 常用 Json 函数 | cnblogs](https://www.cnblogs.com/waterystone/p/5626098.html)\n\n-- EOF --\n","tags":["mysql","yii2"]},{"title":"Composer vendor 提交至 Git","url":"/2020/08/10/commit-composer-vendor-to-git/","content":"\n## 应该将 vendor 提交到 Git 吗\n\n一般建议是 **不**。`vendor` 目录应添加到 `.gitignore`。\n\n最佳实践是让所有开发人员使用 Composer 来安装依赖项。类似地,构建服务器、CI、部署工具等都应该作为项目启动的一部分来运行 Composer。\n\n虽然在某些环境下这样做很诱人,但也会导致一些问题:\n\n- 大型 VCS 存储库的大小和更新代码时的差异。\n- 在你自己的 VCS 复制你所有依赖的历史。\n- 将通过 git 安装的依赖项添加到 git repo 中将显示为 `submodules`。这是有问题的,因为它们不是真正的 `submodules`,您将会遇到问题。\n\n如果你真的觉得你必须这样做,你有几个选择:\n\n- 限制自己安装带标记的版本(没有 dev 版本),这样就只能安装压缩版,并避免与 git `submodules` 有关的问题。\n- Use `--prefer-dist` or set `preferred-install` to `dist` in your config.\n- Remove the `.git` directory of every dependency after the installation, then you can add them to your git repo. You can do that with `rm -rf vendor/\\*\\*/.git` in ZSH or `find vendor/ -type d -name \".git\" -exec rm -rf {} \\;` in Bash. 但这意味着您必须在运行 composer 更新之前从磁盘中删除这些依赖项。\n- Add a `.gitignore` rule `/vendor/**/.git` to ignore all the vendor `.git` folders. 这种方法不需要在运行编写器更新之前从磁盘删除依赖项。\n\n## 我的做法\n\n> 问题解决了,但是不确信做法是否正确。\n\n因为网络环境与部署的原因,在生产环境下是将 `vendor` 目录提交到 `git` 中的。使用过程中确实出现了,部分类库成为了 `submodules`,无法把真实的代码提交进 git。\n\n可尝试执行:\n\n```bash\ngit rm rf --cache vendor\ngit add .\ngit commit -m \"add vendor\"\n```\n\n## References\n\n- [Should I commit the dependencies in my vendor directory? | getcomposer](https://getcomposer.org/doc/faqs/should-i-commit-the-dependencies-in-my-vendor-directory.md)\n\n-- EOF --\n","tags":["php","git","composer"]},{"title":"tcpdump 入门使用","url":"/2020/06/29/tcpdump-getting-started/","content":"\ntcpdump 是 Unix/Linux 下的抓包工具,可以针对指定网卡、端口、协议进行抓包。\n\n<!-- more -->\n\n## 字太多不看\n\n```bash\nsudo tcpdump host api.test and tcp port 80 -A -nn\nsudo tcpdump dst api.test and tcp port 80 -A\n```\n\n## 一举成名天下知\n\n```bash\nman tcpdump\n```\n\n## 获取适配器列表\n\n```bash\ntcpdump -D\ntcpdump --list-interfaces\n\n1.en0 [Up, Running]\n2.p2p0 [Up, Running]\n3.awdl0 [Up, Running]\n4.llw0 [Up, Running]\n5.utun0 [Up, Running]\n6.utun1 [Up, Running]\n7.utun2 [Up, Running]\n8.en5 [Up, Running]\n9.lo0 [Up, Running, Loopback]\n10.bridge0 [Up, Running]\n11.en1 [Up, Running]\n12.en2 [Up, Running]\n13.en3 [Up, Running]\n14.en4 [Up, Running]\n15.gif0 [none]\n16.stf0 [none]\n17.XHC0 [none]\n18.XHC1 [none]\n19.ap1 [none]\n20.XHC20 [none]\n21.VHC128 [none]\n```\n\n## 监听适配器\n\nListen on interface.\n\nmacOS 下监听适配器,必须使用 root 权限。\n\n```bash\nsudo tcpdump -i en0\n\nsudo tcpdump -i 1\n```\n\n## 过滤监听适配器\n\n### 过滤主机\n\n```bash\n# 抓取所有经过 en0,目的或源地址是 192.168.50.1 的网络数据\nsudo tcpdump -i en0 host 192.168.50.1\n\n# 源地址\nsudo tcpdump -i en0 src host 192.168.50.1\n\n# 目的地址\nsudo tcpdump -i en0 dst host 192.168.50.1\n```\n\n### 过滤端口\n\n```bash\nsudo tcpdump -i en0 port 8080\n```\n\n### 过滤网段\n\n```bash\nsudo tcpdump -i en0 net 192.168\n```\n\n### 协议过滤\n\n```bash\nsudo tcpdump -i en0 tcp\nsudo tcpdump -i en0 udp\nsudo tcpdump -i en0 ip\nsudo tcpdump -i en0 arp\nsudo tcpdump -i en0 icmp\n```\n\n### 使用表达式\n\n- 与:&& 或 and\n- 或:|| 或 or\n- 非:! 或 not\n\n## 选项\n\ntcpdump 默认只会截取前 96 字节的内容,要想截取所有的报文内容,可以使用 -s number, number 就是你要截取的报文字节数,如果是 0 的话,表示截取报文全部内容。\n\n- -i any 监听所有的网卡\n- -n 不要解析域名,会优先暂时主机的名字\n- -nn 不展示主机名和端口名(比如 443 端口会被展示成 https)\n- -A 只使用 ascii 打印报文的全部数据,不要和 -X 一起使用。截取 http 请求的时候可以用 sudo tcpdump -nSA port 80\n- -X 同时用 hex 和 ascii 显示报文的内容\n- -XX 同 -X,但同时显示以太网头部\n- -S 显示绝对的序列号(sequence number),而不是相对编号\n- -s 截取的包字节长度,默认情况下 tcpdump 会展示 96 字节的长度,要获取完整的长度可以用 -s0 或者 -s1600。\n- -v, -vv, -vvv:显示更多的详细信息\n- -c number 截取 number 个报文,然后结束\n\n## Flags\n\n> [tcpdump Flags | readthedocs](https://amits-notes.readthedocs.io/en/latest/networking/tcpdump.html#id2)\n\n| TCP Flag | Flag | Meaning |\n| -------- | ------- | -------------------------------------------------------------- |\n| SYN | S | Syn packet, a session establishment request. 一个会话建立请求 |\n| ACK | A | Ack packet, acknowledge sender’s data. 确认发送方的数据 |\n| FIN | F | Finish flag, indication of termination. 终止的的标识 |\n| RESET | R | Reset, indication of immediate abort of conn. 指令立即中止 |\n| PUSH | P | Push, immediate push of data from sender. 从发送方立即推送数据 |\n| URGENT | U | Urgent, takes precedence over other data. 优先于其他数据 |\n| NONE | A dot . | Placeholder, usually used for ACK. 占位符,通常用于 ACK |\n\n## 实例\n\n抓取所有经过 eth1,目的地址是 192.168.1.254 或 192.168.1.200 端口是 80 的 TCP 数据:\n\n```bash\nsudo tcpdump -i eth1 '((tcp) and (port 80) and ((dst host 192.168.1.254) or (dst host\n192.168.1.200)))'\n```\n\n抓取所有经过 eth1,目的网络是 192.168,但目的主机不是 192.168.1.200 的 TCP 数据:\n\n```bash\nsudo tcpdump -i eth1 '((tcp) and ((dst net 192.168) and (not dst host 192.168.1.200)))'\n```\n\n只抓 SYN 包:\n\n```bash\nsudo tcpdump -i eth1 'tcp[tcpflags] = tcp-syn'\n```\n\n抓 SYN, ACK:\n\n```bash\nsudo tcpdump -i eth1 'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack != 0'\n```\n\n抓 DNS 请求数据:\n\n```bash\nsudo tcpdump -i en0 udp dst port 53\n```\n\n-c 参数对于运维人员来说也比较常用,因为流量比较大的服务器,靠人工 CTRL+C 还是抓的太多,于是可以用 -c 参数指定抓多少个包:\n\n```bash\nsudo time tcpdump -nn -i en0 'tcp[tcpflags] = tcp-syn' -c 10000 > /dev/null\n```\n\n实时抓取端口号 8000 的 GET 包,然后写入 GET.log:\n\n```bash\nsudo tcpdump -i eth0 '((port 8000) and (tcp[(tcp[12]>>2):4]=0x47455420))' -nnAl -w /tmp/GET.log\n```\n\n## 三次握手 四次挥手\n\n### TCP 连接建立(三次握手)\n\n客户端 A,服务器 B,初始序号 seq,确认号 ack。\n\n初始状态:B 处于监听状态,A 处于打开状态。\n\n- A -> B : seq = x (A 向 B 发送连接请求报文段,A 进入同步发送状态 SYN-SENT)\n- B -> A : ack = x + 1,seq = y (B 收到报文段,向 A 发送确认,B 进入同步收到状态 SYN-RCVD)\n- A -> B : ack = y + 1 (A 收到 B 的确认后,再次确认,A 进入连接状态 ESTABLISHED)\n\n连接后的状态:B 收到 A 的确认后,进入连接状态 ESTABLISHED。\n\n为什么要握手要三次?防止失效的连接请求突然传到服务器端,让服务器端误认为要建立连接。\n\n### TCP 连接释放(四次挥手)\n\n- A -> B : seq = u (A 发出连接释放报文段,进入终止等待 1 状态 FIN-WAIT-1)\n- B -> A : ack = u + 1,seq = v (B 收到报文段,发出确认,TCP 处于半关闭,B 还可向 A 发数据,B 进入关闭等待状态 WAIT)\n- B -> A : ack = u + 1,seq = w (B 重复发送确认号,进入最后确认状态 LAST-ACK)\n- A -> B : ack = w + 1,seq = u + 1 (A 发出确认,进入时间等待状态 TIME-WAIT)\n\n经过时间等待计时器设置的时间 2MSL 后,A 才进入 CLOSED 状态。\n\n为什么 A 进入 TIME-WAIT 后必须等待 2MSL:\n\n- 保证 A 发送的最后一个 ACK 报文段能达到 B\n- 防止失效的报文段出现在连接中\n\n### 需要思考的问题\n\n问题 1: 请详细描述三次握手和四次挥手的过程\n要求熟悉三次握手和四次挥手的机制,要求画出状态图。\n\n问题 2: 四次挥手中 TIME_WAIT 状态存在的目的是什么?\n这个问题是画出四次挥手状态图,会引申问你。不排除还会问为什么四次挥手是四次不是二次等问题。最好是把相关问题均掌握。\n\n问题 3: TCP 是通过什么机制保障可靠性的?\n从四个方面进行回答,ACK 确认机制、超时重传、滑动窗口以及流量控制,深入的话要求详细讲出流量控制的机制。\n\n### 抓包分析握手过程\n\n```bash\nsudo tcpdump -i en0 host www.qq.com and tcp -S -c 50\n```\n\n![tcpdump 抓包分析握手过程](https://user-images.githubusercontent.com/9289792/87918568-3377e280-caa9-11ea-831a-95000e308ad8.png)\n\n## References\n\n- [macOS 下使用 tcpdump 抓包 | jianshu](https://www.jianshu.com/p/a57a5b0e58f0)\n- [tcpdump | readthedocs](https://amits-notes.readthedocs.io/en/latest/networking/tcpdump.html)\n- [TCP 三次握手、四次挥手与 TcpDump 抓包分析 | 清泉白石](https://www.cnblogs.com/fonxian/p/6565209.html)\n\n-- EOF --\n","tags":["linux"]},{"title":"配置 Laradock PhpStorm Xdubug","url":"/2020/05/26/config-laradock-phpstorm-xdubug/","content":"\n最近在学习 Yii2 的源码,为了方便调试所以研究下 Laradock + PhpStorm + Xdubug 的配置。\n\n## 环境\n\n- macOS\n- Laradock v10.0\n\n请保证 Laradock 是最新的版本,可以减少不必要的麻烦。也推荐使用我精简过的项目 [imzyf/my-dock | github](https://github.com/imzyf/my-dock)。\n\n<!-- more -->\n\n## 配置 Laradock\n\n```bash\nvim .env\n\nWORKSPACE_INSTALL_XDEBUG=true\nPHP_FPM_INSTALL_XDEBUG=true\n```\n\n重新编译 php-fpm 和 workspace 容器:\n\n```bash\ndocker-compose build php-fpm workspace\n```\n\n## 配置 PhpStorm\n\n### 配置 Docker\n\nPreferences > Build, Execution, Deploymnent > Docker\n\n![docker](https://user-images.githubusercontent.com/9289792/82999144-302d2100-a03b-11ea-8a21-08bc67838fc2.png)\n\n### 配置 PHP\n\nPreferences > Languages & Frameworks > PHP,PHP CLI Interpreter 点 `...`\n\n![php 1](https://user-images.githubusercontent.com/9289792/82997395-e7746880-a038-11ea-98ca-d68052d5bd22.png)\n\n点击 +,选择 From Docker, Vagrant...\n\n![php 2](https://user-images.githubusercontent.com/9289792/82997724-55209480-a039-11ea-8235-4a0479aeb832.png)\n\nDebugger 可以显示出 Xdebug。\n\n### 配置 Servers\n\nPreferences > Languages & Frameworks > PHP > Servers\n\n![server](https://user-images.githubusercontent.com/9289792/82998171-f7d91300-a039-11ea-89e7-50b79664b0f6.png)\n\n注意:Name 必须填写 Laradock 中的 PHP_IDE_CONFIG 也就就是 `laradock`。\n\n### 配置 Xdebug\n\nPreferences > Languages & Frameworks > PHP > Debug。点击 `Validate`,填写。\n\n![Xdebug](https://user-images.githubusercontent.com/9289792/82998451-530b0580-a03a-11ea-925a-1770df95eb66.png)\n\nrun > Edit Configurations,添加 PHP Remote Debug。IDE key 为 `PHPSTORM`。\n\n![Xdebug2](https://user-images.githubusercontent.com/9289792/82998663-95ccdd80-a03a-11ea-9dc4-5d00d012e7df.png)\n\n## 配置 Chrome\n\n下载插件 [Xdebug helper](https://chrome.google.com/webstore/detail/eadndfjplgieldjbigjakmdgkmoaaaoc),右键图标 配置。\n\n![chrome](https://user-images.githubusercontent.com/9289792/82998865-d3316b00-a03a-11ea-94cc-a6642fa0cdbf.png)\n\n## enjoy\n\n![start](https://user-images.githubusercontent.com/9289792/82999306-636fb000-a03b-11ea-9c0f-a059fbc47fd3.png)\n\n开启 debug,然后访问页面。\n\n![fly](https://user-images.githubusercontent.com/9289792/82999463-96b23f00-a03b-11ea-922a-4c91169628a9.png)\n\n芜湖起飞。\n\n## References\n\n- [Laradock 使用 PhpStorm Debug 代码 | learnku](https://learnku.com/articles/24389)\n- [Set Debugger Using Xdebug With PHPStorm & Laradock | medium](https://medium.com/@chenpohsun_12588/set-debugger-using-xdebug-with-phpstorm-laradock-454e8c2ad0d9)\n\n-- EOF --\n","tags":["php","phpstorm"]},{"title":"PHP float 精度","url":"/2020/05/09/php-float-precision/","content":"\n## 实例 1\n\n```php\n$a = 1.1;\nvar_dump(gettype($a)); // string(6) \"double\"\nvar_dump($a); // float(1.1)\n```\n\n## 实例 2\n\n```php\n$a = \"123456789.1100110011\";\n$a = (float) $a;\nvar_dump($a); // float(123456789.11001)\nvar_dump(sprintf('%.11f', $a)); // string(21) \"123456789.11001099646\"\n\n$b = 123456789.11001;\nvar_dump($b); // float(123456789.11001)\nvar_dump(sprintf('%.11f', $b)); // string(21) \"123456789.11000999808\"\n\n$c = '123456789.1100110011';\n$c = (float) $c;\nvar_dump($c); // float(123456789.11001)\n$c = (string) $c;\nvar_dump($c); // string(15) \"123456789.11001\"\n$c = (float) $c;\nvar_dump($c); // float(123456789.11001)\nvar_dump(sprintf('%.11f', $c)); // string(21) \"123456789.11000999808\"\n\n\nvar_dump($a === $b); // bool(false) - 说明 $a 还是携带着 float 的精度\nvar_dump($b === $c); // bool(true)\n```\n\n## 实例 3\n\n```php\n// # 1\nvar_dump(120085 === 1200.85 * 100); // bool(false)\n\n// # 2\nvar_dump(120085 == 1200.85 * 100); // bool(false)\n\n// # 3\nvar_dump(120081 == 1200.81 * 100); // bool(true)\n\n// # 4\nvar_dump(120085 - 1200.85 * 100); // float(1.4551915228367E-11)\n```\n\n## 实例 4\n\n```php\n$a = 0.1;\n$b = 0.9;\n$c = 1;\n\nvar_dump(($a + $b) == $c); // bool(true)\nvar_dump(($c - $b) == $a); // bool(false)\n\nvar_dump(sprintf('%.20f', $a + $b)); // string(22) \"1.00000000000000000000\"\nvar_dump(sprintf('%.20f', $c - $b)); // string(22) \"0.09999999999999997780\"\n\nvar_dump((0.5 - 0.25) === 0.25); // bool(true) 0.5 二进制 0.1,0.25 二进制 0.01\nvar_dump((0.25 + 0.25) === 0.5); // bool(true)\n```\n\n## 实例 5\n\n```php\n$n = 19.99;\ndump($n * 100); // 1999.0\ndump((int) ($n * 100)); // 1998 !!!\ndump((string) ($n * 100)); // \"1999\"\ndump((int) (string) ($n * 100)); // 1999\ndump(round($n * 100)); // 1999.0 !!!\ndump((int) round($n * 100)); // 1999\n```\n\n## 分析\n\n看文档:\n\n- [gettype | php.net](https://www.php.net/manual/zh/function.gettype.php)\n- [Float 浮点型 | php.net](https://www.php.net/manual/zh/language.types.float.php)\n\n> 浮点型(也叫浮点数 float,双精度数 double 或实数 real)\n> 浮点数的字长和平台相关,尽管通常最大值是 1.8e308 并具有 14 位十进制数字的精度(64 位 IEEE 格式)。\n> 所以永远不要相信浮点数结果精确到了最后一位,也永远不要比较两个浮点数是否相等。如果确实需要更高的精度,应该使用任意精度数学函数或者 gmp 函数。\n\n实例 1:说明在 PHP 中 `float` 与 `dobule` 是一回事。在 C 级别,所有内容都存储为 double。\n\n实例 2、3:float 的比较结果是 _视情况而定_,**永远不要相信浮点数结果精确到了最后一位**。\n\n实例 4:出现这个问题是因为浮点数计算涉及精度,当浮点数转为二进制时有可能会造成精度丢失。\n\n- Arbitrary-precision arithmetic library for PHP <https://github.com/brick/math>\n\n## 浮点数转二进制方法\n\n整数部分采用除以 2 取余方法,小数部分采用乘以 2 取整方法。\n\n例如:把数字 8.5 转为二进制:\n\n整数部分是 8:\n\n- 8/2=4 8%2=0\n- 4/2=2 4%2=0\n- 2/2=1 2%2=0\n- 1 比 2 小,因此不需要计算下去,整数 8 的二进制为 1000\n\n小数部分是 0.5:\n\n- 0.5x2 = 1.0\n- 因取整后小数部分为 0,因此不需要再计算下去,小数 0.5 的二进制为 0.1\n\n`8.5` 的二进制为 `1000.1`。\n\n计算数字 0.9 的二进制:\n\n- 0.9x2 = 1.8\n- 0.8x2 = 1.6\n- 0.6x2 = 1.2\n- 0.2x2 = 0.4\n- 0.4x2 = 0.8\n- 0.8x2 = 1.6\n- ... 之后不断循环下去,当截取精度为 N 时,N 后的数会被舍去,导致精度丢失。\n\n实例 4 中 `0.9` 在转为二进制时精度丢失,导致比较时出现错误。\n\n> 你看似有穷的小数,在计算机的二进制表示里却是无穷的。\n\n计算数字 0.25 的二进制:\n\n- 0.25x2 = 0.5\n- 0.5x2 = 1.0\n\n`0.25` 的二进制为 `0.01`。\n\n## float 比较方法\n\n### 使用 round 方法处理后再比较\n\n```php\nvar_dump(120085 == round(1200.85 * 100));\n// bool(true)\n\nvar_dump(12008.5 === round(1200.85 * 10, 1));\n// bool(true)\n\nvar_dump(1200.85 === round(1200.8499999, 2));\n// bool(true)\n```\n\n### 使用高精度运算方法\n\n见文档 [BC 数学 函数 | php.net](https://www.php.net/manual/zh/ref.bc.php)。\n\n```php\nvar_dump((1 - 0.9) == 0.1); // bool(false)\nvar_dump(bcsub(1, 0.9, 40) == 0.1); // bool(true)\nvar_dump((float) bcsub(1, 0.9, 40) === 0.1); // bool(true)\n```\n\n## References\n\n- [PHP 浮点型与整型比较的小坑 | codecasts](https://www.codecasts.com/blog/post/php-tricky-floats-comparison-with-int)\n- [php 浮点数比较方法 | csdn](https://blog.csdn.net/fdipzone/article/details/48106065)\n- [PHP 浮点数的一个常见问题的解答 | laruence](https://www.laruence.com/2013/03/26/2884.html)\n\n-- EOF --\n","tags":["php"]},{"title":"PHP Call to undefined function ftp_ssl_connect","url":"/2020/05/08/php-call-to-undefined-function-ftp-ssl-connect/","content":"\n## 环境\n\n- CentOS 7.4\n- PHP 7.1.12 编译安装\n\n<!-- more -->\n\n## 复现\n\n```bash\n/usr/local/php71/bin/php -r \"ftp_ssl_connect('server1.example.com');\"\n\nPHP Fatal error: Uncaught Error: Call to undefined function ftp_ssl_connect() in Command line code:1\n```\n\n## 原因\n\n看文档:[ftp_ssl_connect | php.net](https://www.php.net/ftp_ssl_connect)\n\n1. ftp 扩展没配置\n2. opensll 没有启用\n\n## 解决方案\n\n```bash\n# /root/php-7.1.12/ is php source dir\ncd /root/php-7.1.12/ext/ftp/\n\n# /usr/local/php71/ is php dir\n/usr/local/php71/bin/phpize\n\n# the param --with-openssl-dir is very important\n./configure --with-php-config=/usr/local/php71/bin/php-config --with-openssl-dir\n\nmake\nmake install\n\nvim /usr/local/php71/lib/php.ini\n# add last line\nextension=ftp.so\n\nservice php-fpm reload\n```\n\n这要注意一定要加上 `--with-openssl-dir`,不然会 `FTPS support => disabled`。\n\n当不清楚 `./configure` 有什么参数时,可以执行 `./configure --help`。\n\n## 检查\n\n```bash\n# method 1\n/usr/local/php71/bin/php -r \"phpinfo();\" | grep FTP\n\nFTP support => enabled\nFTPS support => enabled\n\n# method 2\n/usr/local/php71/bin/php -r \"ftp_ssl_connect('server1.example.com');\"\n\nPHP Warning: ftp_ssl_connect(): php_network_getaddresses: getaddrinfo failed: Name or service not known in Command line code on line 1\n```\n\n## References\n\n- [Fatal error: Call to undefined function ftp_ssl_connect() | stackoverflow](https://stackoverflow.com/questions/35085677/fatal-error-call-to-undefined-function-ftp-ssl-connect/61683198#61683198)\n\n-- EOF --\n","tags":["php"]},{"title":"vim Code Snippet","url":"/2020/04/28/vim-code-snippet/","content":"\n## 常用\n\n- 全选(高亮显示):按 esc 后,然后 `ggvG` 或者 `ggVG`\n- 全部复制:按 esc 后,然后 `ggyG`\n- 全部删除:按 esc 后,然后 `dG`\n\n## 粘贴到终端 vim 缩进错乱\n\n<https://stackoverflow.com/questions/2514445/turning-off-auto-indent-when-pasting-text-into-vim/38258720#38258720>\n\n在 vim 中粘贴前先输入:\n\n```bash\n:set paste\n```\n\n原因:在终端的 vim 中没有相应的程序来处理这个从其他应用复制粘贴的过程,所以 vim 通过插入键盘输入的 buffer 来模拟这个粘贴的过程,这个时候 vim 会以为这是用户输入的。\n\n问题就是出在这:当上一行结束,光标进入下一行时 vim 会自动以上一行的的缩进为初始位置。这样就会破坏原始文件的缩进。\n\n## 基础命令\n\n- `gg` 是让光标移到首行,在 vim 才有效,vi 中无效\n- `G` 光标移到最后一行\n- `d` 删除选中内容\n- `y` 复制选中内容到 0 号寄存器\n\n## References\n\n- [vim 全选,全部复制,全部删除 | cnblogs](https://www.cnblogs.com/yangzailu/p/11531972.html)\n- [用 vim 打开后中文乱码怎么办? | zhihu](https://www.zhihu.com/question/22363620)\n\n-- EOF --\n","tags":["vim"],"categories":["code-snippet"]},{"title":"PHP GD 入门使用","url":"/2020/04/16/php-gd-getting-started/","content":"\n## GD 安装、配置\n\n考虑到功能需要使用字体库、图像格式 jpeg\\png 所以先安装相关库。\n\n### 字体库 FreeType 2\n\n<https://www.freetype.org/>\n\n```bash\n# 在临时目录进行操作\ncd /tmp\n\n# https://download.savannah.gnu.org/releases/freetype/\nwget http://download.savannah.gnu.org/releases/freetype/freetype-2.10.1.tar.gz\n\ntar zxvf freetype-2.10.1.tar.gz\n\ncd freetype-2.10.1\n\n./configure --prefix=/usr/local/freetype && make && make install\n```\n\n### 图像格式 jpeg\n\n```bash\ncd /tmp\n\n# http://www.ijg.org/\nwget http://www.ijg.org/files/jpegsrc.v9d.tar.gz\n\ntar zxvf jpegsrc.v9.tar.gz\n\ncd jpeg-9/\n\n./configure --prefix=/usr/local/jpeg && make && make install\n```\n\n### 图像格式 png\n\n```bash\ncd /tmp\n\n# http://www.libpng.org/pub/png/libpng.html\nwget https://download.sourceforge.net/libpng/libpng-1.6.37.tar.gz --no-check-certificate\n\ntar zxvf libpng-1.6.37.tar.gz\n\ncd libpng-1.6.37\n\n./configure --prefix=/usr/local/libpng && make && make install\n```\n\n### 安装 GD\n\n![GD 安装、配置](https://user-images.githubusercontent.com/9289792/80169757-e35bc200-8618-11ea-8851-a4bc7411f6f9.png)\n\n背景:服务器 php 7.1 通过编译自行安装的。\n\n```bash\n# 到 php 源码 dir\ncd {php-source-dir}/ext/gd/\n\n# 生成 configure 文件\n{php-dir}/bin/phpize\n\n# 查看可用参数\n./configure --help\n\n# 设置参数\n./configure --with-php-config={php-dir}/bin/php-config --with-jpeg-dir=/usr/local/jpeg --with-png-dir=/usr/local/libpng --with-freetype-dir=/usr/local/freetype --enable-gd-native-ttf\n\nmake && make install\n\nvim {php-dir}/lib/php.ini\n\n# 最底下增加一行\nextension=gd.so\n\nservice php-fpm reload\n\n# 检查\nphp -r \"var_dump(gd_info());\"\n```\n\n编译前一定要记得 `make clean` 清除上次的编译内容,尤其是已经编译安装过的。\n\n## 实例\n\n```php\n$pic = imagecreate($maxWidth, $maxHeight);\n//定义颜色\n$black = imagecolorallocate($pic, 0, 0, 0);\n$white = imagecolorallocate($pic, 255, 255, 255);\n\n// 白底\nimagefill($pic, 0, 0, $white);\n\n// 打水印\nimagettftext($pic, $fontSize, 30, $x, $y, $lightGrey, $fontFile, $mark);\n\nimagejpeg($pic, $resultPath, 100);\nimagedestroy($pic);\n```\n\n文字右对齐:\n\n```php\n// https://php.golaravel.com/function.imagettfbbox.html\n$bbox = imagettfbbox($fontSize, 0, $fontFile, $text);\n\n$offset = $colWidth - $bbox[2] - 50;\n\nimagettftext($pic, $fontSize, 0, $x + $offset, $y, $_color, $fontFile, $text);\n```\n\n## 遇到的报错\n\n### Call to undefined function imagettftext()\n\n出现此问题应该就是 `FreeType` 没有装好,可参考上面的步骤。\n\n## References\n\n- [php 中文手册 | golaravel](https://php.golaravel.com/intro.image.html)\n- [PHP Fatal error: Call to undefined function imagettftext() | stackoverflow](https://stackoverflow.com/questions/7290958/php-fatal-error-call-to-undefined-function-imagettftext)\n\n-- EOF --\n","tags":["php"]},{"title":"PHP Code Snippet","url":"/2020/04/15/php-code-snippet/","content":"\n## PHP Sandbox\n\n- [PHP Sandbox](https://onlinephp.io/)\n- <https://legacy-sandbox.onlinephpfunctions.com/>\n\n## PHP.net\n\n- [Supported Versions | PHP.net](https://www.php.net/supported-versions.php)\n- [Unsupported Branches | PHP.net](https://www.php.net/eol.php)\n\n## References\n\n- [PHP News, Articles, Upcoming Changes, and more | PHP.Watch](https://php.watch/)\n\n## Laravel illuminate\n\n```php\n# https://github.com/illuminate/database/blob/master/README.md\n$capsule = new Capsule();\n$capsule->addConnection($this->dbConfig);\n$capsule->setAsGlobal();\n// Setup the Eloquent ORM... (optional; unless you've used setEventDispatcher())\n$capsule->bootEloquent();\n$capsule->getDatabaseManager()->extend('mongodb', function ($config, $name) {\n $config['name'] = $name;\n\n return new \\Jenssegers\\Mongodb\\Connection($config);\n});\n```\n\n## 参数查看\n\n```bash\n# 查看 PHP 编译时的参数\nphp -r \"phpinfo();\" | grep configure\n\n# 查看 .ini 配置文件路径\nphp --ini\nphp -r \"phpinfo();\" | grep \"Configuration File\"\n\n# 查看 Modules\nphp -m\n\n# 显示扩展配置\nphp --ri gd\n\n# 检查扩展是否存在\nphp --re decimal\n\n# 交互式运行模式。具有函数、常量、类名、变量、静态方法调用和类常量的 `tab` 补全功能\n# http://php.net/manual/en/features.commandline.interactive.php\nphp -a\n```\n\n### 查看、修改内存限制\n\n```bash\nphp -r \"echo ini_get('memory_limit');\"\n\nphp -r \"phpinfo();\" | grep memory\n```\n\n```ini\nmemory_limit = 1024M;\n```\n\n```php\n// 临时设置最大内存占用\nini_set('memory_limit', '1024M');\n// 设置脚本最大执行时间为 0 永不过期\nset_time_limit(0);\n```\n\n### Too Many Open Files\n\n> PHP-FPM Too Many Open Files 24 Error (set open file descriptor limit)\n\n```bash\nvim php-fpm.conf\n\n;rlimit_files = 1024\nrlimit_files = 4096\n```\n\n重启 PHP-FPM。\n\n## Composer\n\n### aliyun repo\n\n[阿里云 Composer 全量镜像](https://developer.aliyun.com/composer)\n\n```bash\ncomposer config -g repo.packagist composer https://mirrors.aliyun.com/composer/\n```\n\n```bash\n \"config\": {\n \"disable-tls\": true,\n \"gitlab-domains\": [],\n \"optimize-autoloader\": true,\n \"preferred-install\": {\n \"*\": \"dist\"\n }\n \"secure-http\": false,\n \"sort-packages\": true,\n },\n \"repositories\": [\n {\n \"type\": \"cvs\",\n \"url\": \"...\"\n },\n {\n \"type\": \"composer\",\n \"url\": \"https://mirrors.tencent.com/composer/\"\n },\n {\n \"type\": \"composer\",\n \"url\": \"https://mirrors.aliyun.com/composer/\"\n },\n {\n \"type\": \"composer\",\n \"url\": \"https://asset-packagist.org\"\n }\n ]\n```\n\n### 忽略 php 版本限制\n\n**这个是极不推荐的,这样会造成库安装的版本错误。不应该使用。**\n\n```bash\ncomposer require hellogerard/jobby --ignore-platform-reqs\n```\n\n推荐做法:\n\n```bash\nwhich composer\n# /usr/local/bin/composer\n\n{正确的 PHP 版本}/bin/php /usr/local/bin/composer require hellogerard/jobby\n\n/usr/local/opt/[email protected]/bin/php -d memory_limit=-1 /usr/local/bin/composer update -vvv\n```\n\n### emory-limit-errors for more info on how to handle out of memory errors\n\n```bash\nphp -d memory_limit=-1 /usr/local/bin/composer update\n```\n\n### 更新 composer.lock\n\n若项目之前已通过其他源安装,则需要更新 composer.lock 文件:\n\n```bash\ncomposer update --lock\n```\n\n## homebrew PHP\n\n> 🍺 Homebrew tap for PHP 5.6 to 8.2. PHP 8.2 is a nightly build.\n\n[shivammathur/homebrew-php](https://github.com/shivammathur/homebrew-php)\n\n```bash\nbrew tap shivammathur/php\n\nbrew install shivammathur/php/[email protected] -vvv\n\nbrew link --overwrite --force shivammathur/php/[email protected]\nphp -v\n```\n\n> 旧版本 MacOS 可以安装报错时尝试删除 XCode。\n\n```bash\nbrew untap shivammathur/php\n```\n\n- <https://github.com/shivammathur/homebrew-extensions>\n- <https://github.com/nicoverbruggen/phpmon>\n- <https://github.com/phpbrew/phpbrew>\n\n## Real Client IP\n\n```php\n# PHP7+\n$clientIP = $_SERVER['HTTP_CLIENT_IP']\n ?? $_SERVER[\"HTTP_CF_CONNECTING_IP\"] # when behind cloudflare\n ?? $_SERVER['HTTP_X_FORWARDED']\n ?? $_SERVER['HTTP_X_FORWARDED_FOR']\n ?? $_SERVER['HTTP_FORWARDED']\n ?? $_SERVER['HTTP_FORWARDED_FOR']\n ?? $_SERVER['REMOTE_ADDR']\n ?? '0.0.0.0';\n```\n\n## 动态实例化类\n\n```php\nclass Test1{\n public function __construct(){\n echo \"Test1<br>\";\n }\n}\n\n// 方法一\n$class1 = \"Test1\";\nnew $class1();\n\n// 方法二\n$class2 = \"Test2\";\n// 建立类的反射\n$class2 = new ReflectionClass($class2);\n// 相当于实例化类\n$instance = $class2->newInstance();\n```\n\n-- EOF --\n","tags":["php"],"categories":["code-snippet"]},{"title":"MySQL Illegal mix of collations","url":"/2020/04/14/mysql-illegal-mix-of-collations/","content":"\n```sql\nIllegal mix of collations (utf8mb4_general_ci,IMPLICIT) and (utf8mb4_unicode_ci,IMPLICIT) for operation\n```\n\n<!-- more -->\n\n`utf8mb4_unicode_ci` 和 `utf8_general_ci` 列不能混合查询\n\n## 解决方法 1\n\n统一字段 varchar 的编码集,我推荐使用 `utf8mb4_unicode_ci`。\n\n## 解决方法 2\n\n在查询 SQL 中需要转化的字段后面加 `COLLATE utf8mb4_unicode_ci`.\n\n## 对比\n\n准确性:\n\n- utf8mb4_unicode_ci 是基于标准的 Unicode 来排序和比较,能够在各种语言之间精确排序。\n- utf8mb4_general_ci 没有实现 Unicode 排序规则,在遇到某些特殊语言或者字符集,排序结果可能不一致。\n\n性能:\n\n- utf8mb4_unicode_ci 在特殊情况下,Unicode 排序规则为了能够处理特殊字符的情况,实现了略微复杂的排序算法。但是在绝大多数情况下发,不会发生此类复杂比较。相比选择哪一 collation,使用者更应该关心字符集与排序规则在 db 里需要统一。\n- utf8mb4_general_ci 在比较和排序的时候更快。\n\n## utf8mb4\n\nmb4 是 most bytes 4 的意思,专门用来兼容四字节的 unicode。\n\n## References\n\n- [Illegal mix of collations | 少年阿斌](https://www.cnblogs.com/wqbin/p/11852376.html)\n\n-- EOF --\n","tags":["mysql"]},{"title":"NGINX 前后端分离配置","url":"/2020/03/17/nginx-front-back-end-config/","content":"\n前后端分离的开发已是主流,本文主要是记录前后端分离项目的 NGINX 配置。\n\n<!-- more -->\n\n- 前端 Vue web 入口 `host.test`\n- 后端 PHP Yii service 入口 `host.test/api`\n\n```conf\nserver {\n listen 80;\n server_name host.test;\n root /data/project/web/dist/;\n index index.html index.php;\n\n location ^~/api {\n root /data/project/service/web/;\n\n access_log logs/service-access.log;\n error_log logs/service-error.log;\n\n fastcgi_pass 127.0.0.1:9000;\n include fastcgi.conf;\n fastcgi_param SCRIPT_FILENAME /data/project/service/web/index.php;\n fastcgi_param SCRIPT_NAME /api/index.php;\n }\n\n location /index.html {\n add_header Cache-Control \"no-cache, no-store\";\n }\n\n location ^~ / {\n alias /data/project/web/dist/;\n access_log logs/web-access.log;\n error_log logs/web-error.log;\n }\n}\n```\n\n## 负载均衡场景\n\n- 前端 Vue web 入口 `host.test`\n- 后端 PHP Yii service 入口 `host.test/api` 代理到 `api.host.test`\n\n### WEB\n\n```conf\nserver {\n listen 80;\n server_name host.test;\n root /data/project/web/dist/;\n\n access_log logs/web-access.log;\n error_log logs/web-error.log;\n\n location /index.html {\n add_header Cache-Control \"no-cache, no-store\";\n }\n\n location / {\n index index.php index.html index.htm;\n try_files $uri $uri/ /index.html;\n }\n\n location ^~/api/ {\n proxy_set_header Host api.host.test;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-NginX-Proxy true;\n proxy_pass http://api.host.test/;\n }\n}\n```\n\n### SERVICE\n\n```conf\nserver {\n listen 80;\n server_name api.host.test;\n root /data/project/service/web/;\n\n location / {\n access_log logs/service-access.log;\n error_log logs/service-error.log;\n\n index index.php index.html;\n try_files $uri $uri/ /index.php$is_args$args;\n }\n\n location ~ \\.php$ {\n fastcgi_pass 127.0.0.1:10071;\n\n fastcgi_index index.php;\n include fastcgi_params;\n fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n fastcgi_param SERVER_NAME $http_host;\n fastcgi_ignore_client_abort on;\n }\n}\n```\n\n## NGINX 配置转发\n\nlocation 进行的是模糊匹配。\n\n- 当结尾没有 `/` 时,`location /abc/def` 可以匹配 `/abc/defghi` 请求,也可以匹配 `/abc/def/ghi` 等。\n- 当结尾有 `/` 时,`location /abc/def/` 不能匹配 `/abc/defghi` 请求,只能匹配 `/abc/def/anything` 这样的请求。\n\n下面四种情况分别用 `http://192.168.1.4/proxy/test.html` 进行访问:\n\n第一种:\n\n```conf\nlocation /proxy/ {\n proxy_pass http://127.0.0.1:81/;\n}\n```\n\n会被代理到 `http://127.0.0.1:81/test.html`。\n\n第二种(相对于第一种,最后少一个 `/`):\n\n```conf\nlocation /proxy/ {\n proxy_pass http://127.0.0.1:81;\n}\n```\n\n会被代理到 `http://127.0.0.1:81/proxy/test.html`。\n\n第三种:\n\n```conf\nlocation /proxy/ {\n proxy_pass http://127.0.0.1:81/ftlynx/;\n}\n```\n\n会被代理到 `http://127.0.0.1:81/ftlynx/test.html`。\n\n第四种(相对于第三种,最后少一个 `/`):\n\n```conf\nlocation /proxy/ {\n proxy_pass http://127.0.0.1:81/ftlynx;\n}\n```\n\n会被代理到 `http://127.0.0.1:81/ftlynxtest.html`。\n\n-- EOF --\n","tags":["nginx"]},{"title":"【PRCC2019 全套入门教程】笔记","url":"/2020/03/13/prcc2019-getting-started-notes/","content":"\n> u1s1 本教程质量一般,推荐看看 李兴兴 老师的教程。\n\n用视频记录生活越来越普及,年初旅行也拍了些素材,想着自己也当个 up 主。先来学学 PR。[【PR】Premiere Pro CC 2019 全套入门教程 | bilibili](https://www.bilibili.com/video/av37550078) 是我在 B 站看的第一套教学视频,坦白说内容不多,有不少重复的东西,但是这样适合完全零基础的同学。\n\n<!-- more -->\n\n课代表来收我的课后作业:[【交作业】PR CC 2019 全套入门教程 课后作业 | 菲菲与帆](https://www.bilibili.com/video/bv17E41157Pz) 。\n\n我的一条 VLOG:[【VLOG】#01 新年之旅 成都 第一日 | 菲菲与帆](https://www.bilibili.com/video/BV1K7411o73B) 欢迎来一键三连。\n\n<iframe src=\"//player.bilibili.com/player.html?aid=97184760&bvid=BV1K7411o73B&cid=166268624&page=1\" scrolling=\"no\" border=\"0\" frameborder=\"no\" framespacing=\"0\" allowfullscreen=\"true\" width=\"480px\" height=\"270px\"> </iframe>\n\n## 01-基础流程\n\n1、窗口乱了后:【窗口-工作区-编辑&重置为保存的布局】`alt + shift + 0`\n2、【文字工具】加字幕\n3、修改字体:【窗口-基础图形】选择编辑\n4、【剃刀】剪切多余素材\n5、本素材图片来自 <http://unsample.net/>\n\n## 02-转场效果\n\n1、选择素材点击左键拖拽到【左下窗口】 右下的文件夹可以快速新建文件夹,方便素材分类\n2、时序新建时,选择 HDV 720\n3、`ctrl + d` 添加转场效果\n4、【左下窗口】(项目窗口)选择【效果】视频过渡,可以切换效果\n5、点击时序上的过渡,【左上窗口】选择【效果控件】,可以编辑转场效果的持续时间、对齐等\n\n## 03&04-视频转场特效&音频特效\n\n1、【左上窗口】选择【效果控件】,可以编辑视频效果、过渡效果\n2、视频效果,可以点击秒表的图标添加关键帧\n3、新建时查看、修改【暂存盘】位置\n4、【编辑-首选项-时间轴】可以修改:静止图片默认持续时间等,\n已经导入的素材无效\n5、【时序窗口】计时器可以点击编辑,输入 500 会跳到第 5 秒的位置\n6、音频过渡,结尾声音淡出\n\n## 05&06-视频剪辑&音画对应\n\n1、【左上窗口】点击 `i` 标记入点,点击 `o` 标记出点\n2、【左上窗口】窗口右下角的 + 是按钮编辑器\n3、【左上窗口】标记完成后可以点击 插入 或者 覆盖\n4、【节目窗口】点击 `m` 可以标记\n5、【节目窗口】窗口右下角的 +,添加转到下一个标记\n6、转到下一个标记配合在时序上的标,踩点视频\n\n## 07&08-简单动画&素材嵌套\n\n1、【左上窗口】效果控件,可以修改素材位置、缩放\n2、【左上窗口】效果控件,点击小秒表可以添加关键帧动画\n3、导出素材前可以修改静止图片默认持续时间\n4、创建多个时序,再创建一个 master 时序,作为主时序\n5、时序拖入时序时,注意点击前面的 `V1` `A1`,否则可能只有视频或音频\n6、【时序窗口】时序取消链接,可以删除音频轨\n7、按 `alt` 键拖拽可以快速复制\n\n## 09&10&11-自定义转场&视频效果&变形和扭曲\n\n1、使用 PS 制作灰度图,新建 颜色模式 灰度,图片要和视频尺寸一致\n2、在线图片编辑 `https://www.uupoop.com/`\n3、图片使用 `tiff` 格式,原因不懂\n4、视频过渡 擦除 渐变擦除,然后可以选择自己制作的图片\n5、从【效果】可以选择后向上拖入【效果控件】\n6、`fx` 可以开启或关闭效果,还有 清除、复制、粘贴\n7、【项目窗口】新建 调整图层,可以给视频加一个整体的效果\n8、要善于使用标记定位\n9、视频效果:变形、扭曲\n\n## 12-模板的下载和使用\n\n1、.mogrt 模板\n2、导入方法 1:【图形】安装动态图形模板\n3、导入方法 2:【窗口】基础图形,游览,右下角加号\n4、导入方法 3:将模板文件复制到\nMac:`username/Library/Application Support/Adobe/Common/motion Graphics Template/`\nWin:`root://Users/Username/AppData/Roaming/Adobe/Common/Motion Graphics Templates`\n5、【基础图形】编辑,可以修改模板内容\n6、按 `enter` 键,可以渲染视频\n\n## 13-标准模板的使用\n\n1、`.prproj` 模板\n2、【视图】显示标尺、显示参考线,方便对齐\n\n## 14&15-倒计数器及色彩调节&传统与图形字幕\n\n1、【项目窗口】新建 通用倒计时片头,就是到 2 就没了\n2、【向前选择轨道工具】`ctrl + a`\n3、视频效果 扭曲 球面化\n4、视频效果 扭曲 偏移\n5、视频效果 杂色与颗粒 杂色\n6、【文件】新建 字幕 开放式字幕\n\n-- EOF --\n","tags":["pr"]},{"title":"PHP FTP 间歇性无法上传文件","url":"/2020/02/27/php-ftp-intermittently-unable-upload/","content":"\n2020-04-23 后记:疑似是前置的负载均衡服务器有问题,改为直接使用真实 IP 后问题消失。\n\n---\n\n```bash\nPHP Warning: ftp_put(): php_connect_nonb() failed: Operation now in progress (115) in ...\n```\n\n<!-- more -->\n\n环境 `PHP 5.6.40`。在开发过程中遇到了一个很诡异的情况,在使用 FTP 函数上传文件时,会间歇性无法上传文件。找了几圈有说是 PHP bug、有说是防火墙,都不解决问题。\n\n最后找到了一篇 [Why is my PHP script intermittently unable to upload a file via FTP? | stackoverflow](https://stackoverflow.com/questions/42439316/why-is-my-php-script-intermittently-unable-to-upload-a-file-via-ftp) 解了大急。(当搜索结果没有找到答案时,可以考虑换几个相近的词再试试)\n\n解决方法:进行循环调用尝试。\n\n```php\n // 尝试 5 次\n $uploaded = false;\n $tries = 0;\n while (!$uploaded && $tries <= 5) {\n ++$tries;\n $conn = ftp_ssl_connect($host, $port, 10) or die('FTP服务器连接失败');\n //登陆(通过用户名或者匿名登陆)\n $result = ftp_login($conn, $user, $password);\n if (!$result) {\n ftp_close($conn);\n die('ftp_login 失败');\n }\n\n ftp_set_option($conn, FTP_USEPASVADDRESS, false);\n\n // turn passive mode on\n ftp_pasv($conn, true);\n\n $success = ftp_put($conn, $remoteFile, $localFile, FTP_BINARY);\n if ($success) {\n $uploaded = true;\n dump(\"ftp upload: $success, $remoteFile, $localFile\");\n } else {\n dump(\"ftp upload failed, tries: $tries\");\n }\n\n ftp_close($conn);\n }\n```\n\n循环 try 第二次就成功了,不知为啥。[PHP の FTP(S)通信で php_connect_nonb() failed: Operation now in progress (115) | qiita](https://qiita.com/skawata/items/96895fe4eb5e6bbbf92c) 这篇文章有分析,读后还是不太清楚,望有缘人来指导。\n\n> どうやら Operation now in progress (115)は EINPROGRESS から来ているらしい。常に非同期で connect するため最初の connect で EINPROGRESS が帰るのは正常だが、asynchronous=true の場合その後同期処理のために poll で接続完了を待つ。だが poll はエラーが起きても errno を更新しないため、接続に失敗したとき errno=EINPROGRESS の状態で返ってしまっているように見える。\n\n-- EOF --\n","tags":["php"]},{"title":"回顾 2019","url":"/2019/12/31/review-2019/","content":"\n[新裤子 · 夏日终曲](https://y.qq.com/n/ryqq/songDetail/235474091)\n\n2019 年的第一天从三亚开始,在 _第一市场海鲜市场_ 买了海味,然后直接到旁边的 _林姐香味海鲜_ 加工,迟到了超级好吃的香辣蟹。飞回北京就是滑雪团建,年后双双得到了最佳合作奖,感谢我们每周的火锅,让大家多了些感情。\n\n-- EOF --\n\n年后买了烤箱烹饪技术再次升级,烤鸡翅、披萨、蛋挞甚至烤鱼,都已不在话下。受 shilei 推荐也喜欢玩德州扑克。看 B 站 UP 主自驾游,疯狂迷恋上了房车一段时间,还去看了一次房山的房车展(够远的),应该会结下不解之缘。\n\n3 月 29 日公司 7 周年,第一次庆祝公司周年,吃蛋糕前大家合了张影。这张照片我挺感动的,说不好具体的原因,可能是因为大家的笑容吧。清明小假在奥森玩了个露营,是真的有点冷。涛爷来北京面试,转头就成了科大博士,给劲儿。\n\n4 月中的一个双休日和爸妈南下,和比比定了亲 🎉🎉🎉。\n\n5 月乐队的夏天,是今年开过最好的综艺,刺猬、新裤子很棒。\n\n2019 年过去了,我很怀念它。\n","categories":["review"]},{"title":"Lonicera Framework","url":"/2019/12/19/lonicera-framework/","content":"\n项目代码:[imzyf/lonicera | GitHub](https://github.com/imzyf/lonicera)\n\n> 【PHP 核心技术与最佳实践】第二版 第 6 章 读书笔记\n\nLonicera Framework - Every French soldier carries a marshal’s baton in his knapsack.\n\n## MVC\n\nMVC 模式的目的是实现一种动态的程序设计,使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。\n\n## Lonicera 0.1\n\n### bootstrap\n\nindex.php 单一入口模式。\n\n启动 PHP 内置 Web 服务器:\n\n```bash\nphp -S localhost:7070\n```\n\n### 路由器层\n\n更偏向于使用 PATH_INFO 方式来访问。\n\n从传统 URL 参数模式的访问地址进行解析,提取里面的 group、controller、action、param 4 个参数,随后交给 bootstrap 进行 dispatch 处理。\n\n### 数据模型\n\n用 PDO 来实现连接数据库。\n\n- ORM Object Relational Mapping 对象与数据库的映射叫作对象关系映射\n- PO Persistent Object 把一个数据库中的表的一行记录对应的对象称为持久对象\n- BO Business Object 业务对象 把业务逻辑封装为一个对象\n- VO Value Object 值对象 界面显示的数据对象\n- DTO Data Transfer Object 用在热呵呵需要数据传输的地方\n- DAO Data Access Object 指代 Active Record 模式中的数据对象\n\n传统的 ORM 模式提倡数据对象和负责持久化的代码的分开,但是这并没有坚持数据操作的工作量。还有一种 ORM 模式叫作 Active Record。在 Active Record 中,模型层集成了 ORM 的功能,他们及代表实体,包含因为业务逻辑,又是数据对象,并负责把自己存储到数据库中。\n\nActive Record 模式中的数据对象不再是 PO 对象,而是 DAO。\n\n一系列的数据库操作组合起来,称之为 Service。Service 向下负责与数据库打交道,向上负责接收页面传递的参数以及数据的传输。理论上应该对 DAO 进项抽象到一个 Service 中。\n\n### 视图层\n\nPHP MVC 中的显示层开始朝着轻量化、API 化发展了。\n\n增强一个类通常途径:\n\n1. 使用 `__call` `__set` `__get` 等魔术方法\n2. 使用反射\n3. 使用 `trait`\n4. 使用继承和组合\n\n### 初步改进\n\n`spl_autoload_register` 统一加载文件。\n\n## Lonicera 0.2\n\n### 引入异常机制\n\n同时设置 `set_error_handler` 和 `set_exception_handler`。PHP 同时存在错误和异常两个互不包含的概念。\n\n### 拦截器与插件\n\n`dispatch` 中处理。使用正则匹配、`call_user_func`。\n\n### Request 增强与安全防御\n\n包装 `$_REQUEST`。\n\n## Lonicera 0.3\n\n### Composer 类加载机制\n\n`PSR-4`。\n\n### Model 增强\n\n`illuminata/database`。\n\n### 控制反转与依赖注入\n\n`Inversion of Control` `IoC`,`Dependency Injection` `DI`。\n\n- 谁控制谁?IoC 容器控制了对象。\n- 控制什么?主要控制了外部资源获取(不只是对象创建,还包括比如文件等)。\n\n传统应用程序是:由我们自己在对象中主动控制去直接获取依赖对象,而反转则是:由容器来帮忙创建及注入依赖对象。由于是容器帮我们查找及注入依赖对象,对象只是被动接受依赖对象,所以是反转。\n\n- 哪些方面反转了?依赖对象的获取被反转了。\n\n在我们使用 UserOrder 对象的时候,不再需要手动去创建 User 对象和 Order 对象了,而是直接问 IoC 容器去要 UserOrder 对象,IoC 容器会负责查找 UserOrder 的依赖并替我们创建 User 对象和 Order 对象,并管理它们。也就是说和我们打交道的是 IoC 容器。\n\n依赖注入,是组件之间依赖关系由容器在运行期决定形象来说,即由容器动态地将某个依赖关系注入到组件之中。依赖注入的目的并非是为软件系统带来更多功能,而是为了提高组件重用的效率,并为系统搭建一个灵活、可扩展的平台。\n\n通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。\n\n- 谁依赖于谁?当然是应用程序依赖于 IoC 容器。\n- 为什么需要依赖?应用程序需要 IoC 容器来提供对象需要的外部资源。\n- 谁注入谁?IoC 容器注入应用程序某个对象,应用程序依赖的对象。\n- 注入了什么?注入某个对象所需要的外部资源(包括对象、资源、常量数据)。\n\nIoC 和 DI 是两个相辅相成的概念,IoC 的实现是使用了 DI,而 DI 的目的是为了实现 IoC。实际上,它们是同一个概念的不同角度描述。\n\n为什么要这么做,这么做有什么意义呢?依赖注入帮我们降低了创建对象的成本,使得对象之间松耦合。\n\n`composer` 通过 `composer.json` 来管理第三方依赖,从这个角度讲,`composer` 就是一个 IoC 容器。\n\n`PSR-11` `Psr\\Contanier\\ContainerInterface`。\n\n## DB SQL\n\n```sql\nSET NAMES utf8mb4;\nSET FOREIGN_KEY_CHECKS = 0;\n\n-- ----------------------------\n-- Table structure for user\n-- ----------------------------\nDROP TABLE IF EXISTS `user`;\nCREATE TABLE `user` (\n `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名字',\n `age` int(10) unsigned NOT NULL DEFAULT '0',\n PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n\nSET FOREIGN_KEY_CHECKS = 1;\n```\n\n-- EOF --\n","tags":["php","composer"]},{"title":"Windows 文本文件到 Mac 中文乱码","url":"/2019/12/09/windows-text-file-to-mac-chinese-messy-code/","content":"\n文本文件从 Windows 系统复制到 Mac 系统中文发生乱码,原因肯定是编码问题。\n\n<!-- more -->\n\n## 解决办法\n\n[iconv | wikipedia](https://zh.wikipedia.org/wiki/Iconv) 它的作用是在多种国际编码格式之间进行文本内码的转换。\n\n```bash\niconv [OPTION...] [-f ENCODING] [-t ENCODING] [INPUTFILE...]\niconv -f <原始编码> -t <目标编码> <输入文件> -o <输出文件>\n\niconv -f GB18030 -t utf-8 <infile.txt> outfile.txt\n```\n\n## References\n\n- [文本文件从 Windows 拷贝到 Mac 乱码 | super2bai](https://super2bai.github.io/codec/w2m.html)\n\n-- EOF --\n","tags":["mac"]},{"title":"Git log 统计分析","url":"/2019/12/03/analyze-git-log/","content":"\n## 统计个人增删行数\n\n```bash\ngit config user.name\n\ngit log --author=\"zhaoyifan\" --pretty=tformat: --numstat | awk '{ add += $1; subs += $2; loc += $1 - $2 } END { printf \"added lines: %s, removed lines: %s, total lines: %s\\n\", add, subs, loc }' -\n```\n\n<!-- more -->\n\n```txt\nadded lines: 82813, removed lines: 53707, total lines: 29106\n```\n\n## 统计每个人增删行数\n\n```bash\ngit log --shortstat | grep -E \"(Author: )(\\s*(\\w+)){2}|fil(e|es) changed\" | awk '\n{\n if($1 ~ /Author/) {\n author = $2\" \"$3\n } else {\n files[author]+=$1\n inserted[author]+=$4\n deleted[author]+=$6\n }\n}\nEND { for (key in files) { print \"Author: \" key \"\\n\\tfiles changed: \", files[key], \"\\n\\tlines inserted: \", inserted[key], \"\\n\\tlines deleted: \", deleted[key] } }\n'\n```\n\n## 统计每个人提交次数\n\n```bash\ngit shortlog -sn --no-merges\n```\n\n## 忽略某些路径的更改\n\n```bash\ngit log -- . \":(exclude)sub\"\ngit log -- . \":!sub\"\n```\n\n## cloc\n\n[AlDanial/cloc - cloc counts blank lines, comment lines, and physical lines of source code in many programming languages.](https://github.com/AlDanial/cloc)\n\n```bash\ncloc source/\n\n 186 text files.\n 186 unique files.\n 3 files ignored.\n\ngithub.com/AlDanial/cloc v 1.84 T=0.16 s (1112.3 files/s, 138678.8 lines/s)\n-------------------------------------------------------------------------------\nLanguage files blank comment code\n-------------------------------------------------------------------------------\nMarkdown 183 6028 0 16788\n-------------------------------------------------------------------------------\nSUM: 183 6028 0 16788\n-------------------------------------------------------------------------------\n```\n\n## Git Fame\n\n[Git Fame - A command-line tool that helps you summarize and pretty-print git collaborators based on contributions](http://oleander.io/git-fame-rb/)\n\n```txt\ngem install git_fame\ncd /path/to/gitdir && git fame\nTotal number of files: 2,053\nTotal number of lines: 63,132\nTotal number of commits: 4,330\n\n+------------------------+--------+---------+-------+--------------------+\n| name | loc | commits | files | percent |\n+------------------------+--------+---------+-------+--------------------+\n| Johan Sørensen | 22,272 | 1,814 | 414 | 35.3 / 41.9 / 20.2 |\n| Marius Mathiesen | 10,387 | 502 | 229 | 16.5 / 11.6 / 11.2 |\n| Jesper Josefsson | 9,689 | 519 | 191 | 15.3 / 12.0 / 9.3 |\n| Ole Martin Kristiansen | 6,632 | 24 | 60 | 10.5 / 0.6 / 2.9 |\n| Linus Oleander | 5,769 | 705 | 277 | 9.1 / 16.3 / 13.5 |\n| Fabio Akita | 2,122 | 24 | 60 | 3.4 / 0.6 / 2.9 |\n| August Lilleaas | 1,572 | 123 | 63 | 2.5 / 2.8 / 3.1 |\n| David A. Cuadrado | 731 | 111 | 35 | 1.2 / 2.6 / 1.7 |\n| Jonas Ängeslevä | 705 | 148 | 51 | 1.1 / 3.4 / 2.5 |\n| Diego Algorta | 650 | 6 | 5 | 1.0 / 0.1 / 0.2 |\n| Arash Rouhani | 629 | 95 | 31 | 1.0 / 2.2 / 1.5 |\n| Sofia Larsson | 595 | 70 | 77 | 0.9 / 1.6 / 3.8 |\n| Tor Arne Vestbø | 527 | 51 | 97 | 0.8 / 1.2 / 4.7 |\n| spontus | 339 | 18 | 42 | 0.5 / 0.4 / 2.0 |\n| Pontus | 225 | 49 | 34 | 0.4 / 1.1 / 1.7 |\n+------------------------+--------+---------+-------+--------------------+\n```\n\n## 让 git log 的输出漂亮简洁\n\n```bash\ngit config --global alias.lg \"log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an %ae>%Creset' --abbrev-commit\"\n```\n\n> <https://gist.github.com/johanmeiring/3002458>\n\n在 `~/.gitconfig` 看到:\n\n```bash\n[alias]\n lg = log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an %ae>%Creset' --abbrev-commit\n```\n\n这其中的占位符的解释:\n\n- %Cred:将颜色设置为红色\n- %Creset:重置颜色\n- %C(yellow):将颜色设置为黄色\n- %h:缩略的提交哈希值\n- %d:ref names(不知道该怎么解释 😆)\n- %s:提交时的 message\n- %cr:相对的提交时间。可以换成%ci,展示的是年月日时分秒。\n- %an:提交人的 name\n- %ae:提交人的邮箱\n\n更多占位符,可以参考官方文档:<https://git-scm.com/docs/pretty-formats>\n\n## References\n\n- [Git count lines by author | gist ezamelczyk](https://gist.github.com/ezamelczyk/78b9c0dd095f8706a3f6a41e8eae0afd)\n- [Making 'git log' ignore changes for certain paths | stackoverflow](https://stackoverflow.com/questions/5685007/making-git-log-ignore-changes-for-certain-paths)\n- [让 git log 的输出漂亮简洁 | letianbiji](https://www.letianbiji.com/git/git-log.html)\n\n-- EOF --\n","tags":["git"]},{"title":"Git 批量修改历史 commit 中 user.email","url":"/2019/12/02/git-modify-history-commont-user-email/","content":"\n注意:**此操作会修改 Git 历史记录**,正式工作环境不允许。\n\n<!-- more -->\n\n查询都有什么:\n\n```bash\ngit log --format='%aN %aE' | sort -u\n```\n\n注:一个特殊情况如果 email 没被设置过 OLD_EMAIL 可以填 `user.name`。\n\n- OLD_EMAIL 原来的邮箱\n- CORRECT_NAME 更正的名字\n- CORRECT_EMAIL 更正的邮箱\n\n```bash\ngit filter-branch -f --env-filter '\nOLD_EMAIL=\"[email protected]\"\nCORRECT_NAME=\"MyName\"\nCORRECT_EMAIL=\"[email protected]\"\nif [ \"$GIT_COMMITTER_EMAIL\" = \"$OLD_EMAIL\" ]\nthen\n export GIT_COMMITTER_NAME=\"$CORRECT_NAME\"\n export GIT_COMMITTER_EMAIL=\"$CORRECT_EMAIL\"\nfi\nif [ \"$GIT_AUTHOR_EMAIL\" = \"$OLD_EMAIL\" ]\nthen\n export GIT_AUTHOR_NAME=\"$CORRECT_NAME\"\n export GIT_AUTHOR_EMAIL=\"$CORRECT_EMAIL\"\nfi\n' --tag-name-filter cat -- --branches --tags\n```\n\n因为修改了 Git 历史所有要使用强制推送:\n\n```bash\ngit push --f --tags\n```\n\nGitLab 有 master 分支保护的策略:\n\n```bash\nremote: GitLab: You are not allowed to force push code to a protected branch on this project.\n```\n\n在 GitLab 中:Project(项目) -> Setting -> Repository 菜单下面的 Protected branches 把 master 的保护去掉就可以了。\n\n## References\n\n- [Git 批量修改历史 commit 中的 user.name 和 user.email | segmentfault](https://segmentfault.com/a/1190000008032330)\n\n-- EOF --\n","tags":["git"]},{"title":"CentOS yum 升级 git 版本","url":"/2019/11/25/centos-upgrade-git-by-yum/","content":"\n先去官网看看 [Download for Linux and Unix](https://git-scm.com/download/linux):\n\n```txt\nRHEL and derivatives typically ship older versions of git. You can download a tarball and build from source, or use a 3rd-party repository such as the [IUS Community Project](https://ius.io/) to obtain a more recent version of git.\n```\n\nRHEL 和衍生通常提供较老版本的 git。您可以下载 tarball 并从源代码构建,或者使用第三方存储库,如 [IUS Community Project](https://ius.io/) 来获得最新版本的 git。\n\n## 使用 IUS\n\nRHEL/CentOS 7\n\n```bash\nyum install \\\nhttps://repo.ius.io/ius-release-el7.rpm \\\nhttps://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm\n```\n\n安装新版 git:\n\nius 通常会在高版本的软件名后面 + u\n\n```bash\nyum list git\n```\n\n如果你已经装有低版本的 git,你需要先 remove(否则安装的时候会报错)\n\n```bash\nyum remove git\n```\n\n安装 2.0 以上版本的 git\n\n```bash\nyum install git2u\n```\n\n## WANdisco\n\n另一个源:[WANdisco Replication Binaries](http://docs.wandisco.com/git/binaries/)\n\n```bash\nsudo vi /etc/yum.repos.d/wandisco-git.repo\n```\n\nwandisco-git.repo\n\n```txt\n[wandisco-git]\nname=Wandisco GIT Repository\nbaseurl=http://opensource.wandisco.com/centos/7/git/$basearch/\nenabled=1\ngpgcheck=1\ngpgkey=http://opensource.wandisco.com/RPM-GPG-KEY-WANdisco\n```\n\nImport the repository GPG keys with:\n\n```bash\nsudo rpm --import http://opensource.wandisco.com/RPM-GPG-KEY-WANdisco\n```\n\n```bash\nsudo yum remove git\n\nsudo yum install git\ngit --version\n```\n\n## References\n\n- [How To Install Git on CentOS 7 | linuxize](https://linuxize.com/post/how-to-install-git-on-centos-7/)\n- [How to install latest version of git on CentOS 7.x/6.x | stackoverflow](https://stackoverflow.com/questions/21820715/how-to-install-latest-version-of-git-on-centos-7-x-6-x)\n\n-- EOF --\n","tags":["git","centos"]},{"title":"CRLF will be replaced by LF","url":"/2019/11/22/crlf-will-be-replaced-by-lf/","content":"\n```bash\ngit add .\n\nwarning: CRLF will be replaced by LF in X.\n```\n\n- CRLF:windows 环境下的换行符\n- LF:linux 环境下的换行符\n\n这个错误的意思,就是文件中存在两种环境的换行符,git 会自动替换 CRLF 为 LF ,所以提示警告。\n\n<!-- more -->\n\n首先推荐扩展阅读:[配置 Git 处理行结束符 | GitHub](https://help.github.com/cn/github/using-git/configuring-git-to-handle-line-endings)\n\n我项目中是配置了 `.gitattributes` 的:\n\n```txt\n# Set the default behavior, in case people don't have core.autocrlf set.\n* text=auto\n\n# Explicitly declare text files you want to always be normalized and converted\n# to native line endings on checkout.\n*.c text\n*.h text\n\n# Declare files that will always have CRLF line endings on checkout.\n*.sln text eol=crlf\n\n# Denote all files that are truly binary and should not be modified.\n*.png binary\n*.jpg binary\n```\n\n## .gitattributes\n\n公式:`要匹配的文件模式 属性1 属性2 ...`。\n\n### 示例 1\n\n```txt\n* text=auto\n```\n\n对任何文件,设置 text=auto,表示文件的行尾自动转换。如果是文本文件,则在文件入 Git 库时,行尾自动转换为 LF。如果已经在入 Git 库中的文件的行尾为 CRLF,则该文件在入 Git 库时,不再转换为 LF。\n\n### 示例 2\n\n```txt\n*.txt text\n```\n\n对于 txt 文件,标记为文本文件,并进行行尾规范化。\n\n### 示例 3\n\n```txt\n*.jpg -text\n\n*.jpg binary\n```\n\n对于 jpg 文件,标记为非文本文件,不进行任何的行尾转换。`*.jpg -text` 可能是旧版本的写法。\n\n### 示例 4\n\n```txt\n*.vcproj text eol=crlf\n```\n\n对于 vcproj 文件,标记为文本文件,在文件入 Git 库时进行规范化,即行尾为 LF。但是在检出到工作目录时,行尾自动转换为 CRLF。\n\n### 示例 5\n\n```txt\n*.sh text eol=lf\n```\n\n对于 sh 文件,标记为文本文件,在文件入 Git 库时进行规范化,即行尾为 LF。在检出到工作目录时,行尾也不会转换为 CRLF(即保持 LF)。\n\n### 示例 6\n\n```txt\n*.py eol=lf\n```\n\n对于 py 文件,只针对工作目录中的文件,行尾为 LF。\n\n## 还是有问题\n\n在项目中已经添加 `.gitattributes` 文件,但是还是出现了报错,这时要检查 git 的版本。(CentOS 自带的 git 版本较低)\n\n> 可以参考:[CentOS yum 升级 git 版本](https://zyf.im/2019/11/25/centos-upgrade-git-by-yum/)\n\n后来还查到了一个方法,`.gitattributes`:\n\n```txt\n* -crlf\n```\n\n## References\n\n- [Git 的 gitattributes 文件详解 | csdn](https://blog.csdn.net/taiyangdao/article/details/78484623)\n- [gitattributes - Defining attributes per path | git-scm](https://git-scm.com/docs/gitattributes)\n- [How to make Git ignore different line endings | rtuin](https://www.rtuin.nl/2013/02/how-to-make-git-ignore-different-line-endings/)\n\n-- EOF --\n","tags":["git"]},{"title":"Vue Code Snippet","url":"/2019/08/08/vue-code-snippet/","content":"\n## Style Guide\n\n<https://cn.vuejs.org/v2/style-guide/>\n\n<!-- more -->\n\n## 重置 data 的数据为初始状态\n\n```js\nthis.$data = { ...this.$data, ...this.$options.data() };\n// Object.assign(this.$data, this.$options.data());\nObject.assign({}, this.$options.data().form, { type: 1 });\n```\n\n## 修改 ElementUI 默认样式\n\n> [vue 中令人头疼的 element-ui 组件默认 css 样式修改 | juejin](https://juejin.cn/post/7011016159545786376)\n\n```html\n<template>\n <div class=\"my-class\">\n <el-table> </el-table>\n </div>\n</template>\n<style lang=\"scss\" scoped>\n .my-class__expand-column .cell {\n display: none;\n }\n\n .my-class .el-table tbody tr:hover > td {\n cursor: pointer;\n }\n</style>\n```\n\n## watch 对象的属性\n\n```js\ndata: {\n a: 100,\n msg: {\n channel: \"音乐\",\n style: \"活泼\"\n }\n},\nwatch: {\n a(newval, oldVal) {\n console.log(\"new: %s, old: %s\", newval, oldVal);\n }\n}\n```\n\n```js\nwatch: {\n msg: {\n handler(newValue, oldValue) {\n console.log(newValue)\n },\n // 深度监听\n deep: true\n }\n}\n```\n\n监听对象内的某一具体属性,可以通过 `computed` 做中间层来实现:\n\n```js\ncomputed: {\n channel() {\n return this.msg.channel\n }\n},\nwatch:{\n channel(newValue, oldValue) {\n console.log('new: %s, old: %s', newval, oldVal)\n }\n}\n```\n\n## 判断 data 中的对象是否为空\n\n1、利用 `jQuery` 的 `isEmptyObject`:\n\n```js\n$.isEmptyObject(data.list);\n```\n\n实现源码:\n\n```js\nisEmptyObject: function(obj) {\n var name;\n for (name in obj) {\n return false;\n }\n return true;\n}\n```\n\n2、获取到对象中的属性名,存到一个数组中,通过判断数组的 `length` 来判断此对象是否为空:\n\n```js\nvar arr = Object.getOwnPropertyNames(data);\nalert(arr.length == 0); // true\n```\n\nES6:\n\n```js\nvar arr = Object.keys(data.list);\nalert(arr.length == 0); // true\n```\n\n3、转化为 `json` 字符串,再判断该字符串是否为 `{}`:\n\n```js\nvar b = JSON.stringify(data) == \"{}\";\nalert(b); // true\n```\n\n## 关闭 webpack-dev-server 自动刷新\n\n文件保存后页面刷新,刷的眼晕。\n\n```js\ndevServer: {\n ...\n hot: false,\n inline: false,\n liveReload: false\n}\n```\n\n注释掉 `plugins` 中的相关插件。\n\n```js\nplugins: [\n // new webpack.HotModuleReplacementPlugin()\n],\n```\n\n> 必须有 webpack.HotModuleReplacementPlugin 才能完全启用 HMR。如果 webpack 或 webpack-dev-server 是通过 --hot 选项启动的,那么这个插件会被自动添加,所以你可能不需要把它添加到 webpack.config.js 中。\n\n## nextTick\n\n在 Vue 生命周期的 `created()` 钩子函数进行的 DOM 操作一定要放在 `Vue.nextTick()` 的回调函数中。\n\n在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的 DOM 结构的时候,这个操作都应该放进 `Vue.nextTick()` 的回调函数中。\n\n> Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 `watcher` 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环 \"tick\" 中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部尝试对异步队列使用原生的 `Promise.then` 和 `MessageChannel`,如果执行环境不支持,会采用 `setTimeout(fn, 0)` 代替。\n\n## 键盘监听事件\n\n> [按键修饰符 | vue](https://cn.vuejs.org/v2/guide/events.html#%E6%8C%89%E9%94%AE%E4%BF%AE%E9%A5%B0%E7%AC%A6)\n\n```html\n<input type=\"text\" @keyup.esc=\"KeyUpEsc\" />\n```\n\n> [将原生事件绑定到组件 | vue](https://cn.vuejs.org/v2/guide/components-custom-events.html#%E5%B0%86%E5%8E%9F%E7%94%9F%E4%BA%8B%E4%BB%B6%E7%BB%91%E5%AE%9A%E5%88%B0%E7%BB%84%E4%BB%B6)\n\n使用 element 组件库的 el-input 标签,绑定 delete 键:\n\n```html\n<el-input\n v-model=\"input\"\n placeholder=\"请输入内容\"\n @keyup.delete.native=\"KeyUpDelete\"\n></el-input>\n```\n\n为什么这次绑定事件多一个 `.native` 修饰符,这个可能是因为 element-ui 封装了个 div 在 input 标签外面,把原来的事件隐藏了,所以如果不加 `.native` 的话,按键不会生效。\n\n上面两种实现效果是当 input 标签获取到 _焦点_ 的时候,才能监听到键盘,下面这种是全局监听 enter 键,是把监听事件绑定到 document 上(登录页面常用):\n\n```js\ncreated: function() {\n var _this = this;\n document.onkeydown = function(e) {\n let key = window.event.keyCode;\n if (key == 13) {\n _this.submit();\n }\n };\n},\n```\n\n## 键盘回车后页面被刷新\n\nform 回车后执行查询方法,但是却重新刷新了页面,并且路由多了一个问号,`http://localhost:3009/?#/admin`。\n\n解决方案:`el-from` 加上 `@submit.native.prevent`:\n\n```html\n<el-form @submit.native.prevent>\n <el-form-item>\n <el-input @keyup.enter.native=\"handleInputConfirm(obj)\"> </el-input>\n </el-form-item>\n</el-form>\n\n<script>\n export default {\n methods: {\n handleInputConfirm(obj) {\n // ... 提交内容\n },\n },\n };\n</script>\n```\n\n原因:当一个 `form` 元素中只有一个输入框时,在该输入框中按下回车应提交该表单。如果希望阻止这一默认行为,可以在标签上添加 `@submit.native.prevent`。\n\n> [vue 键盘回车事件导致页面刷新的问题,路由多了一个问号 | cnblogs](https://www.cnblogs.com/hibiscus-ben/p/10861062.html)\n\n解决方案二:为表单元素增加属性 `onSubmit=\"return false\"`。\n\n> [Vue element-ui 键盘回车事件表单自动提交造成页面刷新问题](https://weiku.co/article/297/)\n\n## .sync 修饰符\n\n[.sync 修饰符 | vue](https://cn.vuejs.org/v2/guide/components-custom-events.html#sync-%E4%BF%AE%E9%A5%B0%E7%AC%A6)\n\n```html\n<history-dialog\n :historys=\"historyTable\"\n :dialogVisible.sync=\"dialogHistoryTableVisible\"\n/>\n```\n\n```js\nmethods: {\n onClose() {\n this.$emit(\"update:dialogVisible\", false);\n },\n},\n```\n\n- [【Vue】Vue 中的父子组件通讯以及使用 sync 同步父子组件数据 | penghuwan](https://www.cnblogs.com/penghuwan/p/7473375.html)\n\n## 优化图片加载失败\n\n```html\n<div>\n <img :src=\"item.picture\" @error.once=\"handleImgError($event)\" />\n</div>\n```\n\n```js\nhandleImgError (e) {\n e.currentTarget.src = \"http://www.ianbiangou.cn/index/ICON2.png\"\n}\n```\n\n## 数据绑定\n\n```js\nthis.form = {}; // 不要用这种:this.form = '';\n```\n\n## References\n\n- [jQuery.isEmptyObject() 函数详解 | csdn](https://blog.csdn.net/wangqing84411433/article/details/79582888)\n- [简单理解 Vue 中的 nextTick | juejin](https://juejin.im/post/5a6fdb846fb9a01cc0268618)\n- [Vue 中键盘监听事件(解决 element 监听键盘不生效)| jianshu](https://www.jianshu.com/p/f2172afaf9bf)\n- [图片加载失败优化处理 | cnblogs](https://www.cnblogs.com/zhuzhenwei918/p/6891368.html)\n- [How to disable webpack dev server auto reload? | stackoverflow](https://stackoverflow.com/questions/41797704/how-to-disable-webpack-dev-server-auto-reload)\n","tags":["vue"],"categories":["code-snippet"]},{"title":"npx 入门使用","url":"/2019/07/17/npx-getting-started/","content":"\n今天在使用的 [Gulp](https://gulpjs.com/) 时看到首页的一段命令:\n\n```bash\n...\nnpx -p touch nodetouch gulpfile.js\n...\n```\n\n[npx](https://www.npmjs.com/package/npx) 是个啥?决定一探究竟。\n\n<!-- more -->\n\n## 调用项目安装的模块\n\n`npx` 是 npm 的自带模块,可以直接使用,手动安装为:\n\n```bash\nnpm install -g npx\n```\n\n`npx` - execute npm package binaries. 它解决的问题是:更方便的从本地 `node_modules/.bin` 或中央缓存中执行命令。\n\n比如项目安装了 `gulp` 需要执行命令时需要:\n\n```bash\nnode_modules/.bin/gulp -v\n```\n\n如果使用 `npx` 只需要:\n\n```bash\nnpx gulp -v\n```\n\n`npx` 的原理很简单,就是运行的时候,会到 `node_modules/.bin` 路径和环境变量 `$PATH` 里面,检查命令是否存在。由于 `npx` 会检查环境变量 `$PATH`,所以系统命令也可以调用。\n\n```bash\n# 等同于 ls\nnpx ls\n```\n\n注意,`Bash` 内置的命令不在 `$PATH` 里面,所以不能用。比如,`cd` 是 `Bash` 命令,因此就不能用 `npx cd`。\n\n## 避免全局安装模块\n\n`create-react-app` 这个模块是全局安装,`npx` 可以运行它,而且不进行全局安装:\n\n```bash\nnpx create-react-app my-react-app\n```\n\n上面代码运行时,`npx` 将 `create-react-app` 下载到一个临时目录,使用以后再删除。以后再次执行上面的命令,会重新下载 `create-react-app`。\n\n注意,只要 `npx` 后面的模块无法在本地发现,就会下载同名模块。比如,本地没有安装 `http-server` 模块,下面的命令会自动下载该模块,在当前目录启动一个 Web 服务。\n\n```bash\nnpx http-server\n```\n\n## `--no-install` 和 `--ignore-existing` 参数\n\n如果想让 `npx` 强制使用本地模块,不下载远程模块,可以使用 `--no-install` 参数。如果本地不存在该模块,就会报错。\n\n```bash\nnpx --no-install http-server\n```\n\n反过来,如果忽略本地的同名模块,强制安装使用远程模块,可以使用 `--ignore-existing` 参数。\n\n## 使用不同版本的 Node\n\n可以指定某个版本的 `Node` 运行脚本,某些场景下,这个方法用来切换 `Node` 版本,要比 `nvm` 那样的版本管理器方便一些。\n\n```bash\nnpx [email protected] -v\nv0.12.8\n```\n\n## `-p` 参数\n\n`-p` 参数用于指定 `npx` 所要安装的模块,所以上一节的命令可以写成下面这样。\n\n```bash\nnpx -p [email protected] node -v\nv0.12.8\n```\n\n上面命令先指定安装 `[email protected]` ,然后再执行 `node -v` 命令。\n\n所以开头时的疑问 `npx -p touch nodetouch gulpfile.js` 也就解答了,就是先安装 `touch` 在执行 `nodetouch gulpfile.js`。\n\n## 执行 GitHub 源码\n\n```bash\n# 执行 Gist 代码\nnpx https://gist.github.com/zkat/4bc19503fe9e9309e2bfaa2c58074d32\n\n# 执行仓库代码\nnpx github:piuccio/cowsay hellos\n```\n\n注意,远程代码必须是一个模块,即必须包含 `package.json` 和入口脚本。\n\n## References\n\n- [npx 使用教程 - 阮一峰](http://www.ruanyifeng.com/blog/2019/02/npx.html)\n\n-- EOF --\n","tags":["node-js","npm"]},{"title":"VSCode 使用经验","url":"/2019/06/11/vscode-using-experience/","content":"\n## 编辑 settings.json\n\n`command + ,` 右上角点击 `打开设置`。\n\n```json\n{\n // 通过整体放大窗口比例\n \"window.zoomLevel\": 1.1,\n // 字体\n \"editor.fontSize\": 14,\n // 软换行\n \"editor.wordWrap\": \"on\",\n}\n```\n\n## 显示空格和 tab 符号\n\n```txt\nEditor: Render Whitespace\n控制编辑器在空白字符上显示符号的方式。\nall\n```\n\n## Settings 同步\n\n只需从左下角的设置齿轮启用内置同步。\n\n## 扩展推荐\n\n- Community Material Theme\n- EditerConfig For VS Code\n- ESLint\n- Git History\n- GitLens — Git supercharged\n- markdownlint\n- Material Icon Theme\n- Material Theme\n- Prettier - Code formatter\n- Terminal\n- Vetur\n\n-- EOF --\n"},{"title":"Git 提交 message 规范","url":"/2019/06/04/git-commit-message-style-guide/","content":"\ncommit message 应该清晰明了,说明本次提交的目的。基本公式:\n\n```txt\n<type>(<scope>): <subject>\n```\n\n<!-- more -->\n\n完整公式:\n\n```txt\n<type>(<scope>): <subject>\n<BLANK LINE>\n<body>\n<BLANK LINE>\n<footer>\n```\n\n## type\n\n用于说明 commit 的类别,只允许使用下面 7 个标识。\n\n- feat:新功能(feature)\n- fix:修补 bug\n- docs:文档(documentation)\n- style:格式(不影响代码运行的变动)\n- refactor:重构(即不是新增功能,也不是修改 bug 的代码变动)\n- test:增加测试\n- chore:构建过程或辅助工具的变动\n\n## scope\n\n用于说明 commit 影响的范围,比如数据层、控制层、视图层等等,视项目不同而不同。\n\n## subject\n\n是 commit 目的的简短描述,不超过 50 个字符。\n\n1. 以动词开头,使用第一人称现在时,比如 change,而不是 changed 或 changes\n2. 第一个字母小写\n3. 结尾不加句号 `.`\n\n## cli 工具\n\n### commitizen cli\n\n[commitizen/cz-cli](https://github.com/commitizen/cz-cli) 使用它提供的 `git cz` 命令替代我们的 `git commit` 命令,帮助我们生成符合规范的 commit message。\n\n除此之外,我们还需要为 `commitizen` 指定一个 `Adapter` 比如:[commitizen/cz-conventional-changelog](https://github.com/commitizen/cz-conventional-changelog)(一个符合 Angular 团队规范的 preset)使得 `commitizen` 按照我们指定的规范帮助我们生成 commit message。\n\n全局安装(首选):\n\n```bash\nnpm install -g commitizen cz-conventional-changelog\necho '{ \"path\": \"cz-conventional-changelog\" }' > ~/.czrc\n```\n\n项目级安装(首选):\n\n```bash\nnpm install -D commitizen cz-conventional-changelog\n```\n\n`package.json` 中配置:\n\n```bash\n\"script\": {\n ...,\n \"commit\": \"git-cz\",\n},\n\"config\": {\n \"commitizen\": {\n \"path\": \"node_modules/cz-conventional-changelog\"\n }\n}\n```\n\n如果全局安装过 `commitizen`,那么在对应的项目中执行 `git cz` or `npm run commit` 都可以。\n\n### 自定义规范\n\n也许 Angular 的那套规范我们不习惯,那么可以通过指定 Adapter [leonardoanalista/cz-customizable](https://github.com/leonardoanalista/cz-customizable) 指定一套符合自己团队的规范。\n\n### 校验 message\n\n可以通过 [conventional-changelog/commitlint](https://github.com/conventional-changelog/commitlint) 校验 commit message。同时可以配合使用 [typicode/husky](https://github.com/typicode/husky) 🐶 Git hooks made easy。\n\n### 生成 CHANGELOG\n\n[conventional-changelog/standard-version](https://github.com/conventional-changelog/standard-version) 🏆 自动化版本和更新日志生成。\n\n还看到一个有意思的库 [📦🚀 semantic-release](https://github.com/semantic-release/semantic-release) 待研究。\n\n## References\n\n- [你可能会忽略的 Git 提交规范 - juejin](https://juejin.im/entry/5b429be75188251ac85830ff)\n- [优雅的提交你的 Git Commit Message - juejin](https://juejin.im/post/5afc5242f265da0b7f44bee4)\n- [Commit message 和 Change log 编写指南 - ruanyifeng](http://www.ruanyifeng.com/blog/2016/01/commit_message_change_log.html)\n- [AngularJS Git Commit Message Conventions - google docs](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#)\n\n-- EOF --\n","tags":["git"]},{"title":"最左前缀原理与相关优化","url":"/2019/05/25/the-left-prefix-index-rule/","content":"\nMySQL 中的索引可以以一定顺序引用多个列,这种索引叫做联合索引,一般的,一个联合索引是一个有序元组 `<a1, a2, …, an>`,其中各个元素均为数据表的一列。另外,单列索引可以看成联合索引元素数为 1 的特例。\n\n我们在 [Employees Sample Database](https://dev.mysql.com/doc/employee/en/employees-installation.html) 中实验,MySQL 版本 5.7。\n\n<!-- more -->\n\n以 employees.titles 为例,查看其索引:\n\n```sql\nSHOW INDEX FROM employees.titles;\n\n+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+\n| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |\n+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+\n| titles | 0 | PRIMARY | 1 | emp_no | A | 301292 | NULL | NULL | | BTREE | | |\n| titles | 0 | PRIMARY | 2 | title | A | 442605 | NULL | NULL | | BTREE | | |\n| titles | 0 | PRIMARY | 3 | from_date | A | 442605 | NULL | NULL | | BTREE | | |\n+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+\n```\n\n## 全列匹配\n\n```sql\nEXPLAIN SELECT * FROM employees.titles\nWHERE\n emp_no = '10009'\n AND title = 'Senior Engineer'\n AND from_date = '1995-02-18';\n\n+----+-------------+--------+------------+-------+---------------+---------+---------+-------------------+------+----------+-------+\n| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |\n+----+-------------+--------+------------+-------+---------------+---------+---------+-------------------+------+----------+-------+\n| 1 | SIMPLE | titles | NULL | const | PRIMARY | PRIMARY | 159 | const,const,const | 1 | 100.00 | NULL |\n+----+-------------+--------+------------+-------+---------------+---------+---------+-------------------+------+----------+-------+\n```\n\n当按照索引中所有列进行精确匹配(这里精确匹配指 `=` 或 `IN` 匹配)时,索引可以被用到。\n\n这里有一点需要注意,理论上索引对顺序是敏感的,但是由于 MySQL 的 **查询优化器会自动调整 where 子句的条件顺序** 以使用适合的索引,例如我们将 `where` 中的条件顺序颠倒:\n\n```sql\nEXPLAIN SELECT * FROM employees.titles\nWHERE\n from_date = '1995-02-18'\n AND emp_no IN ( '10009' )\n AND title = 'Senior Engineer';\n\n+----+-------------+--------+------------+-------+---------------+---------+---------+-------------------+------+----------+-------+\n| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |\n+----+-------------+--------+------------+-------+---------------+---------+---------+-------------------+------+----------+-------+\n| 1 | SIMPLE | titles | NULL | const | PRIMARY | PRIMARY | 159 | const,const,const | 1 | 100.00 | NULL |\n+----+-------------+--------+------------+-------+---------------+---------+---------+-------------------+------+----------+-------+\n```\n\n和上面是一样的。\n\n## 最左前缀匹配\n\n```sql\nEXPLAIN SELECT * FROM employees.titles\nWHERE\n emp_no = '10009';\n\n+----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------+\n| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |\n+----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------+\n| 1 | SIMPLE | titles | NULL | ref | PRIMARY | PRIMARY | 4 | const | 3 | 100.00 | NULL |\n+----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------+\n```\n\n当查询条件精确匹配索引的 **左边连续一个或几个列** 时,如 `<emp_no>` 或 `<emp_no, title>`,所以可以被用到,但是只能用到一部分,即条件所组成的最左前缀。\n\n上面的查询从分析结果看用到了 `PRIMARY` 索引,但是 `key_len` 为 4,说明只用到了索引的第一列前缀。\n\n## 中间某个条件未提供\n\n查询条件用到了索引中列的精确匹配,但是中间某个条件未提供。\n\n```sql\nEXPLAIN SELECT * FROM employees.titles WHERE emp_no='10009' AND from_date='1995-02-18';\n\n+----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------------+\n| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |\n+----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------------+\n| 1 | SIMPLE | titles | NULL | ref | PRIMARY | PRIMARY | 4 | const | 1 | 10.00 | Using where |\n+----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------------+\n```\n\n此时索引使用情况和情况二相同,因为 `title` 未提供,所以查询只用到了索引的第一列,而后面的 `from_date` 虽然也在索引中,但是由于 `title` 不存在而无法和左前缀连接,因此需要对结果进行扫描过滤 `from_date`(这里由于 `emp_no` 唯一,所以不存在扫描)。\n\n如果想让 `from_date` 也使用索引而不是 `where` 过滤,可以增加一个 _辅助索引_ `<emp_no, from_date>`,此时上面的查询会使用这个索引。\n\n除此之外,还可以使用一种称之为 **隔离列** 的优化方法,将 `emp_no` 与 `from_date` 之间的 _坑_ 填上。\n\n首先我们看下 `title` 有几种不同的值:\n\n```sql\nSELECT DISTINCT(title) FROM employees.titles;\n```\n\n只有 7 种。在这种成为 _坑_ 的列值比较少的情况下,可以考虑用 `IN` 来填补这个 _坑_ 从而形成最左前缀:\n\n```sql\nEXPLAIN SELECT * FROM employees.titles\nWHERE emp_no='10009'\nAND title IN ('Senior Engineer', 'Staff', 'Engineer', 'Senior Staff', 'Assistant Engineer', 'Technique Leader', 'Manager')\nAND from_date='1995-02-18';\n\n+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+\n| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |\n+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+\n| 1 | SIMPLE | titles | NULL | range | PRIMARY | PRIMARY | 159 | NULL | 7 | 100.00 | Using where |\n+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+\n```\n\n这次 `key_len` 为 `159`,说明索引被用全了,但是从 `type` 和 `rows` 看出 `IN` 实际上执行了一个 `range` 查询,这里检查了 7 个 key。看下两种查询的性能比较:\n\n```sql\nSET profiling = 1;\nSELECT * FROM... -- 1\nSELECT * FROM... -- 2\nSHOW PROFILES;\n\n| Query_ID | Duration | Query |\n+----------+------------+---------+\n| 1 | 0.00083950 | SELECT * ..|\n| 2 | 0.00063700 | SELECT * ..|\n```\n\n“填坑” 后性能提升了一点。如果经过 `emp_no` 筛选后余下很多数据,则后者性能优势会更加明显。当然,如果 `title` 的值很多,用填坑就不合适了,必须建立辅助索引。(笔者:多次测试后发现是有快有慢,可能是数据的原因,效果并不明显)\n\n## 查询条件没有指定索引第一列\n\n```sql\nEXPLAIN SELECT * FROM employees.titles WHERE from_date='1995-02-18';\n\n+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+\n| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |\n+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+\n| 1 | SIMPLE | titles | NULL | ALL | NULL | NULL | NULL | NULL | 442605 | 10.00 | Using where |\n+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+\n```\n\n由于不是最左前缀,索引这样的查询显然用不到索引。\n\n## 匹配某列的前缀字符串\n\n```sql\nEXPLAIN SELECT * FROM employees.titles WHERE emp_no=10009 AND title LIKE 'Senior%';\n\n+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+\n| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |\n+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+\n| 1 | SIMPLE | titles | NULL | range | PRIMARY | PRIMARY | 156 | NULL | 1 | 100.00 | Using where |\n+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+\n```\n\n此时可以用到索引,如果通配符 `%` 不出现在开头,则可以用到索引,但根据具体情况不同可能只会用其中一个前缀。\n\n## 范围查询\n\n```sql\nEXPLAIN SELECT * FROM employees.titles WHERE emp_no < '10010' and title = 'Senior Engineer';\n\n+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+\n| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |\n+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+\n| 1 | SIMPLE | titles | NULL | range | PRIMARY | PRIMARY | 4 | NULL | 14 | 10.00 | Using where |\n+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+\n```\n\n范围列可以用到索引(必须是最左前缀),但是范围列后面的列无法用到索引。同时,索引最多用于一个范围列,因此如果查询条件中有两个范围列则无法全用到索引。\n\n```sql\nEXPLAIN SELECT * FROM employees.titles\nWHERE emp_no < '10010'\nAND title = 'Senior Engineer'\nAND from_date BETWEEN '1986-01-01' AND '1986-12-31';\n\n+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+\n| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |\n+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+\n| 1 | SIMPLE | titles | NULL | range | PRIMARY | PRIMARY | 4 | NULL | 14 | 1.11 | Using where |\n+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+\n```\n\n可以看到索引对第二个范围索引无能为力。这里特别要说明 MySQL 一个有意思的地方,那就是仅用 explain 可能无法区分 范围索引 和 多值匹配,因为在 `type` 中这两者都显示为 `range`。\n\n同时,用了 `between` 并不意味着就是范围查询,例如下面的查询:\n\n```sql\nEXPLAIN SELECT * FROM employees.titles\nWHERE emp_no BETWEEN '10001' AND '10010'\nAND title='Senior Engineer'\nAND from_date BETWEEN '1986-01-01' AND '1986-12-31';\n\n+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+\n| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |\n+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+\n| 1 | SIMPLE | titles | NULL | range | PRIMARY | PRIMARY | 159 | NULL | 15 | 1.11 | Using where |\n+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+\n```\n\n看起来是用了两个范围查询,但作用于 `emp_no` 上的 `BETWEEN` 实际上相当于 `IN`,也就是说 `emp_no` 实际是多值精确匹配。可以看到这个查询用到了索引全部三个列。因此在 MySQL 中要谨慎地区分多值匹配和范围匹配,否则会对 MySQL 的行为产生困惑。\n\n还有个值得注意的事情:\n\n```sql\nEXPLAIN SELECT * FROM employees.titles where emp_no > 10000 AND emp_no < 10011 AND title='Senior Engineer' AND from_date BETWEEN '1986-01-01' AND '1986-12-31';\n\n+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+\n| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |\n+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+\n| 1 | SIMPLE | titles | NULL | range | PRIMARY | PRIMARY | 4 | NULL | 15 | 1.11 | Using where |\n+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+\n\nEXPLAIN SELECT * FROM employees.titles where emp_no >= 10001 and emp_no <= 10010 AND title='Senior Engineer' AND from_date BETWEEN '1986-01-01' AND '1986-12-31';\n\n+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+\n| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |\n+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+\n| 1 | SIMPLE | titles | NULL | range | PRIMARY | PRIMARY | 159 | NULL | 15 | 1.11 | Using where |\n+----+-------------+--------+------------+-------+---------------+---------+---------+------+------+----------+-------------+\n```\n\n> 疑问:`=` 影响 范围索引 还是 多值匹配?\n\n## 查询条件中含有函数或表达式\n\n很不幸,如果查询条件中含有函数或表达式,则 MySQL 不会为这列使用索引(虽然某些在数学意义上可以使用)。例如:\n\n```sql\nEXPLAIN SELECT * FROM employees.titles WHERE emp_no='10009' AND left(title, 6)='Senior';\n\n+----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------------+\n| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |\n+----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------------+\n| 1 | SIMPLE | titles | NULL | ref | PRIMARY | PRIMARY | 4 | const | 3 | 100.00 | Using where |\n+----+-------------+--------+------------+------+---------------+---------+---------+-------+------+----------+-------------+\n```\n\n虽然这个查询和情况五中功能相同,但是由于使用了函数 left,则无法为 title 列应用索引,而情况五中用 LIKE 则可以。\n\n```sql\nEXPLAIN SELECT * FROM employees.titles WHERE emp_no - 1 = 10000;\n\n+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+\n| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |\n+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+\n| 1 | SIMPLE | titles | NULL | ALL | NULL | NULL | NULL | NULL | 442605 | 100.00 | Using where |\n+----+-------------+--------+------------+------+---------------+------+---------+------+--------+----------+-------------+\n```\n\n显然这个查询等价于查询 `emp_no` 为 `10001` 的函数,但是由于查询条件是一个表达式,MySQL 无法为其使用索引。看来 MySQL 还没有智能到自动优化常量表达式的程度,因此在写查询语句时尽量避免表达式出现在查询中,而是先手工私下代数运算,转换为无表达式的查询语句。\n\n## References\n\n- [MySQL 索引背后的数据结构及算法原理](http://blog.codinglabs.org/articles/theory-of-mysql-index.html)\n- [13.7.5.30 SHOW PROFILE Syntax](https://dev.mysql.com/doc/refman/5.7/en/show-profile.html)\n\n-- EOF --\n","tags":["mysql"]},{"title":"归并排序","url":"/2019/05/23/merge-sort/","content":"\n归并排序(英语:Merge sort,或 mergesort),是创建在归并操作上的一种有效的排序算法,效率为 `O(nlogn)`。1945 年由约翰·冯·诺伊曼首次提出。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。\n\n<!-- more -->\n\n![merge-sort](https://user-images.githubusercontent.com/9289792/58546744-9751a300-8238-11e9-84d1-0d33d5eaacee.gif)\n\n采用分治法:\n\n- 分割:递归地把当前序列平均分割成两半。\n- 集成:在保持元素顺序的同时将上一步得到的子序列集成到一起(归并)。\n\n归并操作(merge),也叫归并算法,指的是将两个已经排序的序列合并成一个序列的操作。归并排序算法依赖归并操作。\n\n```php\n<?php\n\nfunction mergeSort($arr)\n{\n $len = count($arr);\n if ($len <= 1) {\n return $arr;\n } // 递归结束条件, 到达这步的时候, 数组就只剩下一个元素了, 也就是分离了数组\n\n $mid = $len / 2;\n $left = array_slice($arr, 0, $mid); // 拆分数组0-mid这部分给左边left\n $right = array_slice($arr, $mid); // 拆分数组mid-末尾这部分给右边right\n $left = mergeSort($left); // 左边拆分完后开始递归合并往上走\n $right = mergeSort($right); // 右边拆分完毕开始递归往上走\n\n $arr = merge($left, $right); // 合并两个数组,继续递归\n return $arr;\n}\n\n// merge函数将指定的两个有序数组(arrA, arr)合并并且排序\nfunction merge($arrA, $arrB)\n{\n $arrC = array();\n while (count($arrA) && count($arrB)) {\n // 这里不断的判断哪个值小, 就将小的值给到arrC, 但是到最后肯定要剩下几个值,\n // 不是剩下arrA里面的就是剩下arrB里面的而且这几个有序的值, 肯定比arrC里面所有的值都大所以使用\n $arrC[] = $arrA[0] < $arrB[0] ? array_shift($arrA) : array_shift($arrB);\n }\n\n return array_merge($arrC, $arrA, $arrB);\n}\n\n$startTime = microtime(1);\n\n$arr = range(1, 1000);\nshuffle($arr);\necho 'before sort: ', implode(', ', $arr), \"\\n\";\n$sortArr = mergeSort($arr);\necho 'after sort: ', implode(', ', $sortArr), \"\\n\";\n\necho 'use time: ', microtime(1) - $startTime, \"s\\n\";\n```\n\n假设被排序的数列中有 N 个数。遍历一趟的时间复杂度是 `O(N)`,需要遍历多少次呢?\n\n归并排序的形式就是一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据 **完全二叉树** 的可以得出它的时间复杂度是 `O(N*lgN)`。\n\n## References\n\n- [PHP 算法 —— 归并排序](https://shockerli.net/post/merge-sort-implement-by-php/)\n\n-- EOF --\n","tags":["php","algorithm"]},{"title":"My MacBook","url":"/2019/05/20/my-macbook/","content":"\n个人 MacBook 食用说明。推荐自动化环境配置脚本项目 [imzyf/dotfiles](https://github.com/imzyf/dotfiles)。\n\n<!-- more -->\n\n## 软件下载\n\n使用 `brew cask` 安装软件,非常方便。\n\n安装 [brew](https://brew.sh/):\n\n```bash\n/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n\n# 查找软件\nbrew desc iterm2\nbrew search iterm2\nbrew info iterm2\n# 安装软件\nbrew install --cask iterm2\n```\n\n## 软件清单\n\n### brew cask\n\n- [arc](https://formulae.brew.sh/cask/arc#default) Chromium based browser\n- chromium - Free and open-source web browser 🌐\n- dbeaver-community - Universal database tool and SQL client 🌐\n- [docker](https://formulae.brew.sh/cask/docker#default) - App to build and share containerised applications and microservices 🌐 office network can't access `https://desktop.docker.com/`, have to [manual install](https://www.docker.com/products/docker-desktop).\n- drawio - Online diagram software 🌐\n- figma - Collaborative team software 💅🏼\n- firefox\n- [fork](https://formulae.brew.sh/cask/fork#default) GIT client\n- github - Desktop client for GitHub repositories 🌐\n- google-chrome - Web browser 💻\n- [iina](https://formulae.brew.sh/cask/iina#default) - Free and open-source media player 💻\n- [imageoptim](https://formulae.brew.sh/cask/imageoptim#default) - Tool to optimise images to a smaller size 💻\n- [iterm2](https://formulae.brew.sh/cask/iterm2#default) - Terminal emulator as alternative to Apple's Terminal app 💻\n- [itsycal](https://formulae.brew.sh/cask/itsycal#default) - Menu bar calendar 💻\n- [licecap](https://formulae.brew.sh/cask/licecap#default) - Animated screen capture application 💻\n- [logitech-g-hub](https://formulae.brew.sh/cask/logitech-g-hub#default) - Support for Logitech G gear 💻\n- mysqlworkbench\n- [notion](https://formulae.brew.sh/cask/notion#default) - App to write, plan, collaborate, and get organised 💻\n- openinterminal\n- [orbstack](https://formulae.brew.sh/cask/orbstack#default) Replacement for Docker Desktop\n- phpstorm\n- [postman](https://formulae.brew.sh/cask/postman#default) - Collaboration platform for API development 💻\n- [qlstephen](https://formulae.brew.sh/cask/qlstephen#default) - Quick Look plugin for plaintext files without an extension 💻\n- [raycast](https://formulae.brew.sh/cask/raycast#default) Control your tools with a few keystrokes\n- sequel-ace\n- slack\n- [snipaste](https://formulae.brew.sh/cask/snipaste#default) - Snip or pin screenshots 💻\n- [sourcetree](https://www.sourcetreeapp.com/) - Graphical client for Git version control 💻\n- the-unarchiver - Unpacks archive files\n- [visual-studio-code](https://formulae.brew.sh/cask/visual-studio-code#default)\n- [warp](https://formulae.brew.sh/cask/warp#default) - Rust-based terminal 💻\n- wetype - Text input app from WeChat team for Chinese users 💻\n- youdaodict - Youdao Dictionary 💻\n- zoom-for-it-admins\n\n### brew cask archive\n\n- [adrive](https://formulae.brew.sh/cask/adrive#default) - Intelligent cloud storage platform 💻\n- another-redis-desktop-manager - Redis desktop manager 🌐\n- [baidunetdisk](https://formulae.brew.sh/cask/baidunetdisk#default) - Cloud storage service 💻\n- blender - 3D creation suite 💅🏼\n- [lens](https://formulae.brew.sh/cask/lens#default) - Kubernetes IDE 🌐\n- [mongodb-compass](https://formulae.brew.sh/cask/mongodb-compass#default) - Interactive tool for analyzing MongoDB data 🌐\n- fliqlo - Flip clock screensaver - 可以配合 `触发角` 使用\n- paper - Pap.er, 4K 5K HD Wallpaper Application 💻\n- wireshark - Network analyzer and capture tool - without graphical user interface 🌐\n- [path-finder](https://formulae.brew.sh/cask/path-finder#default) - 💰\n- dash - API documentation browser and code snippet manager - 💰\n- Medis 2 - 💰\n- [redisinsight](https://formulae.brew.sh/cask/redisinsight#default) GUI for streamlined Redis application development\n- [craft](https://formulae.brew.sh/cask/craft#default) Native document editor\n- [input-source-pro](https://formulae.brew.sh/cask/input-source-pro#default) Tool for multi-language users\n- [cleanshot](https://formulae.brew.sh/cask/cleanshot#default) Screen capturing tool\n- cleanmymac-zh - Tool to remove unnecessary files and folders from disk - 💰\n\n```bash\n# brew list\n# brew list --cask\n#\n# ==> Casks\n#\n# arc\n# chromium\n# dbeaver-community\n# drawio\n# figma\n# firefox\n# fork\n# github\n# google-chrome\n# iina\n# imageoptim\n# iterm2\n# itsycal\n# licecap\n# logitech-g-hub\n# mysqlworkbench\n# notion\n# openinterminal\n# orbstack\n# path-finder\n# phpstorm\n# postman\n# qlstephen\n# raycast\n# sequel-ace\n# slack\n# snipaste\n# sourcetree\n# the-unarchiver\n# visual-studio-code\n# warp\n# wetype\n# youdaodict\n# zoom-for-it-admins\n#\n# 💻\nbrew install --cask adrive alfred google-chrome visual-studio-code\nbrew install --cask visual-studio-code wetype youdaodict\n# 🌐\nbrew install --cask itsycal\n# 💅🏼\nbrew install --cask blender\n```\n\n### brew\n\n- [mas](https://formulae.brew.sh/formula/mas#default) Mac App Store command-line interface\n- nvm\n- curl\n- [jmeter](https://formulae.brew.sh/formula/jmeter#default) - Load testing and performance measurement application\n- wget\n- yarn\n- [protobuf](https://formulae.brew.sh/formula/protobuf#default) - Protocol buffers (Google's data interchange format)\n- ~~fish~~\n\n## Manual\n\n- [JetBrains Developer Tools](https://www.jetbrains.com/) 全家桶 - 💰\n- charles - Web debugging Proxy application - 💰\n- Navicat Premium - 💰\n- [oh-my-zsh](https://ohmyz.sh/)\n- [ClashX Pro](https://install.appcenter.ms/users/clashx/apps/clashx-pro/distribution_groups/public)\n- axure-rp - Planning and prototyping tool for developers - 💰\n- [Badgeify](https://badgeify.app/) Add Any App to Your Mac Menu Bar\n- [TotalFinder](https://totalfinder.binaryage.com/) for Mac users who demand more from their Finder\n- ~~[one-key-hidpi](https://github.com/xzhih/one-key-hidpi) 3182x1332~~\n\n### App Store \n\n- Xnip - Handy Screenshot App for Mac\n- iHosts - /etc/hosts 编辑器\n- [Xiaomi Home](https://apps.apple.com/cn/app/xiaomi-home/id957323480?l=en-GB)\n\n```bash\n# brew install mas\n\n# mas search Xnip\n# mas upgrade\n\n# Xnip\nmas install 1221250572\n# iHosts\nmas install 408981434\n# WeChat\nmas install 836500024\n# QQ音乐\nmas install 595615424\n# Xcode\nmas install 497799835\n# Xiaomi Home ?\nmas install 957323480\n```\n\n## AI\n\n- LobeChat\n- Ollama\n\n## 快捷键\n\n- [Mac 键盘快捷键](https://support.apple.com/zh-cn/102650)\n- [Chrome 键盘快捷键](https://support.google.com/chrome/answer/157179?hl=zh-Hans)\n\n## 个性化设置\n\n- 触发角:`系统偏好设置 -> 桌面与屏幕保护程序 -> 屏幕保护程序 -> 触发角`\n\n## Refereneces\n\n~~- 设置安装源允许任何 `sudo spctl --master-disable`~~\n\n-- EOF --\n","tags":["mac"]},{"title":"Laravel 中 composer 加载流程","url":"/2019/04/28/composer-autoload-in-laravel/","content":"\n## 启动\n\n- Laravel 5.8\n\n文章以 Laravel 学习。入口文件 `public/index.php`:\n\n```php\n// Register The Auto Loader\nrequire __DIR__.'/../vendor/autoload.php';\n```\n\n`autoload.php` 不负责具体功能逻辑,只做了两件事:初始化自动加载类、注册自动加载类。\n\n`autoload_real.php` 中的类名为 `ComposerAutoloaderInit...` 这可能是为防止与用户自定义类名跟这个类重复冲突,加上了哈希值。\n\n其实还有一个做法我们更加熟悉,是定义一个命名空间。这里为什么不定义一个命名空间呢?一种理解:命名空间一般都是为了复用,而这个类只需要运行一次即可,以后也不会用得到,用哈希值更加合适。\n\n<!-- more -->\n\n## autoload_real.php\n\n`autoload.php` 主要调用了 `getLoader()`:\n\n```php\npublic static function getLoader()\n{\n // 单例模式,自动加载类只能有一个 1\n if (null !== self::$loader) {\n return self::$loader;\n }\n\n // 获得自动加载核心类对象 2\n spl_autoload_register(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'), true, true);\n self::$loader = $loader = new \\Composer\\Autoload\\ClassLoader();\n spl_autoload_unregister(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'));\n\n // 初始化自动加载核心类对象 3\n $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());\n if ($useStaticLoader) {\n require_once __DIR__ . '/autoload_static.php';\n\n call_user_func(\\Composer\\Autoload\\ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::getInitializer($loader));\n } else {\n $map = require __DIR__ . '/autoload_namespaces.php';\n foreach ($map as $namespace => $path) {\n $loader->set($namespace, $path);\n }\n\n $map = require __DIR__ . '/autoload_psr4.php';\n foreach ($map as $namespace => $path) {\n $loader->setPsr4($namespace, $path);\n }\n\n $classMap = require __DIR__ . '/autoload_classmap.php';\n if ($classMap) {\n $loader->addClassMap($classMap);\n }\n }\n\n // 注册自动加载核心类对象 4\n $loader->register(true);\n\n // 自动加载全局函数 5\n if ($useStaticLoader) {\n $includeFiles = Composer\\Autoload\\ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$files;\n } else {\n $includeFiles = require __DIR__ . '/autoload_files.php';\n }\n foreach ($includeFiles as $fileIdentifier => $file) {\n composerRequire76e88f0b305cd64c7c84b90b278c31db($fileIdentifier, $file);\n }\n\n return $loader;\n}\n```\n\n## 单例模式 1\n\n```php\nif (null !== self::$loader) {\n return self::$loader;\n}\n```\n\n## 构造 ClassLoader 核心类 2\n\n```php\nspl_autoload_register(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'), true, true);\nself::$loader = $loader = new \\Composer\\Autoload\\ClassLoader();\nspl_autoload_unregister(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'));\n```\n\n```php\npublic static function loadClassLoader($class)\n{\n if ('Composer\\Autoload\\ClassLoader' === $class) {\n require __DIR__ . '/ClassLoader.php';\n }\n}\n```\n\n`composer` 先向 `PHP` 自动加载机制注册了一个函数,这个函数 `require` 了 `ClassLoader` 文件。成功 `new` 出该文件中核心类 `ClassLoader()` 后,又销毁了该函数。\n\n为什么不直接 `require`?原因是:怕有的用户也定义了个 `\\Composer\\Autoload\\ClassLoader` 命名空间,导致自动加载错误文件。\n\n那为什么不跟引导类一样用个哈希值呢?原因是:这个类是可以复用的,框架允许用户使用这个类。\n\n## 初始化核心类对象 3\n\n对自动加载类的初始化,主要是给自动加载核心类初始化顶级命名空间映射。初始化的方法有两种:\n\n1. 使用 `autoload_static` 进行静态初始化\n2. 调用核心类接口初始化\n\n### autoload_static 静态初始化\n\n静态初始化只支持 `PHP 5.6` 以上版本、不支持 `HHVM` 虚拟机、不存在 `Zend-encoded file`。\n\n`autoload_static.php`\n\n```php\n<?php\n\n// autoload_static.php @generated by Composer\n\nnamespace Composer\\Autoload;\n\n// hash 防止冲突\nclass ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db\n{\n public static $files = array (...);\n public static $prefixLengthsPsr4 = array (...);\n public static $prefixDirsPsr4 = array (...);\n public static $fallbackDirsPsr4 = array (...);\n public static $prefixesPsr0 = array (...);\n public static $classMap = array array (...);\n\n public static function getInitializer(ClassLoader $loader)\n {\n return \\Closure::bind(function () use ($loader) {\n $loader->prefixLengthsPsr4 = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$prefixLengthsPsr4;\n $loader->prefixDirsPsr4 = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$prefixDirsPsr4;\n $loader->fallbackDirsPsr4 = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$fallbackDirsPsr4;\n $loader->prefixesPsr0 = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$prefixesPsr0;\n $loader->classMap = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$classMap;\n\n }, null, ClassLoader::class);\n }\n}\n```\n\n这个静态初始化类的核心就是 `getInitializer()` 函数,它将自己类中的顶级命名空间映射给了 ClassLoader 类。\n\n值得注意的是这个函数返回的是一个匿名函数,为什么呢?原因就是 `ClassLoader` 中的 `prefixLengthsPsr4` 、`prefixDirsPsr4` 等等方法都是 `private` 的。普通的函数没办法给类的 `private` 成员变量赋值。利用匿名函数的绑定功能就可以将把匿名函数转为 `ClassLoader` 类的成员函数。\n\n关于匿名函数的 [绑定功能](http://www.cnblogs.com/yjf512/p/4421289.html)。\n\n接下来就是 顶级命名空间 初始化的关键了。\n\n#### classMap\n\n```php\npublic static $classMap = array (\n 'App\\\\Api\\\\Middleware\\\\DeviceRecord' => __DIR__ . '/../..' . '/app/Api/Middleware/DeviceRecord.php',\n 'App\\\\Api\\\\Middleware\\\\HeaderCheck' => __DIR__ . '/../..' . '/app/Api/Middleware/HeaderCheck.php',\n ...\n)\n```\n\n直接命名空间全名与目录的映射,没有顶级命名空间。简单粗暴,也导致这个数组相当的大。\n\n#### PSR0 顶级命名空间映射\n\n```php\npublic static $prefixesPsr0 = array (\n 'P' =>\n array (\n 'Prophecy\\\\' =>\n array (\n 0 => __DIR__ . '/..' . '/phpspec/prophecy/src',\n ),\n 'Parsedown' =>\n array (\n 0 => __DIR__ . '/..' . '/erusev/parsedown',\n ),\n ),\n ...\n);\n```\n\n为了快速找到顶级命名空间,这里使用命名空间第一个字母作为前缀索引。这个映射的用法比较明显,假如我们有 `Parsedown/example` 这样的命名空间,首先通过首字母 `P`,找到:\n\n```php\n'P' => array (...)\n```\n\n这个数组,然后就会遍历这个数组来和 `Parsedown/example` 比较,发现第一个 `Prophecy` 不符合,第二个 `Parsedown` 符合,然后得到了映射目录(映射目录可能不止一个):\n\n```php\n0 => __DIR__ . '/..' . '/erusev/parsedown',\n```\n\n接着遍历这个数组,尝试 `__DIR__ . '/..' . '/erusev/parsedown/Parsedown/example.php'` 是否存在,如果不存在接着遍历数组(这个例子数组只有一个元素),如果数组遍历完都没有,就会加载失败。\n\n#### PSR4 标准顶级命名空间映射\n\n```php\npublic static $prefixLengthsPsr4 = array (\n 'p' =>\n array (\n 'phpDocumentor\\\\Reflection\\\\' => 25,\n ),\n 'Z' =>\n array (\n 'Zend\\\\Diactoros\\\\' => 15,\n ),\n ...\n);\n\npublic static $prefixDirsPsr4 = array (\n 'phpDocumentor\\\\Reflection\\\\' =>\n array (\n 0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src',\n 1 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src',\n 2 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src',\n ),\n 'Zend\\\\Diactoros\\\\' =>\n array (\n 0 => __DIR__ . '/..' . '/zendframework/zend-diactoros/src',\n ),\n ...\n);\n```\n\n`PSR4` 标准 `顶级命名空间` 映射用了两个数组,第一个和 `PSR0` 一样用命名空间第一个字母作为前缀索引,然后是 `顶级命名空间`,但是最终并不是文件路径,而是 `顶级命名空间` 的长度。为什么呢?因为 `PSR4` 的文件目录更加灵活,更加简洁。\n\n`PSR0` 中 `顶级命名空间` 目录 **直接加** 到命名空间前面就可以得到路径:\n\n```\n ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓\nParsedown/example => __DIR__ . '/..' . '/erusev/parsedown/Parsedown/example.php\n ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑\n```\n\n而 `PSR4` 却是用顶级命名空间目录 **替换** 顶级命名空间,所以获得顶级命名空间的 **长度** 很重要:\n\n```\n ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓\nParsedown/example => __DIR__ . '/..' . '/erusev/parsedown/example.php\n ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑\n```\n\n举例:假如我们找 `Symfony\\\\Polyfill\\\\Mbstring\\\\example` 这个类,和 `PSR0` 一样通过前缀索引和字符串匹配我们得到了:\n\n```\n'Symfony\\\\Polyfill\\\\Mbstring\\\\' => 26,\n```\n\n这条记录,键是顶级命名空间,值是命名空间的长度。拿到顶级命名空间后去 `$prefixDirsPsr4` 获取它的映射目录数组(注意映射目录可能不止一条):\n\n```php\n'Symfony\\\\Polyfill\\\\Mbstring\\\\' =>\narray (\n 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',\n),\n```\n\n将 `Symfony\\\\Polyfill\\\\Mbstring\\\\example` 前 26 个字母替换为 `__DIR__ . '/..' . '/symfony/polyfill-mbstring` 也就是:\n\n```\n__DIR__ . '/..' . '/symfony/polyfill-mbstring/example.php\n```\n\n先验证磁盘上这个文件是否存在,如果不存在接着遍历。如果遍历后没有找到,则加载失败。\n\n自动加载核心类 `ClassLoader` 的静态初始化完成!\n\n> 其实还有 `$fallbackDirsPsr4`,暂未研究\n\n### 调用接口初始化\n\n如果 `PHP` 版本低于 `5.6` 或者使用 `HHVM` 虚拟机环境或者存在 `zend_loader_file_encoded`,那么就要使用核心类的接口进行初始化。\n\n```php\n\n/*\nPSR0 取出命名空间的第一个字母作为索引,一个索引对应多个顶级命名空间,一个顶级命名空间对应多个目录路径,具体形式可以查看上面的 autoload_static 的 $prefixesPsr0。\n\n如果没有顶级命名空间,就只存储一个路径名,以便在后面尝试加载。\n*/\n$map = require __DIR__ . '/autoload_namespaces.php';\nforeach ($map as $namespace => $path) {\n $loader->set($namespace, $path);\n}\n\n/*\nPSR4 如果没有顶级命名空间,就直接保存目录。\n如果有命名空间的话,要保证顶级命名空间最后是 \\,然后分别保存\n( 前缀 -> 顶级命名空间,顶级命名空间 -> 顶级命名空间长度 )\n( 顶级命名空间 -> 目录 )\n\n这两个映射数组。具体形式可以查看上面我们讲的 autoload_static 的 prefixLengthsPsr4、$prefixDirsPsr4 。\n*/\n$map = require __DIR__ . '/autoload_psr4.php';\nforeach ($map as $namespace => $path) {\n $loader->setPsr4($namespace, $path);\n}\n\n/*\n整个命名空间与目录之间的映射\n*/\n$classMap = require __DIR__ . '/autoload_classmap.php';\nif ($classMap) {\n $loader->addClassMap($classMap);\n}\n```\n\n## 注册核心类对象 4\n\nComposer 自动加载功能的启动与初始化,经过启动与初始化,自动加载核心类对象已经获得了顶级命名空间与相应目录的映射,换句话说,如果有命名空间 `App\\Console\\Kernel`,我们已经知道了 `App\\` 对应的目录,接下来我们就要解决下面的就是 `\\Console\\Kernel` 这一段。\n\n```php\n/**\n * Registers this instance as an autoloader.\n *\n * @param bool $prepend Whether to prepend the autoloader or not\n*/\npublic function register($prepend = false)\n{\n spl_autoload_register(array($this, 'loadClass'), true, $prepend);\n}\n```\n\n一行代码实现自动加载。核心在 `ClassLoader` 的 `loadClass()` 函数上,这个函数负责按照 `PSR` 标准将顶层命名空间以下的内容转为对应的目录,也就是上面所说的将 `App\\Console\\Kernel` 中 `Console\\Kernel` 这一段转为目录。\n\n## 自动加载全局函数 5\n\n`Composer` 不止可以自动加载命名空间,还可以加载全局函数。就是把全局函数写到特定的文件里面去,在程序运行前挨个 `require` 就行了。\n\n```php\nif ($useStaticLoader) {\n // 静态初始化\n $includeFiles = Composer\\Autoload\\ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$files;\n} else {\n // 普通初始化\n $includeFiles = require __DIR__ . '/autoload_files.php';\n}\nforeach ($includeFiles as $fileIdentifier => $file) {\n composerRequire76e88f0b305cd64c7c84b90b278c31db($fileIdentifier, $file);\n}\n```\n\n```php\nfunction composerRequire76e88f0b305cd64c7c84b90b278c31db($fileIdentifier, $file)\n{\n if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {\n require $file;\n\n $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;\n }\n}\n```\n\n### 问题 1\n\n为什么不直接 `require` `$includeFiles` 里面的每个文件名,而要用类外面的函数 `composerRequire...` ?\n\n- 避免和用户定义函数冲突\n- 防止有人在全局函数所在的文件写 `$this` 或者 `self`\n\n假如 `$includeFiles` 有个 `app/helper.php` 文件,这个 `helper.php` 文件的函数外有一行代码: `$this->foo()`,如果引导类在 `getLoader()` 函数直接 `require($file)`,那么引导类就会运行这句代码,调用自己的 `foo()` 函数,这显然是错的。\n\n事实上 `helper.php` 就不应该出现 `$this` 或 `self` 这样的代码,这样写一般都是用户写错了的,一旦这样的事情发生:\n\n- 第一种情况:引导类恰好有 `foo()` 函数,那么就会莫名其妙执行了引导类的 `foo()`。\n- 第二种情况:引导类没有 `foo()` 函数,但是却甩出来引导类没有 `foo()` 方法这样的错误提示,用户不知道自己哪里错了。把 `require` 语句放到 **引导类的外面**,遇到 `$this` 或者 `self` ,程序就会告诉用户根本没有类, `$this` 或 `self` 无效,错误信息更加明朗。\n\n### 问题 2\n\n为什么要用 `hash` 作为 `$fileIdentifier`?\n\n这个变量是用来控制全局函数只被 `require` 一次的,那为什么不用 `require_once` 呢?事实上 `require_once` 比 `require` 效率低很多,使用全局变量 `$GLOBALS` 这样控制加载会更快。猜测另一个原因应该是 `require_once` 对相对路径的支持并不理想,所以 `composer` 尽量少用 `require_once`。\n\n## 运行\n\n`ClassLoader` 将 `loadClass()` 函数注册到 `PHP SPL` 中的 `spl_autoload_register()` 里面去。这样,每当 PHP 遇到一个不认识的命名空间的时候,PHP 会自动调用注册到 `spl_autoload_register()` 里面的函数堆栈,运行其中的每个函数,直到找到命名空间对应的文件。\n\n```php\n/**\n * Loads the given class or interface.\n *\n * @param string $class The name of the class\n * @return bool|null True if loaded, null otherwise\n */\npublic function loadClass($class)\n{\n if ($file = $this->findFile($class)) {\n includeFile($file); // include $file; Prevents access to $this/self from included files.\n\n return true;\n }\n}\n\n/**\n * Finds the path to the file where the class is defined.\n *\n * @param string $class The name of the class\n *\n * @return string|false The path if found, false otherwise\n */\npublic function findFile($class)\n{\n // class map lookup\n if (isset($this->classMap[$class])) {\n return $this->classMap[$class];\n }\n\n // classMapAuthoritative 关闭搜索未在类映射中注册的类的 prefix and fallback directories。- 不清楚干啥的 暂没研究\n if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {\n return false;\n }\n\n // 如果启用扩展名,则使用 APCu 前缀来缓存已找到/未找到的类。 - 不清楚干啥的 暂没研究\n if (null !== $this->apcuPrefix) {\n $file = apcu_fetch($this->apcuPrefix.$class, $hit);\n if ($hit) {\n return $file;\n }\n }\n\n $file = $this->findFileWithExtension($class, '.php');\n\n // Search for Hack files if we are running on HHVM\n if (false === $file && defined('HHVM_VERSION')) {\n $file = $this->findFileWithExtension($class, '.hh');\n }\n\n if (null !== $this->apcuPrefix) {\n apcu_add($this->apcuPrefix.$class, $file);\n }\n\n if (false === $file) {\n // Remember that this class does not exist.\n $this->missingClasses[$class] = true;\n }\n\n return $file;\n}\n```\n\n`loadClass()` 主要调用 `findFile()` 函数。`findFile()` 在解析命名空间的时候主要分为两部分:\n\n- `classMap` 直接看命名空间是否在映射数组\n- `findFileWithExtension()` 包含了 `PSR0`、`PSR4`\n\n如果我们在代码中写 `'phpDocumentor\\Reflection\\example`,PHP 会通过 SPL 调用 `loadClass` -> `findFile` -> `findFileWithExtension`。\n\n- 首先默认用 `.php` 后缀名调用 `findFileWithExtension` 函数里,利用 `PSR4` 标准尝试解析目录文件,如果文件不存在则继续用 `PSR0` 标准解析\n- 如果解析出来的目录文件仍然不存在,但是环境是 `HHVM` 虚拟机,继续用后缀名 `.hh` 再次调用 `findFileWithExtension` 函数,如果不存在,说明此命名空间无法加载,放到 `classMap` 中设为 `false`,以便以后更快地加载\n\n### PSR4\n\n对于 `phpDocumentor\\Reflection\\example`,当尝试利用 `PSR4` 标准映射目录时,步骤如下:\n\n```php\n// $class: phpDocumentor\\Reflection\\example\n\n// PSR-4 lookup\n$logicalPathPsr4 = strtr($class, '\\\\', DIRECTORY_SEPARATOR) . $ext;\n// $logicalPathPsr4: phpDocumentor/Reflection/example.php(hh)`\n\n$first = $class[0];\n// $first: p\n\nif (isset($this->prefixLengthsPsr4[$first])) {\n /* 'p' =>\n array (\n 'phpDocumentor\\\\Reflection\\\\' => 25,\n ),\n */\n $subPath = $class;\n // $subPath: phpDocumentor\\Reflection\\example\n while (false !== $lastPos = strrpos($subPath, '\\\\')) {\n // $lastPos 13\n $subPath = substr($subPath, 0, $lastPos);\n $search = $subPath.'\\\\';\n if (isset($this->prefixDirsPsr4[$search])) {\n // search phpDocumentor\\\\Reflection\\\\\n // $lastPos 25\n\n /* 'phpDocumentor\\\\Reflection\\\\' =>\n array (\n 0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src',\n 1 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src',\n 2 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src',\n ),\n */\n $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);\n // $pathEnd /example.php(hh)\n\n foreach ($this->prefixDirsPsr4[$search] as $dir) {\n // 遍历 3 个\n if (file_exists($file = $dir . $pathEnd)) {\n // $file __DIR__ . '/..' . /phpdocumentor/type-resolver/src/example.php(hh)`\n return $file;\n }\n }\n }\n }\n}\n```\n\n### PSR0\n\n如果 `PSR4` 标准加载失败,则要进行 `PSR0` 标准加载。对于 `phpDocumentor\\Reflection\\example`,当尝试利用 `PSR0` 标准映射目录时,步骤如下:\n\n```php\n// $class: phpDocumentor\\Reflection\\example\n\n// PSR-0 lookup\nif (false !== $pos = strrpos($class, '\\\\')) {\n // namespaced class name\n $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)\n . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);\n} else {\n // PEAR-like class name\n $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;\n}\n\n// $logicalPathPsr0: phpDocumentor/Reflection/example.php(hh)`\nif (isset($this->prefixesPsr0[$first])) {\n foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {\n /* 'P' =>\n array (\n 'Prophecy\\\\' =>\n array (\n 0 => __DIR__ . '/..' . '/phpspec/prophecy/src',\n ),\n 'Parsedown' =>\n array (\n 0 => __DIR__ . '/..' . '/erusev/parsedown',\n ),\n ), */\n if (0 === strpos($class, $prefix)) {\n foreach ($dirs as $dir) {\n if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {\n // $file __DIR__ . '/..' . '/phpspec/prophecy/src' . phpDocumentor/Reflection/example.php(hh)\n return $file;\n }\n }\n }\n }\n}\n```\n\n## Q&A\n\n个人一些疑问:\n\n### 防止用户自定义与 ClassLoader 命名空间冲突\n\n```php\nspl_autoload_register(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'), true, true);\nself::$loader = $loader = new \\Composer\\Autoload\\ClassLoader();\nspl_autoload_unregister(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'));\n```\n\n为什么这样可以解决:与用户也定义了个 `\\Composer\\Autoload\\ClassLoader` 命名空间,导致自动加载错误文件。\n\n与第四个参数 `$prepend` `true` 有关吗?\n\n### composer StaticLoader 有什么优势\n\n`composer` 在加载类和加载全局方法时,都有两种方式。\n\n```\n$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());\n```\n\n以 `$useStaticLoader` 的值进行选择,为什么一定分两种,静态方法是有什么优势吗?\n\n## References\n\n- [PHP Composer - 初始化源码分析](https://github.com/LeoYang90/laravel-source-analysis/blob/master/PHP%20Composer%E2%80%94%E2%80%94%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md)\n\n-- EOF --\n","tags":["php","composer"]},{"title":"PHP 请小心判断 strpos","url":"/2019/04/10/php-strpos-warning/","content":"\n有开始写世界上最后的语言 PHP 了(狗头保命)。一个很简单的字符串是否包含判断就掉坑了。\n\n方法签名:\n\n```php\nstrpos ( string $haystack , mixed $needle [, int $offset = 0 ] ) : int\n```\n\n<!-- more -->\n\n```php\n$mystring = 'abc';\n$findme = 'a';\nif (strpos($mystring, $findme)) {\n dump('yes');\n}\n```\n\n注意这时是不会输出 `yes`,因为 `strpos($mystring, $findme)` 返回的是 `0`。就想官方文档说的:\n\n> Warning 此函数可能返回布尔值 FALSE,但也可能返回等同于 FALSE 的非布尔值。应使用 === 运算符来测试此函数的返回值。\n\n正解:\n\n```php\nif (strpos($mystring, $findme) !== false) {\n dump('yes');\n}\n```\n\n这次问题是网上一搜,找到 `strpos` 后看到 `如果没找到 needle,将返回 FALSE` 就没多想就用了。语言间的差异还有注意。\n\n## References\n\n- [php.net - strpos](https://www.php.net/manual/zh/function.strpos.php)\n\n-- EOF --\n","tags":["php"]},{"title":"ES6 中使用 jQuery $(this) 的问题","url":"/2019/04/10/es6-jquery-this-arrow-function/","content":"\n在老项目中开始改用 `laravel-mix` `ES6` 逐渐过渡。摸索中遇到在与 `jQuery` 一同使用时 `箭头函数` 中 `$(this)` 的含义发生了变化。\n\n遇到这个问题主要是没有搞清楚 [箭头函数](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions):\n\n```js\n$('.js-bottom-btn').click(() => {\n let flag = $(this).date('flag');\n ...\n});\n```\n\n转换为了:\n\n```js\n$('.js-bottom-btn').click(function() {\n let flag = $(_this).date('flag');\n ...\n});\n```\n\n`_this` is undefined\n\n根据 [jQuery click 文档](https://api.jquery.com/click/) 可以修改为:\n\n```js\n$('.js-bottom-btn').click(event => {\n let flag = $(event.currentTarget).date('flag');\n ...\n});\n```\n\n类似的问题:\n\n```js\n$(\"jquery-selector\").each(() => {\n $(this).click();\n});\n```\n\n需要改为:\n\n```js\n$(\"jquery-selector\").each((index, element) => {\n $(element).click();\n});\n```\n\n## References\n\n- [Using jQuery \\$(this) with ES6 Arrow Functions (lexical this binding)](https://stackoverflow.com/questions/27670401/using-jquery-this-with-es6-arrow-functions-lexical-this-binding)\n\n-- EOF --\n","tags":["javascript"]},{"title":"寻找数组中轴索引","url":"/2019/03/06/find-pivot-index/","content":"\n将 pivot 索引定义为:左边的数字之和等于索引右边的数字之和。\n\n```swift\nInput:\nnums = [1, 7, 3, 6, 5, 6]\nOutput: 3\nExplanation:\n1 + 7 + 3 = 5 + 6\n\nInput:\nnums = [1, 2, 3]\nOutput: -1\nExplanation:\nThere is no index that satisfies the conditions in the problem statement.\n```\n\n<!-- more -->\n\nNote:\n\n- The length of `nums` will be in the range `[0, 10000]`.\n- Each element `nums[i]` will be an integer in the range `[-1000, 1000]`.\n\n## 关键点\n\n- 动态规划\n- 数组的和 - 中轴数 = 中轴数左边数组的和 \\* 2\n\n## 解答\n\n```swift\nfunc findPivot(_ array: [Int]) -> Int {\n // 数组和\n let sum = array.reduce(0, +)\n // 左侧数组和\n var leftSum = 0\n for (key, value) in array.enumerated() {\n if sum - value == leftSum * 2 {\n return key\n }\n leftSum += value\n }\n return -1\n}\n\nlet array = [1, 7, 3, 6, 5, 6]\nsearch(array) // 3\n```\n\n## References\n\n- [找到数组中左右两边的和相等的 pivot 的下标 Find Pivot Index](https://my.oschina.net/liyurong/blog/1608204)\n\n-- EOF --\n","tags":["algorithm","swift"]},{"title":"m 进制转 n 进制","url":"/2019/03/02/convert-m-number-to-n-number/","content":"\n## 思路\n\n- m 进制 -> 十进制 -> n 进制\n- 利用柯里化生成函数(炫技 🐶)\n\n<!-- more -->\n\n## m 进制 -> 十进制\n\n```\n// carry 范围值: 2-36\n// origin 范围值: 0-9 [ascii 48-58], A-Z [65-90], a-z [97-122]\nfunc carryToDecimalism(_ carry: Int) -> (_ origin: String) -> Int {\n return { origin in\n // 得到字符串对应的 ascii 码\n let asciis = origin.uppercased().unicodeScalars.map { Int($0.value) }\n // 累加每一位\n let result = asciis.reversed().enumerated().map { (index, ascii) -> Int in\n var standard: Int\n if 65 <= ascii && ascii <= 90 {\n standard = ascii - 65 + 10\n } else {\n standard = ascii - 48\n }\n return standard * Int(pow(Double(carry), Double(index)))\n }.reduce(0, +)\n return result\n }\n}\n\nlet 十六进制转十进制 = carryToDecimalism(16)\nprint(十六进制转十进制(\"1a\")) // 26\n\nlet 二进制转十进制 = carryToDecimalism(2)\nprint(二进制转十进制(\"110\")) // 6\n```\n\n## 十进制 -> n 进制\n\n```\nfunc decimalismToCarry(_ carry: Int) -> (_ origin: Int) -> String {\n return { origin in\n var result = [Int]()\n var remain = origin\n while remain > 0 {\n result.append(remain % carry)\n remain /= carry\n }\n if carry <= 10 {\n return result.reversed().map(String.init).joined()\n } else {\n return result.reversed().map { i -> String in\n return i < 10 ? String(i) : String(UnicodeScalar(i + 55)!)\n }.joined()\n }\n }\n}\n\nlet 十进制转二进制 = decimalismToCarry(2)\nprint(十进制转二进制(26)) // \"11010\"\n```\n\n## References\n\n- [ASCII 码对照表](http://ascii.911cha.com/)\n\n-- EOF --\n","tags":["algorithm","swift"]},{"title":"超长阶乘的计算","url":"/2019/03/01/extra-long-factorials/","content":"\n打印 `n!` 的结果(1 <= n <= 100)。注意:当 `n > 20` 时 64 位的 `Int` 将无法直接存储结果。\n\n## 思路\n\n- 将大数字用 **数组** 形式表示。比如 987 使用 [9,8,7] 代替。\n- 每一位乘以 n,再进行进位操作,得到新数组。\n\n```swift\nlet nums = [9, 8, 7]\nlet tmpNums = nums.map { $0 * 2 } // [18, 16, 14]\n\n// 遍历 tmpNums 每一个数字,进行进制操作\n\n[18, 16, 14] -> [18, 17, 4] -> [19, 7, 4] -> [1, 9, 7, 4]\n\nprint(tmpNums.map(String.init).joined()) // 1974\n```\n\n## 解答项目\n\n```swift\nfunc extraLongFactorials(n: Int) -> Void {\n guard n > 0 else {\n return\n }\n // 结果数组\n var result: [Int] = [1]\n for index in 1...n {\n // 数组翻转 从低位开始每一位乘以本次的数字\n let tmpNums = result.reversed().map { $0 * index }\n // 进位数\n var carryNum = 0\n // 重置结果\n result = []\n tmpNums.forEach {\n // 每一位加上上一位的进的数\n let tmpNum = $0 + carryNum\n // 向下一位进制的数\n carryNum = tmpNum / 10\n // 本位实际剩下的数 插入结果\n result.append(tmpNum % 10)\n }\n // 处理剩余进位数 进位数是可能大于 100\n while carryNum > 0 {\n // 逐渐插入进制\n result.append(carryNum % 10)\n carryNum /= 10\n }\n // 翻转回数组\n result = result.reversed()\n }\n // 连接字符串\n print(result.map(String.init).joined())\n}\n```\n\n## References\n\n- [Extra Long Factorials | HackerRank](https://www.hackerrank.com/challenges/extra-long-factorials/problem)\n- [Swift 3 calculate factorial number. Result becomes too high?](https://stackoverflow.com/questions/43830151/swift-3-calculate-factorial-number-result-becomes-too-high)\n\n-- EOF --\n","tags":["algorithm","swift"]},{"title":"fastlane 入门使用","url":"/2019/02/28/fastlane-getting-started/","content":"\n<img src=\"https://images.unsplash.com/37/QAdTsSj8TOOWzlyLn3Rg_14248396556_aefcd9a926_o.jpg?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=960&q=80\" />\n\n这次以 [fastlane](https://fastlane.tools/) 为例,尝试项目中有什么事情可以被自动完成。\n\nfastlane 是 Ruby scripts 的集合,安装方法不多说了见 [官网文档](https://docs.fastlane.tools/)。\n\nfastlane 中有但不限于以下工具集:\n\n- [produce](https://docs.fastlane.tools/actions/produce/) 同时在 Apple Developer Portal 和 App Store Connect 中创建新的 iOS apps。\n- [cert](https://docs.fastlane.tools/actions/cert/) 自动创建和维护 iOS 签名证书。\n- [sigh](https://docs.fastlane.tools/actions/sigh/) 创建,更新,下载和修复配置文件。\n- [snapshot](https://docs.fastlane.tools/actions/snapshot/) 自动在每台设备上获取 iOS 应用的本地化屏幕截图。\n- [frameit](https://docs.fastlane.tools/actions/frameit/) 将您的屏幕截图放入正确的设备框架中。\n- [gym](https://docs.fastlane.tools/actions/gym/) 构建和打包您的 iOS apps。\n- [deliver](https://docs.fastlane.tools/actions/deliver/) 将截图,元数据和您的应用上传到 App Store。\n- [pem](https://docs.fastlane.tools/actions/pem/) 自动生成并更新推送通知配置文件。\n- [spaceship](https://github.com/fastlane/fastlane/tree/master/spaceship) 一个 Ruby 库能够访问苹果开发者中心和应用商店连接 api。\n- [pilot](https://docs.fastlane.tools/actions/pilot/) 自动化 TestFlight 部署并管理测试用户。\n- [boarding](https://github.com/fastlane/boarding) 邀请 beta 测试人员。\n- [match](https://docs.fastlane.tools/actions/match/) 使用 Git 同步整个团队的证书和配置文件。\n- [scan](https://docs.fastlane.tools/actions/scan/) 运行 app 测试。\n\n<!-- more -->\n\n> 实验环境:Xcode 10.1、Swift 4.2、fastlane 2.116.1、\\$99 开发者账户\n\n## 项目添加 fastlane\n\n在项目根目录下:\n\n```\nfastlane init\n```\n\n在出现 `What would you like to use fastlane for?` 是选择 `4 Manual setup`,在根据提示回车确认。\n\n之后项目文件中将新增:\n\n- `Gemfile` 这个文件包含了 fastlane gem 的依赖\n- `fastlane` 这个文件夹包含\n - `Appfile` 存储 app identifier,Apple ID 以及 fastlane 设置 app 所需的任何其他标识信息\n - `Fastfile` 管理 lanes 创建的可被调用的 actions\n\n## 创建 App\n\n打开 `Fastfile` 将里面所有内容替换为:\n\n```\ndefault_platform(:ios)\n\nplatform :ios do\n# 1 lane 的描述\n desc \"Create app on Apple Developer and App Store Connect sites\"\n# 2 create_app 是这个 lane 的名字\n lane :create_app do\n# 3 使用 produce action 将 app 添加到 Developer Portal 和 App Store Connect\n produce\n end\nend\n```\n\n打开 `Appfile` 将 `apple_id` 前的 # 移除,将 `[[APPLE_ID]]` 替换为真实 Apple ID,这样 fastlane 将不会反复提示你输入。\n\n如果 App Store Connect 和 Apple Developer Portal 可以将 `apple_id` 替换为:\n\n```\napple_dev_portal_id(\"[[APPLE_DEVELOPER_ACCOUNT_USERNAME]]\")\nitunes_connect_id(\"[[APP_STORE_CONNECT_ACCOUNT_USERNAME]]\")\n```\n\n打开命令行在项目目录输入:\n\n```\nfastlane create_app\n```\n\n根据提示输入密码,之后将提示你输入 bundle ID,以反 host 格式创建 ID,之后是 App name 最长 30 个字符。\n\n完成后登陆 Apple Developer Center 和 App Store Connect,app 在二者已经都被创建了。\n\n再次打开 Appfile 将 `app_identifier` 填写刚才创建的 bundle ID。\n\n## 生成交付文件\n\n```\nbundle exec fastlane deliver\n```\n\n根据提醒暂不使用 Swift 代替 Ruby,因为 `fastlane.swift` 现在为 beta。\n\n完成后 fastlane 文件夹中新增:\n\n- `metadata` 这个文件夹存放了 app 大部分的 metadata 元数据\n- `Deliverfile` 保存这剩余小部分的元数据\n- `screenshots` 将保存 app 截图\n\n<img src=\"https://tva1.sinaimg.cn/large/006tKfTcly1g0lz3u80l5j31eb0e6q8a.jpg\" alt=\"fastlane-getting-started-metadata\"/>\n\n`metadata` 文件夹中文件内容就是提交给 App Store Connect,多语言 app 可以手动创建对应的语言文件夹。\n\n更多 `deliver` 参数见 [这里](https://docs.fastlane.tools/actions/deliver/#more-options)\n\n## 自动截图\n\n多种设备和多语言 app 的截图是繁杂的。\n\n```\nfastlane snapshot init\n```\n\nfastlane 文件夹中新增:`Snapfile`,将里面的所有内容替换为一下:\n\n```\n# 1 - 想截图的设备列表\n\ndevices([\n \"iPhone 8 Plus\",\n \"iPhone SE\"\n])\n\n# 2 - 支持的语言列表\n\nlanguages([\n 'en-US',\n 'fr-FR'\n])\n\n# 3 - 包含 UI Tests 的 scheme 名字\n\nscheme(\"mZone Poker UITests\")\n\n# 4 - 截图存储位置\n\noutput_directory \"./fastlane/screenshots\"\n\n# 5 - 是否清理之前的截图\n\nclear_previous_screenshots(true)\n```\n\n保存关闭,然后命令中执行:\n\n```\nfastlane snapshot init\n```\n\n<img src=\"https://tva1.sinaimg.cn/large/006tKfTcly1g0m3olslyhj30vq0ft44l.jpg\" alt=\"fastlane-getting-started-screenshots\" style=\"width: 600px; display: block; margin: auto;\" />\n\n### 创建 Test Target\n\n在 Xcode 选择 File -> New -> Target 选择 iOS UI Testing Bundle 点击 Next。\n\n<img src=\"https://tva1.sinaimg.cn/large/006tKfTcly1g0m6hxg0dej30m80g0781.jpg\" alt=\"fastlane-getting-started-screenshots\" style=\"width: 600px; display: block; margin: auto;\" />\n\nProduct Name 输入上面 `scheme` 填写的名字 MZone Poker UITests 点击 Finish。\n\n之后将 fastlane 文件夹中的 SnapshotHelper.swift 拖到 mZone Poker UITests 中。\n\n之后将 mZone_Poker_UITests.swift 中的 setUp() 和 tearDown() 移除,替换 testExample():\n\n```\n// 1 设置截屏和启动 app\nlet app = XCUIApplication()\nsetupSnapshot(app)\napp.launch()\n// 2 点击 Chip Count 输入框(在 Storyboard 中的 accessibility identifier 设置 \"chip count\")然后输入 10\nlet chipCountTextField = app.textFields[\"chip count\"]\nchipCountTextField.tap()\nchipCountTextField.typeText(\"10\")\n// 3 点击 Big Blind 输入框 然后输入 100\nlet bigBlindTextField = app.textFields[\"big blind\"]\nbigBlindTextField.tap()\nbigBlindTextField.typeText(\"100\")\n// 4 截图\nsnapshot(\"01UserEntries\")\n// 5 点击 what should i do 再进行截图\napp.buttons[\"what should i do\"].tap()\nsnapshot(\"02Suggestion\")\n```\n\n之后创建 mZone Poker UITests scheme,点击 run stop 右边的按钮选择 `Manage Schemes...`\n\n<img src=\"https://tva1.sinaimg.cn/large/006tKfTcly1g0m88o46ovj30m80ccdit.jpg\" alt=\"fastlane-getting-started-create-test-target\" style=\"width: 600px; display: block; margin: auto;\" />\n\n选择 `Edit Schemes...` 勾选 `Test` 和 `Run`\n\n<img src=\"https://tva1.sinaimg.cn/large/006tKfTcly1g0m8eq3o8uj30m80c0di3.jpg\" alt=\"fastlane-getting-started-create-test-target\" style=\"width: 600px; display: block; margin: auto;\" />\n\n打开 Fastfile 添加:\n\n```\ndesc \"Take screenshots\"\nlane :screenshot do\n snapshot\nend\n```\n\n命令行执行:\n\n```\nbundle exec fastlane screenshot\n```\n\n完成后将自动打开:\n\n<img src=\"https://tva1.sinaimg.cn/large/006tKfTcly1g0m8gy5vl9j30df0dwgpb.jpg\" alt=\"fastlane-getting-started-create-test-target\" style=\"width: 600px; display: block; margin: auto;\" />\n\n## 创建 IPA 文件\n\n首先要确保已经 `target` 设置 `bundle identifier` 和 `signing identity`。\n\n命令行执行:\n\n```\nfastlane gym init\n```\n\n打开 `Gymfile` 替换为以下内容:\n\n```\n# 1 指定 scheme\nscheme(\"mZone Poker\")\n# 2 指定存放 .ipa 文件夹\noutput_directory(\"./fastlane/builds\")\n# 3 从构建中排除 bitcode。Bitcode 允许 Apple 优化你的 app,但是现在排除它以提高构建速度。\ninclude_bitcode(false)\n# 4 从构建中排除符号。包含符号允许 Apple 访问应用程序的调试信息,但是现在排除它以提高构建速度。\ninclude_symbols(false)\n# 5 允许 Xcode 使用自动配置。\nexport_xcargs(\"-allowProvisioningUpdates\")\n```\n\n打开 `Fastfile` 添加:\n\n```\ndesc \"Create ipa\"\nlane :build do\n # 1 允许 Xcode 自动配置\n enable_automatic_code_signing\n # 2 构建号自增(App Store Connect 要求构建号不能重复)\n increment_build_number\n # 3 创建签名的 .ipa 文件\n gym\nend\n```\n\n保存后命令行执行:\n\n```\nbundle exec fastlane build\n```\n\n<img src=\"https://tva1.sinaimg.cn/large/006tKfTcly1g0m8ua4im3j30xy0ceaca.jpg\" alt=\"fastlane-getting-started-create-test-target\" style=\"width: 600px; display: block; margin: auto;\" />\n\n## 上传到 App Store Connect\n\n使用 `deliver` 将截图、元数据、.ipa 文件上传到 App Store Connect。\n\n替换 `Deliverfile` 内容:\n\n```\n# 1 价格为 0 则是免费应用\nprice_tier(0)\n# 2 回答 Apple 在手动提交审核时会向您呈现的问题\nsubmission_information({\n export_compliance_encryption_updated: false,\n export_compliance_uses_encryption: false,\n content_rights_contains_third_party_content: false,\n add_id_info_uses_idfa: false\n})\n# 3 提供应用评级配置位置\napp_rating_config_path(\"./fastlane/metadata/app_store_rating_config.json\")\n# 4 提供.ipa文件位置\nipa(\"./fastlane/builds/mZone Poker.ipa”)\n# 5 将s ubmit_for_review 设置为 true 以自动提交应用以供审核\nsubmit_for_review(true)\n# 6 必须在应用审核接受后手动发布应用\nautomatic_release(false)\n```\n\n在 `Fastfile` 中添加:\n\n```\ndesc \"Upload to App Store\"\nlane :upload do\n deliver\nend\n```\n\n然后命令行执行:\n\n```\nbundle exec fastlane upload\n```\n\n完成后,登录 App Store Connect。屏幕截图,元数据和构建应该在那里,等待审查。\n\n## 将命令放在一起\n\n打开 `Fastfile` 添加:\n\n```\ndesc \"Create app, take screenshots, build and upload to App Store\"\nlane :do_everything do\n create_app\n screenshot\n build\n upload\nend\n```\n\n在 `Deliverfile` 添加:\n\n```\nforce(true)\n```\n\n执行:\n\n```\nbundle exec fastlane do_everything\n```\n\n## References\n\n- [fastlane Tutorial: Getting Started](https://www.raywenderlich.com/233168-fastlane-tutorial-getting-started)\n\n-- EOF --\n","tags":["ios"]},{"title":"【Swifter - Swift 开发者必备 Tips】笔记","url":"/2019/02/15/swifter-tips-reading-notes/","content":"\n再读王巍的【Swifter - Swift 开发者必备 Tips】,看看有什么新收获。\n\n## 柯里化(Currying)\n\n[柯里化](https://zh.wikipedia.org/wiki/%E6%9F%AF%E9%87%8C%E5%8C%96]) 是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术,这个词自己是第一次见到。\n\n自己的理解就是:把接受多个参数的函数变换为,先接受一个参数,然后返回一个函数,这个函数再接受其他参数。\n\n两个细节:\n\n- 只有一个参数,并且这个参数是该函数的第一个参数。必须按照参数的定义顺序来调用柯里化函数。\n- 柯里化函数的函数体只会执行一次,只会在调用完最后一个参数的时候执行柯里化函数体。\n\n<!-- more -->\n\n```swift\n/// 一个数加 x 的函数\nfunc addTo(_ adder: Int) -> (Int) -> Int {\n return { adder + $0 }\n}\n// +2\nlet addTwo = addTo(2)\nlet result = addTwo(6) // 8\n\n// +10\nlet addTen = addTo(10)\naddTen(6) // 16\n```\n\n柯里化是一种量产相似方法的好办法,可以通过柯里化一个方法模板来避免写出很多重复代码,也方便了今后维护。\n\n书中还提到了一个封装 [Selector](https://oleb.net/blog/2014/07/swift-instance-methods-curried-functions/?utm_campaign=iOS_Dev_Weekly_Issue_157&utm_medium=email&utm_source=iOS%252BDev%252BWeekly) 的例子,但是没懂,欢迎指教。\n\nReference:\n\n- [Swift 函数柯里化介绍及使用场景](https://www.jianshu.com/p/5b27fec8c616)\n\n## 将 protocol 的方法声明为 mutating\n\n`protocol` 不仅可以被 `class` 类型实现,也适用于 `struct` 和 `enum`。因为这个原因就要考虑定义的方法是否应该使用 `mutating` 来修饰。在 `protocl` 中使用 `mutating` 修饰的方法,对于 `class` 的实现是完全透明的。\n\n## 多元组(Tuple)\n\npython 中有见过类似。\n\n```swift\n/// 交互数据\nfunc swapMe<T>(_ a: inout T, _ b: inout T) {\n (a, b) = (b, a)\n}\n\nvar a = 10\nvar b = 20\nswapMe(&a, &b) // a: 20 b: 10\n```\n\n```swift\n/// 可读的返回值\nlet rect = CGRect(x: 0, y: 0, width: 100, height: 100)\nlet (slice, remainder) = rect.divided(atDistance: 20, from: .minYEdge)\n\n// slice {x 0 y 0 w 100 h 20}\n// remainder {x 0 y 20 w 100 h 80}\n```\n\n## `@autoclosure` 和 `??`\n\n`@autoclosure` 做的事情就是把一句表达式自动的封装成一个闭包(closure)。这样有时候在语法上看起来就会非常漂亮。\n\n```swift\nfunc logIfTrue(_ predicate: @autoclosure () -> Bool) {\n if predicate() {\n print(\"True\")\n }\n}\n\nlogIfTrue(2 > 1)\n// 而不是 logIfTrue { 2 > 1 }\n```\n\nSwift 把 `2 > 1` 这个表达式自动转换为 `() -> Bool`。\n\nSwift 中 `??` 定义为:\n\n```swift\nfunc ??<T>(optional: T?, defaultValue: @autoclosure () -> T?) -> T?\n\nfunc ??<T>(optional: T?, defaultValue: @autoclosure () -> T) -> T\n```\n\n```swift\n/// 猜测 ?? 的实现\nfunc ??<T>(optional: T?, defaultValue: @autoclosure() -> T) -> T {\n switch optional {\n case .some(let value):\n return value\n default:\n return defaultValue()\n }\n}\n```\n\n使用 `@autoclosure` 来修饰默认值看起来有些画蛇添足,但是如果默认值是通过一系列复杂计算得到话,在 optional 不为 `nil` 的情况下就会造成浪费。`@autoclosure` 将计算推迟到 `optional` 为 `nil`。\n\n## `@escaping`\n\n```swift\nfunc doWork(block: () -> Void) {\n block()\n}\n\ndoWork {\n print(\"work\")\n}\n```\n\n这里默认了一个隐藏假设:参数中 `block` 的内容会在 `doWork` 发会之前就完成了,也就是说,对于 `block` 的调用是同步行为。\n\n```swift\nfunc doWorkAsync(block: @escaping () -> ()) {\n DispatchQueue.main.async {\n block()\n }\n}\n```\n\n`@escaping` 表明这个闭包是会 **逃逸** 出该方法。\n\n```swift\nclass S {\n var foo = \"foo\"\n\n func method1() {\n doWork {\n print(foo)\n }\n foo = \"method1\"\n }\n func method2() {\n doWorkAsync {\n print(self.foo)\n }\n foo = \"method2\"\n }\n func method3() {\n foo = \"method4\"\n doWorkAsync { [weak self] in\n print(self?.foo ?? \"nil\")\n }\n foo = \"method3\"\n }\n}\n\nS().method1() // foo - 同步\nS().method2() // method2 - 强引用造成?\nS().method3() // nil - 已经释放\n```\n\n## Optional Chaining\n\n```swift\nlet playClosure = { (child: Child) -> Void in\n child.pet?.toy?.play()\n}\n```\n\n这里 `Void` 是不合理的,因为真正的结果是一个 `Optional` 的结果,所有应该为 `Void?`。\n\n```swift\nlet playClosure = { (child: Child) -> Void? in\n child.pet?.toy?.play()\n}\n\n// 判断方法是否调用成功:\nif let result = playClosure {\n print(\"happy\")\n} else {\n print(\"can't play\")\n}\n```\n","tags":["swift","ios"]},{"title":"PromiseKit 入门使用","url":"/2019/01/19/promisekit-getting-started/","content":"\n<img src=\"https://images.unsplash.com/photo-1579208570378-8c970854bc23?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=960&q=80\" />\n\n在 GitHub Trending 中总是看到 [mxcl/PromiseKit](https://github.com/mxcl/PromiseKit) 它是主要解决的是 “回调地狱” 的问题,决定尝试用一下。\n\n> 环境:Swift 4.2、PromiseKit 6\n\n## then and done\n\n下面是一个典型的 promise 链式(chain)调用:\n\n```\nfirstly {\n login()\n}.then { creds in\n fetch(avatar: creds.user)\n}.done { image in\n self.imageView = image\n}\n```\n\n<!-- more -->\n\n如果这段代码使用完成回调(`completion handler`)实现,他将是:\n\n```\nlogin { creds, error in\n if let creds = creds {\n fetch(avatar: creds.user) { image, error in\n if let image = image {\n self.imageView = image\n }\n }\n }\n}\n```\n\n`then` 是完成回调的另一种方式,但是它更丰富。在处级阶段的理解,它更具有可读性。上面的 promise chain 更容易阅读和理解:一个异步操作接着另一个,一行接一行。它与程序代码非常接近,因为我们很容易得到 Swift 的当前状态。\n\n`done` 与 `then` 基本是一样的,但是它将不再返回 promise。它是典型的在末尾 “success” 部分的 chain。在上面的例子 `done` 中,我们接收到了最终的图片并使用它设置了 UI。\n\n让我们对比一下两个 `login` 的方法签名:\n\n```\n// Promise:\nfunc login() -> Promise<Creds>\n\n// Compared with:\nfunc login(completion: (Creds?, Error?) -> Void)\n // 可选型,两者都是可选\n```\n\n区别在于 promise,方法返回 promises 而不是的接受和运行回调。每一个处理器(handler)都会返回一个 promise。Promise 对象们定义 `then` 方法,该方法在继续链式调用之前等待 promise 的完成。chains 在程序上解决,一次一个 promise。\n\nPromise 代表未来异步方法的输入值。它有一个表示它包装的对象类型的类型。例如,在上面的例子里,`login` 的返回 Promise 值代表一个 Creds 的一个实例。\n\n可以注意到这与 completion pattern 的不同,promises chain 似乎忽略错误。并不是这样,实际上:promise chain 使错误处理更容易访问(accessible),并使错误更难被忽略。\n\n## catch\n\n有了 promises,错误在 promise chain 上级联(cascade along),确保你的应用的健壮(robust)和清晰的代码。\n\n```\nfirstly {\n login()\n}.then { creds in\n fetch(avatar: creds.user)\n}.done { image in\n self.imageView = image\n}.catch {\n // 整个 chain 上的错误都到了这里\n}\n```\n\n> 如果你忘记了 catch 这个 chain,Swift 会发出警告\n\n每个 promise 都是一个表示单个(individual)异步任务的对象。如果任务失败,它的 promise 将成为 `rejected`。产生 `rejected` promises 将跳过后面所有的 `then`,而是将执行 `catch`。(严格上说是执行后续所有的 `catch` 处理)\n\n这与 completion handler 对比:\n\n```\nfunc handle(error: Error) {\n //...\n}\n\nlogin { creds, error in\n guard let creds = creds else { return handle(error: error!) }\n fetch(avatar: creds.user) { image, error in\n guard let image = image else { return handle(error: error!) }\n self.imageView.image = image\n }\n}\n```\n\n使用 `guard` 和合并错误对处理有所保证,但是 promise chain 更具有可读性。\n\n## ensure\n\n```\nfirstly {\n UIApplication.shared.isNetworkActivityIndicatorVisible = true\n return login()\n}.then {\n fetch(avatar: $0.user)\n}.done {\n self.imageView = $0\n}.ensure {\n UIApplication.shared.isNetworkActivityIndicatorVisible = false\n}.catch {\n // ...\n}\n```\n\n无论在 chain 哪里结束,成功或者失败,`ensure` 终将被执行。也可以使用 `finally` 来完成相同的事情,区别是没有返回值。\n\n```swift\nspinner(visible: true)\n\nfirstly {\n foo()\n}.done {\n // ...\n}.catch {\n // ...\n}.finally {\n self.spinner(visible: false)\n}\n```\n\n## when\n\n多个异步操作同时处理时可能又难又慢。例如当 `操作1` 和 `操作2` 都完成时再返回结果:\n\n```\n// 串行操作\noperation1 { result1 in\n operation2 { result2 in\n finish(result1, result2)\n }\n}\n```\n\n```\n// 并行操作\nvar result1: ...!\nvar result2: ...!\nlet group = DispatchGroup()\ngroup.enter()\ngroup.enter()\noperation1 {\n result1 = $0\n group.leave()\n}\noperation2 {\n result2 = $0\n group.leave()\n}\ngroup.notify(queue: .main) {\n finish(result1, result2)\n}\n```\n\n使人 Promises 将变得容易很多:\n\n```\nfirstly {\n when(fulfilled: operation1(), operation2())\n}.done { result1, result2 in\n // ...\n}\n```\n\n`when` 等待所有的完成再返回 promises 结果。\n\n## PromiseKit 扩展\n\nPromiseKit 提过了一些 Apple API 的扩展,例如:\n\n```\nfirstly {\n CLLocationManager.promise()\n}.then { location in\n CLGeocoder.reverseGeocode(location)\n}.done { placemarks in\n self.placemark.text = \"\\(placemarks.first)\"\n}\n```\n\n同时需要指定 subspaces:\n\n```\npod \"PromiseKit\"\npod \"PromiseKit/CoreLocation\"\npod \"PromiseKit/MapKit\"\n```\n\n更多的扩展可以查询 [PromiseKit organization](https://github.com/PromiseKit),甚至扩展了 [Alamofire](https://github.com/PromiseKit/Alamofire-) 这样的公共库。\n\n## 制作 Promises\n\n有时你的 chains 仍然需要以自己开始,或许你使用的三方库没有提供 promises 或者自己写了异步系统,没关系,他们非常容易添加 promises。如果你查看了 PromiseKit 的标准扩展,可以看到使用了下面相同的描述:\n\n已有代码:\n\n```\nfunc fetch(completion: (String?, Error?) -> Void)\n```\n\n转换:\n\n```\nfunc fetch() -> Promise<String> {\n return Promise { fetch(completion: $0.resolve) }\n}\n```\n\n更具有可读性的:\n\n```\nfunc fetch() -> Promise<String> {\n return Promise { seal in\n fetch { result, error in\n seal.resolve(result, error)\n }\n }\n}\n```\n\nPromise 初始化程序提供的 `seal` 对象定义了很多处理 `garden-variety` 完成回调的方法。\n\n> PromiseKit 设置尝试以 `Promise(fetch)` 进行处理,但是完成通过编译器的消歧义。\n\n## Guarantee<T>\n\n从 PromiseKit 5 开始,提供了 Guarantee 以做补充,目的是完善 Swift 强的的异常处理。\n\n`Guarantee` 永远不会失败,所以不能被 `rejected`。\n\n```\nfirstly {\n after(seconds: 0.1)\n}.done {\n // 这里不要加 catch\n}\n```\n\n`Guarantee` 的语法相较更简单:\n\n```\nfunc fetch() -> Promise<String> {\n return Guarantee { seal in\n fetch { result in\n seal(result)\n }\n }\n}\n\n// 减少为\n\nfunc fetch() -> Promise<String> {\n return Guarantee(resolver: fetch)\n}\n```\n\n## map compactMap 等\n\n- `then` 要求返回另一个 promise\n- `map` 要求返回一个 object 或 value 类型\n- `compactMap` 要求返回一个 可选型,如过返回 `nil`,chain 将失败并报错 `PMKError.compactMap`\n\n```\nfirstly {\n URLSession.shared.dataTask(.promise, with: rq)\n}.compactMap {\n try JSONSerialization.jsonObject($0.data) as? [String]\n}.done { arrayOfStrings in\n // ...\n}.catch { error in\n // Foundation.JSONError if JSON was badly formed\n // PMKError.compactMap if JSON was of different type\n}\n```\n\n除此之外还有:`thenMap` `compactMapValues` `firstValue` etc\n\n## get\n\n`get` 会得到 `done` 中相同值。\n\n```\nfirstly {\n foo()\n}.get { foo in\n // ...\n}.done { foo in\n // same foo!\n}\n```\n\n## tap\n\n为 debug 提供 `tap`,与 `get` 类似但是可以得到 `Result<T>` 这样就可以检查 chain 上的值:\n\n```\nfirstly {\n foo()\n}.tap {\n print($0)\n}.done {\n // ...\n}.catch {\n // ...\n}\n```\n\n## 补充\n\n### firstly\n\n上面例子中的 `firstly` 是语法糖,非必须但是可以让 chains 更有可读性。\n\n```\nfirstly {\n login()\n}.then { creds in\n // ...\n}\n\n// 也可以\nlogin().then { creds in\n // ...\n}\n```\n\n知识点:`login()` 返回了一个 `Promise`,同时所有的 `Promise` 有一个 `then` 方法。`firstly` 返回一个 `Promise`,同样 `then` 也返回一个 `Promise`。\n\n### when 变种\n\n- `when(fulfilled:)` 在所有异步操作执行完后才执行回调,一个失败 chain 将 rejects。It's important to note that all promises in the when continue. Promises have no control over the tasks they represent. Promises are just wrappers around tasks.\n- `when(resolved:)` 使一个或多个组件承诺失败也会等待。此变体 `when` 生成的值是 `Result<T>` 的数组,所有要保证相同的泛型。\n- `race` 只要有一个异步操作执行完毕,就立刻执行 `then` 回调。其它没有执行完毕的异步操作仍然会继续执行,而不是停止。\n\n### Swift 闭包接口\n\nSwift 有自动推断返回值和单行返回。\n\n```\nfoo.then {\n bar($0)\n}\n\n// is the same as:\n\nfoo.then { baz -> Promise<String> in\n return bar(baz)\n}\n```\n\n这样有好有坏,具体可以查询 [Troubleshooting](https://github.com/mxcl/PromiseKit/blob/master/Documentation/Troubleshooting.md)\n\n## 更多阅读\n\n- 强力建议阅读 [API Reference](https://mxcl.dev/PromiseKit/reference/v6/Classes/Promise.html)\n- 在 Xcode 使用 optinon-click 阅读 PromiseKit 代码\n- 在网上有一些 PMK < 5 的文章,里面的 API 有些不同要注意\n\n## References\n\n- [Getting Started](https://github.com/mxcl/PromiseKit/blob/master/Documentation/GettingStarted.md)\n- [Swift - 异步编程库 PromiseKit 使用详解 1(安装配置、基本用法)](https://www.hangge.com/blog/cache/detail_2231.html)\n\n-- EOF --\n","tags":["ios"]},{"title":"回顾 2018","url":"/2018/12/31/review-2018/","content":"\n<iframe frameborder=\"no\" border=\"0\" marginwidth=\"0\" marginheight=\"0\" width=298 height=52 src=\"//music.163.com/outchain/player?type=2&id=476618833&auto=0&height=32\"></iframe>\n\n重新翻阅的自己工作邮件的发件箱,回顾一年工作。新年伊始自己还是在开发 P 项目的 iOS App,开始写 Q&A 功能。一些不算太难的 tableView 布局的需求,对我来说,都是头大的问题。\n\n这段时期招聘时的面试,竟成为我学习 App 开发的一扇小窗。\n\n年前收到了奖金还是挺开心的。leader 新年寄语:\n\n- 要发声,要当主力\n- 当有好的想法时,要学会说服别人\n- 要有耐心,Yifan 需要时间的沉淀\n\n前两点意思差不多,这段时间思想上困扰我的是:自己对自己的定位是一个初级工程师,认为会的东西、经验不多,见识少,我尊重比我年长的工程师的想法与观点,也相信他们是经过长远思考过的。这个思维设定,我觉得没有什么问题。但是也许有人忽略了 **责任**,对方案负责,对项目负责。开发方案一再重建性修改,接口结构没有规范。\n\n2017 的总结说胜利属于伏地魔,本想苟着发育,这时发现:苟是苟不住的,这个世界 **需要英雄** carry。\n\n后来我感觉应该将公司看做一个舞台,舞台上有灯光、音效就要利用,展示自己、锻炼自己,即使是出糗,那就整理整理再来一次,who care? 成长是最重要的。\n\nCourse 模块是前工程师用 Objective-C 写的,离职后一直没有再维护,过年期间自己重构了所有 Objective-C 的代码,项目完全转为了一个纯 Swift 项目。使用 Realm 作为数据本地化方案,选择的原因也很朴素,GitHub 哪个星多我就优先选用什么。\n\n后来参考 [Jack Feng - 6ag](https://github.com/6ag) 的几个 Swift 开源项目,新创建了 P 项目的工具 App,也对主项目结构做了重新的整理,在这里再次特别感谢。\n\n西安运营部的成立,加多了 C 项目的后台需求,难以都顾及项目两头。使用 laravel-admin 搭建新后台,也开始使用 Docker 部署项目,感觉从此离不开 Docker 了,像极了遇到 Git 时的感觉。\n\nC 项目主站的前端是在服务端渲染,非常传统的模式,多个工程师转手也是十分混乱。参考了 [白俊遥](https://baijunyao.com/) 工程的博客、laravel 项目,修改了项目结构,添加了 gulp 工具制定了工作流,虽然没有实现前后端的完全分离,但终究是向现代化前端走出了一步。\n\n转眼就到了年中调薪,公司不含糊,薪水涨到了我满意的值。这对我很关键,调整的不单单是我的薪水,也调整了我的心态。因为当时我认为没有强的工程师、甚至归我负责,却拿着比我还高的薪水。现在总算是有了一个平衡。\n\n不断向 C 项目投入更多的资源,项目 **指标** 的要求越来越多。比如:优化了项目、优化了查询,到底优化了多少,怎么量化?这些在之前一直不被重视,改好了就都算叫优化了。项目中出现的问题错误,都要查找真实具体的原因,而不是说一个可能的什么原因,就当解答的了。我一开始也很难适应这些,但心里是认同的。\n\nC 项目数据源重新整理,终于摸到了一次算法的边。“你觉的算法没用,是因为你不会。” 数据标记、正确率、召回率、特征词提取、文本相似度、词条打分、背包算法…… 一段最让人回忆的开发经历,与业务开发完全不同的模式。\n\nC 项目衍生出新项目 G,我一个人做,一次实践 Laravel 框架的机会。\n\n再次和 Z 哥合作了一个 App 项目,私活做的真是的太累了,极不推荐。其实这段时期工作上并没有在做 iOS,因为这个项目没让自己 iOS 的技能凉下来,为后面回归 iOS 项目保持了状态。\n\n参加了年度的 Swift 大会,在群里简单了很多大佬、看过博客的工程师,没想到他们都很乐意加微信。也发现他们相互都认识,这就是圈子吧。\n\n2018 是世界杯年,我不懂球就是看个热闹,最开心的是公司有活动基金,我拉着不少新同事们吃喝了不少。冬季后发展成了和同事的每周的火锅,真挺快乐的。\n\n买了心心念的 Switch 叫的闪送,上午下单一会后,就像收外卖一样收了机子。荒野之息是好玩,呃… 男主竟然叫林克不是塞尔达,你敢想?\n\n因为 MSI 的胜利开始关注 RNG LPL。这年 LPL 后面的比赛我基本都看了,中午还会看 dys 的直播实况。洲际赛 RW 救世主真是精彩,“当没人相信你的时候,你自己相信了自己,并且赢得了胜利,这便是成为英雄的道路。” 登峰造極 2018 我们是冠军。\n\n永利\\*暴雷崩盘,损失不少,派出所报案。家里,多年的期房要发钥匙了,感恩父母。一次团建向比比求婚成功。\n\n这年最后一天是在三亚,吃到了棒棒的早晨,去了蜈支洲岛,感受了乘风破浪,遇到了一个有趣的东北司机,参加了酒店的晚会,看了喝酒赢到的电影。\n\n2018 Happy Ending.\n\n> 做了一个梦 我们 iG 拿冠军了 啊哈哈哈…\n> 我说是不是假的 确实是个梦吧,\n> 但是就是当时虽然在做 但是特别开心。\n> 所以我们尽量就是自己努力一下,\n> 然后每个人发挥好一点的话,\n> 我觉得真的有可能 也可以拿冠军的。\n\n-- EOF --\n","categories":["review"]},{"title":"二分查找 Binary Search","url":"/2018/12/10/binary-search/","content":"\n快速从一个数组中查找一个元素。\n\n## Linear Search 线性查找\n\n```swift\nfunc linearSearch<T: Equatable>(_ a: [T], _ key: T) -> Int? {\n for i in 0 ..< a.count {\n if a[i] == key {\n return i\n }\n }\n return nil\n}\n```\n\n线性查找在最坏情况:遍历了整个数组,但没有找到合适的元素。平均要遍历一半的元素性能为 `O(n)`,而二分查找的效率为 `O(log n)`,也就是说一个有 `1,000,000` 元素的数组只需要 `20` 步就可以找到想要的元素 `log_2(1,000,000) = 19.9`。\n\n但是二分查找要求数组必须是排好序。\n\n二分查找步骤:\n\n1. 将数组分为两半。\n2. 判断想要找的元素是在左边数组还是右边,这也是数组需要排好顺序的原因。\n3. 如果要的元素在左边,就将左边的数组分成更小的两部分,并判断要的元素在哪部分。\n4. 重复步骤直到找到想要的元素。如果数组不能进一步查分,就说明要找的元素不在数组中。\n\ndivide-and-conquer\n\n## The code\n\n```\nfunc binarySearch<T: Comparable>(_ a: [T], key: T, range: Range<Int>) -> Int? {\n if range.lowerBound >= range.upperBound {\n // If we get here, then the search key is not present in the array.\n return nil\n\n } else {\n // Calculate where to split the array.\n let midIndex = range.lowerBound + (range.upperBound - range.lowerBound) / 2\n\n // Is the search key in the left half?\n if a[midIndex] > key {\n return binarySearch(a, key: key, range: range.lowerBound ..< midIndex)\n\n // Is the search key in the right half?\n // 这里 + 1 的原因是排除 midIndex 中间值\n } else if a[midIndex] < key {\n return binarySearch(a, key: key, range: midIndex + 1 ..< range.upperBound)\n\n // If we get here, then we've found the search key!\n } else {\n return midIndex\n }\n }\n}\n```\n\n```\n// 19 numbers\nlet numbers = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67]\n\n// 0 ..< numbers.count 覆盖所有范围\nbinarySearch(numbers, key: 43, range: 0 ..< numbers.count) // gives 13\n```\n\n二分查找是将数组分为两个,但是我们不需要正真的创建两个新数组。取而代之,我们使用 Swift `Range` 对象跟踪这些拆分。左闭右开。upperBound 总是比最后一个元素的索引多一。\n\n```\nmidIndex = (lowerBound + upperBound) / 2\n```\n\n如果这样写将存在一个 bug,就是当这两值非常大时,将存在一个越界的问题。\n\n## Iterative vs recursive 迭代 vs 递归\n\n二分查找本质是递归。\n\n使用迭代的方式实现:\n\n```\nfunc binarySearch(_ a: [Int], key: Int) -> Int? {\n var lowerBound = 0\n var upperBound = a.count\n while lowerBound < upperBound {\n // 这行僵硬了 没有必要的\n var range = lowerBound ..< upperBound\n let midIndex = range.lowerBound + (range.upperBound - range.lowerBound) / 2\n let midValue = a[midIndex]\n if key < midValue {\n upperBound = midIndex\n } else if key > midValue {\n lowerBound = midIndex + 1\n } else {\n return midIndex\n }\n }\n return nil\n}\n```\n\n## The end\n\n查找前数组一定要先排序吗?这取决于排序花费的时间,有时候:数组排序加二分查找比线性搜索还要慢。二分查找的优势在于一次排序后多次查找。\n\n文章代码:[GitHub - imzyf/data-structure-and-algorithm/004-Binary Search/](https://github.com/imzyf/data-structure-and-algorithm/tree/master/004-Binary%20Search)。\n\n## References\n\n- [raywenderlich/swift-algorithm-club/Binary Search](https://github.com/raywenderlich/swift-algorithm-club/tree/master/Binary%20Search)\n","tags":["algorithm","swift"]},{"title":"插入排序 Insertion Sort","url":"/2018/11/24/insertion-sort/","content":"\n将一个数组从高到低或者从低到高排序。\n\n插入排序算法的工作原理:\n\n1. 将若干数字放在一个数组里,数组是乱序的。\n2. 从数组中挑选一个数字,它是哪个并不重要,但是为了方便我们挑选数组头部的这个。\n3. 将这个数字插入到一个新的数组里。\n4. 从乱序数组里挑选下一个数字也将它放到新数组里。这个数字要么在第一个数字前或者后,所以这个两个数字是被排序的。\n5. 再次重从乱序数组里挑选下一个数字也将它放到新数组里,并将数字放在正确的位置。\n6. 一直如此进行直到乱序数组中没有数字。这时也将等到一个排序好的新数组。\n\n<!-- more -->\n\n自己的一个实现:\n\n```\nlet array = [2, 1, 3, 8, 3, 5, 4]\n\nvar newArray = [Int]()\nfor (k, v) in array.enumerated() {\n for (nK, nV) in newArray.enumerated() {\n // 本次的数 小于 存在的数的第一个(nv)\n if v < nV {\n newArray.insert(v, at: nK)\n break\n }\n }\n // 没有插入成功 放在末尾\n if newArray.count < k + 1 {\n newArray.append(v)\n }\n}\n```\n\n## In-place sort\n\n上面的排序需要两个数组,一个原始的,一个排好顺序的。但是我们也可以 _就地排序_ 无需创建一个额外的数组。我们只需要跟踪记录原始数组中哪里部分排好顺序了,哪一部分还没有排序。\n\n举例:`[ 8, 3, 5, 4, 6 ]` 使用 `|` 分割是否排好顺序的部分。\n\n```\n// 开始时 | 在最前\n[| 8, 3, 5, 4, 6 ]\n\n// 开始向左移动,左侧只有个 8 无论什么顺序都是正确的,右侧是未排序的部分\n[ 8 | 3, 5, 4, 6 ]\n\n// 依次进行 将未排序的头部元素放在已排部分的正确位置\n[ 3, 8 | 5, 4, 6 ]\n[ 3, 5, 8 | 4, 6 ]\n[ 3, 4, 5, 8 | 6 ]\n[ 3, 4, 5, 6, 8 |]\n```\n\n每次 `|` 移动,都对左侧进行排序,未排序的部分逐渐减少,排序部分增加。直到未排序部分为零。\n\n## How to insert\n\n将未排序的头部元素放在已排部分的正确位置,如何这到这点的?\n\n```\n// 从此状态开始。下个说的 4,我们需要将 4 插入到 [ 3, 5, 8 ] 这个已经排好的数组里\n[ 3, 5, 8 | 4, 6 ]\n\n// 移动 |,这时我们注意 8 这个元素\n[ 3, 5, 8, 4 | 6 ]\n ^\n\n// 8 大于 4,所有 8 应该在 4 的右边,8 与 4 进行位置调换\n[ 3, 5, 4, 8 | 6 ]\n <-->\n swapped\n\n// 将 4 与现在的前面的元素 5 进行比较,5 大于 4,所以 5 与 4 进行位置调换\n[ 3, 4, 5, 8 | 6 ]\n <-->\n swapped\n\n// 3 小于 4 这个数,所以我们完成了对 4 的排序,这时从头到 |,是排好顺序的\n[ 3, 4, 5, 8 | 6 ]\n```\n\n这就是对插入排序算法的内循环的描述。\n\n## The code\n\n```\nfunc insertionSort(_ array: [Int]) -> [Int] {\n // 1\n var a = array\n // 2\n for x in 1..<a.count {\n var y = x\n // 3\n while y > 0 && a[y] < a[y - 1] {\n a.swapAt(y - 1, y)\n y -= 1\n }\n }\n return a\n}\n```\n\n1. 将 `array` 复制一个副本。因为我们无法直接修改参数中的 `array`,就想 Swift 自身的 `sort()`,`insertionSort()` 将返回一个排序顺序的副本数组。\n2. 两个循环在方法中。外层循环遍历轮到排序的元素,也就是从待排数组中挑选出头部的元素。`x` 索引为排好顺序的结尾索引同时也是待排数组的开头。记住,如何时间从开头到 `x` 永远都是排好顺序的,从 `x` 到最后的元素都是未排序的。\n3. 内层循环查询 x 索引位置的元素。这个元素可能小于之前排序顺序数组中的每一个元素。内层循环从后倒序遍历每一个已排序的元素,每次发现这个元素之前的元素比它大,则交互位置。当内层循环完成时,数组从开头到 x 将又是已排序的。\n\ntip:外层循环从索引 1 开始,而不是 0。将第一个元素从堆移动到排序部分实际上并没有改变任何东西,所以我们不妨跳过它。\n\n## No more swaps\n\n上面的插入排序可以正常的工作了。我们还可以通过移除调用 swap() 让程序更快一些。\n\n我们可以将所有需要换位置的元素向右移动一个位置,然后将新数字复制到正确的位置。\n\n```\n[ 3, 5, 8, 4 | 6 ] remember 4\n *\n\n[ 3, 5, 8, 8 | 6 ] shift 8 to the right\n --->\n\n[ 3, 5, 5, 8 | 6 ] shift 5 to the right\n --->\n\n[ 3, 4, 5, 8 | 6 ] copy 4 into place\n *\n```\n\n```\nfunc insertionSort(_ array: [Int]) -> [Int] {\n var a = array\n for x in 1..<a.count {\n var y = x\n let temp = a[y]\n // tip\n while y > 0 && temp < a[y - 1] {\n // 1\n a[y] = a[y - 1]\n y -= 1\n }\n // 2\n a[y] = temp\n }\n return a\n}\n```\n\n1. 原本需要换位置的元素右移一个位置。\n2. 当内层结束时,`y` 的索引位置就是新元素的排序后的位置,将元素放在此。\n\ntip:这里我自己写成了 `while y > 0 && a[y] < a[y - 1]` 这是不对的,因为我要找的是 **原本** 的 `a[y]` 的位置,但是循环一次后 `a[y]` 将发生变化。\n\n## Making it generic\n\n```\nfunc insertionSort<T>(_ array: [T], _ isOrderedBefore: (T, T) -> Bool) -> [T] {\n var a = array\n for x in 1..<a.count {\n var y = x\n let temp = a[y]\n while y > 0, isOrderedBefore(temp, a[y - 1]) {\n a[y] = a[y - 1]\n y -= 1\n }\n a[y] = temp\n }\n\n return a\n}\n```\n\n通过闭包来执行大小比较。\n\n```\nlet numbers = [ 10, -1, 3, 9, 2, 27, 8, 5, 1, 3, 0, 26 ]\ninsertionSort(numbers, <)\n\nlet objects = [ obj1, obj2, obj3, ... ]\ninsertionSort(objects) { $0.priority < $1.priority }\n```\n\n插入排序是一种稳定 `stable` 的排序。当排序后具有相同排序键的元素保持相同的相对顺序时,排序是稳定的。这对于诸如数字或字符串之类的简单值并不重要,但在排序更复杂的对象时这很重要。在上面的示例中,如果两个对象具有相同的优先级,则无论其他属性的值如何,这两个对象都不会被交换。\n\n## Performance\n\n最差的插入排序是 `O(n^2)` 因为俩个相近的循环嵌套。其他排序算法(如快速排序和合并排序)具有 `O(n log n)` 性能,在大输入时速度更快。\n\n插入排序实际上对于排序小数组非常快。某些标准库具有排序功能,当分区大小为 `10` 或更小时,可以从快速排序切换到插入排序。\n\n将 `insertSort()` 与 Swift 的内置 `sort()` 进行比较。在大约 `100` 元素左右的阵列上,速度差异很小。但是,随着输入变大,`O(n^2)` 快速开始执行比 `O(n log n)` 差很多,并且插入排序无法跟上。\n\n文章代码:[GitHub - imzyf/data-structure-and-algorithm/003-Insertion Sort/](https://github.com/imzyf/data-structure-and-algorithm/tree/master/003-Insertion%20Sort)。\n\n## References\n\n- [raywenderlich/swift-algorithm-club/Insertion Sort](https://github.com/raywenderlich/swift-algorithm-club/tree/master/Insertion%20Sort)\n","tags":["algorithm","swift"]},{"title":"队列 Queue Data Structure","url":"/2018/11/22/queue-data-structure/","content":"\n实现一个 `队列`,包括 `enqueue`、`dequeue`、`peek`。\n\n## Queue\n\n`队列` 核心也是 array,A queue gives you a FIFO or first-in, first-out order. 队列是:先进先出的。\n\n```swift\npublic struct Queue<T> {\n fileprivate var array = [T]()\n}\n```\n\n<!-- more -->\n\n## enqueue\n\n进队,在数组尾部追加元素。\n\n```swift\npublic mutating func enqueue(_ element: T) {\n array.append(element)\n}\n```\n\n## dequeue\n\n出队,将首位的元素移除。因为首位元素移除后,其他元素依次向前移动,所以是 O(n)。\n\n```swift\npublic var isEmpty: Bool {\n // 使用数组自身的方法,而不是 array.count > 0\n return array.isEmpty\n}\n\npublic mutating func dequeue() -> T? {\n // 使用定义的变量\n if isEmpty {\n return nil\n } else {\n return array.removeLast()\n }\n}\n```\n\n## peek\n\n查看队首元素。\n\n```swift\n/// peek() 改为更有语义话的只读变量\npublic var front: T? {\n return array.first\n}\n```\n\n## 优化出队\n\n在出队后不移动元素而是移动 `起始索引`,就像动的收银台而不是排队的人。\n\n```swift\n/// 优化 队列 的出队\npublic struct OptimizedQueue<T> {\n\n /// 这里改为了可选型,为了可以清理无效的元素\n fileprivate var array = [T?]()\n /// 起始索引\n fileprivate var head = 0\n\n public var count: Int {\n // 减去 起始索引 前面的数量\n return array.count - head\n }\n\n public var isEmpty: Bool {\n // 根据实际数量判断\n return count == 0\n }\n\n // 保持不变\n public mutating func enqueue(_ element: T) {\n array.append(element)\n }\n\n public mutating func dequeue() -> T? {\n guard head < array.count,\n let element = array[head] else {\n return nil\n }\n // 置空当前位置元素\n array[head] = nil\n // 前移起始索引\n head += 1\n\n // 空索引的占用比例\n let percentage = Double(head)/Double(array.count)\n // 50 0.25 都是魔法数字,主要是为了控制数组修剪的频率,可以自行调整\n if array.count > 50 && percentage > 0.25 {\n // 将起始空元素删除\n array.removeFirst(head)\n // 重置 起始索引\n head = 0\n }\n\n return element\n }\n\n public var front: T? {\n if isEmpty {\n return nil\n } else {\n // 根据 起始索引进行 返回\n return array[head]\n }\n }\n}\n```\n\n文章代码:[GitHub - imzyf/data-structure-and-algorithm/002-Queue/](https://github.com/imzyf/data-structure-and-algorithm/tree/master/002-Queue)。\n\n## References\n\n- [raywenderlich/swift-algorithm-club/Queue](https://github.com/raywenderlich/swift-algorithm-club/tree/master/Queue)\n","tags":["algorithm","swift"]},{"title":"栈 Stack Data Structure","url":"/2018/11/22/stack-data-structure/","content":"\n加入 [Swift Algorithm Club](https://github.com/raywenderlich/swift-algorithm-club) /'ælgə'rɪðəm/,回炉重新学习数据结构与算法。\n\n自己创建的项目:[GitHub - imzyf/data-structure-and-algorithm](https://github.com/imzyf/data-structure-and-algorithm)。\n\n实现一个 `栈` /stæk/,包含 `push` `peek` `pop` 与 `Generics` 泛型。\n\n<!-- more -->\n\n## stack\n\n`栈` 非常像一个数组,它包括少量的方法。\n\n- push 添加一个新元素到栈顶\n- pop 从栈顶移除一个元素\n- peek 查看栈顶的一个元素但是不 pop\n\nA stack gives you a LIFO or last-in first-out order. 栈是后进先出,队列是先进先出。\n\n```\npublic struct Stack<Element> {\n fileprivate var array: [Element] = []\n}\n```\n\n## push\n\n`push` 是在数组的尾部添加元素是以 `O(1)`,如果是在数组最前添加是 `O(n)` 这是昂贵的。\n\n```\npublic mutating func push(_ element: Element) {\n array.append(element)\n}\n```\n\n因为使用的 `struct`,修改属性值的方法要加 `mutating`。\n\n## pop\n\n想从一个空栈中弹出最后一个元素将返回 `nil`。\n\n```\npublic mutating func pop(_ element: Element) {\n return array.popLast()\n}\n```\n\n## peek\n\n与 `pop` 有点像,但是并没有移除栈顶的元素。\n\n```\n/// peek 改为更加语义化的 top 只读变量\npublic var top: T? {\n return array.last\n}\n```\n\n## other\n\n两个其他的常用属性,栈是否为空,栈中元素的个数。\n\n```\npublic var isEmpty: Bool {\n return array.isEmpty\n}\n\npublic var count: Int {\n return array.count\n}\n```\n\n## References\n\n- [Swift Algorithm Club: Swift Stack Data Structure](https://www.raywenderlich.com/800-swift-algorithm-club-swift-stack-data-structure)\n\n-- EOF --\n","tags":["algorithm","swift"]},{"title":"解决 Too many symbol files","url":"/2018/10/30/correct-too-many-symbol-files-issues/","content":"\n在上传 App 到 App Store 后收到邮件,有 issues **Too many symbol files**。在之前看到 _Your delivery was successful_,此 issues 不影响发布,所以一直搁置了。\n\n今天决定彻底处理下。\n\n<!-- more -->\n\n## 背景\n\n先说 `*.symbols` 这文件是干嘛的,我现在(2018-10)对此的理解:\n\n- symbols 为符号表文件\n- 符号表是内存地址与函数名、文件名、行号的映射表 `<起始地址> <结束地址> <函数> [<文件名:行号>]`\n\n为什么要配置符号表?\n\n为了能快速并准确地定位用户 App 发生 **Crash 的代码位置**,使用符号表对 App 发生 Crash 的程序 _堆栈_ 进行 _解析_ 和 _还原_。\n\n![006tNbRwly1fwq98vcjeoj30i00383yh](https://user-images.githubusercontent.com/9289792/80204521-9ea54a80-865b-11ea-9420-7d8a2d32e910.jpg)\n\n## 项目情况\n\n再说下项目情况,因为数字都是用了的是 Int,为防止 32 位设备发生越界情况(理由好像有点扯),所以项目端设置了设备限制 `arm64`,也就是 5s 之前的设备不可以安装。\n\n因为使用了三方库,但是三方库是支持 32 位设备的,所以生成了冗余的 symbols 文件。\n\n查询 symbols 文件的生成情况:Xcode Window -> Organizer 选择有问题的 archive,右击选择 Show in finder,命令行进入 \\*.app 中的 dSYMs 文件夹,执行 `dwarfdump --uuid *` 可以查询到是否生成了多余的文件。\n\n## 解决\n\n在 `Podfile` 中:\n\n```txt\npost_install do |installer|\n installer.pods_project.targets.each do |target|\n target.build_configurations.each do |config|\n config.build_settings['ENABLE_BITCODE'] = 'NO'\n config.build_settings['ARCHS'] = 'arm64'\n end\n end\nend\n```\n\n## 检查\n\n在 `info.plist` 中:\n\n```plist\n<key>UIRequiredDeviceCapabilities</key>\n<array>\n <string>arm64</string>\n</array>\n```\n\n在 build Settings 搜索 `valid architecture` 中,填写 `arm64`\n\n## References\n\n- [“Too many symbol files” after successfully submitting my apps](https://stackoverflow.com/questions/25755240/too-many-symbol-files-after-successfully-submitting-my-apps)\n- [“Too many symbol files” warnning when submitting app](https://stackoverflow.com/questions/34313049/too-many-symbol-files-warnning-when-submitting-app)\n- [App 提交 iTunes Connect,\"二进制无效\"问题解决方案。](https://www.jianshu.com/p/3511ec38ca20)\n- [Bugly iOS 符号表配置](https://bugly.qq.com/docs/user-guide/symbol-configuration-ios/?v=20180709165613#_2)\n\n-- EOF --\n","tags":["ios"]},{"title":"在 MySQL 中选择合适的日期类型","url":"/2018/05/25/select-the-appropriate-date-type-in-mysql/","content":"\n如何在 MySQL 中选择合适的日期类型困扰了很久,`varchar`、`int`、`timestamp`、`datetime` 都有尝试过,近来有所感悟,做此总结。\n\n注:此总结考虑了 PHP 和 Laravel 框架的特点。\n\n## 使用 varchar\n\n`varchar` 存储日期时间的格式完全可以自己控制,`月/日/年` 还是 `年-月-日` 需求怎么说就怎么存,读取后展示是也不用在格式化。同时伏笔也就此埋下:日期时间格式没强制约束,总有一天字段里出现了与众不同的格式;要是日期时间会 _变化_ 或作为 _查询条件_ 或要进行 _排序_ 时就又是一坑,还是要格式化标准格式再处理。可以说 `varchar` 应该是最差的选择了。\n\n<!-- more -->\n\n## 使用 int 与 timestamp\n\nPHP `time()` 可以直接获取当前时间戳秒数,数据库字段要也是 `int` 一存就完事了,不会有格式问题,谁用什么样转什么样。但是在数据库工具中查看此字段时显示不够直观,范围时会不方便,这些在使用 `timestamp` 是会得到解决。\n\ntimestamp 是我一直迷惑的一个类型。我写了几个例子做测试:\n\n1. 将 Laravel 项目设置为 `CST` 中国标准时间,MySQL 时区设置为 `UTC`,使用 `now()` 获取当前日期时间,比如:`2018-5-25 11:00:00` 存入 `timestamp` 类型的字段中,使用数据库工具查看字段结果为仍然为 `2018-5-25 11:00:00`。\n2. 继续上面的操作,项目中使用查询语句查询刚才的记录,结果显示为 `2018-5-25 11:00:00`,将项目时区从 `CST` 改为 `UTC` 后再次查询的结果仍然为 `2018-5-25 11:00:00` 没有变化。\n3. 继续上面的操作,将数据库的时区改为 `+8:00`,数据库工具、项目查询后的结果为 `2018-5-25 19:00:00` 发生了变化,修改项目为 `CST` 查询结果是 `2018-5-25 19:00:00` 和刚才一样也变化了。\n\n这个测试说明了:\n\n- 项目的时区影响的是 PHP 的时区,影响的是 `now()` 产生的日期时间。\n- 一个日期时间从项目存入数据库时,这个日期时间的时区是数据库设置的时区,和项目无关了。传入什么样子数据库存什么样子,更换了参照系,但是没有转换日期时间。\n- 从数据库中读一个时间戳日期时间,这个是时间戳日期时间受到数据库的时区影响,和项目的时区无关。\n\n我现在认识的结论:\n\n- 因为数据库时区不可能轻易改变,所以依靠数据库转换时区不可能。\n- 没看出来时间戳对于处理时区对 `datatime` 有什么过人之处,若有就是时间戳将时间标准设置在了 `UTC`,占 4 字节更小些。\n- 数据库时区和项目时区不一致可能会坑,如果一直不一致同时各个项目也一直错下去,没不会有察觉。\n- 对应国际化项目,是不是应该将数据库和项目还有服务器都设置为 `UTC`?谁使用谁根据用户的时区进行处理。\n\n## 使用 timestamp 与 datetime\n\n`datetime` 字段类型存储的时间不会随时间库时区发生变化,占 8 个字节,可存储 `1000-01-01 00:00:00` 到 `9999-12-31 23:59:59`。而时间戳只能存储 `1970` 年到 `2038` 年多,`2038` 还有 20 年怎么感觉马上就到了。\n\n`datetime` 个人推荐的存储时间的格式。\n\n## 时区处理常见问题\n\n北京用户在北京时间 `2018-05-27 10:00:00` 发布一个文章。\n\n如果在前端发布日期时间为 `2018-05-27 10:00:00`,在北京用户看来是没问题的,但是在美国的用户看来是奇怪的,因为美国没有到 `2018-05-27 10:00:00` 他们甚至还在 26 日。\n\n这时 `1 min ago` 这种展示形式就解决了这个问题,`ago` 是一种绝对的方式,在一个月之后再显示完整的日期时间,此时世界各地肯定已经度过了这个要展示日期时间,无论在哪国用户显示 `2018-05-27 10:00:00` 都不会奇怪了。\n\n同时也要注意,我们将各个时区都设置为 `UTC` 后,`ago` 的时间很好计算,但是在一个月之后日前时间完全显示,美国用户看北京用户的发布日期时间也应该是在中国看来的 `2018-05-27 10:00:00`,此发布日期不应该以美国用户为标准。这个问题的解决方法还在考虑,日期时间是存储多个字段来区分用于显示或比较,还是自行转换处理,需要实践测试。\n\n## MySQL 查看、修改时区\n\n### 查看时区\n\n```\n> select now(); # 或 select curtime();\n\n> show variables like \"%time_zone%\"; # SELECT @@global.time_zone, @@session.time_zone;\n+------------------+--------+\n| Variable_name | Value |\n+------------------+--------+\n| system_time_zone | CST |\n| time_zone | SYSTEM |\n+------------------+--------+\n\n# time_zone 说明 mysql 使用 system 的时区,system_time_zone 说明 system 使用 CST 时区\n```\n\n### 修改时区\n\n```\n> set global time_zone = '+8:00'; # 修改 mysql 全局时区为北京时间,即我们所在的东8区\n> set time_zone = '+8:00'; # 修改当前会话时区 SET time_zone = 'Asia/Shanghai'\n> flush privileges; # 立即生效\n```\n\n通过修改 `my.cnf` 配置文件来修改时区\n\n```\n# vim /etc/my.cnf\n# 在[mysqld]区域中加上\ndefault-time-zone='+8:00'\n\n# /etc/init.d/mysqld restart # 重启 mysql 使新时区生效\n```\n\n## 最后的总结\n\n全球化的项目应当考虑:\n\n- 项目、数据库、服务器都建议设置为 `UTC`\n- 要考虑时分秒的字段用 `datetime` 类型,不用考虑时分秒的字段用 `date`\n- `created_at` 和 `updated_at` 字段,考虑 `Laravel` 框架的特点使用 `timestamp`\n\n## References\n\n- [mysql 修改时区的几种方法](http://coolnull.com/4091.html)\n\n-- EOF --\n","tags":["mysql"]},{"title":"NGINX 禁止 IP 访问","url":"/2018/05/20/nginx-ip-forbidden/","content":"\n禁止 IP 访问,其他域名跳转到 `www.xxx.com`:\n\n```conf\nserver {\n listen 80;\n server_name 55.66.77.88;\n deny all;\n}\n\nserver {\n listen 80;\n server_name www.xxx.com xxx.com;\n\n return 301 https://www.xxx.com$request_uri;\n}\n\nserver {\n listen 443 ssl http2;\n\n #...\n\n if ($host = 55.66.77.88) {\n return 403;\n }\n if ($host != 'www.xxx.com'){\n rewrite ^/(.*)$ https://www.xxx.com/$1 permanent;\n }\n\n #...\n}\n```\n\n-- EOF --\n","tags":["nginx"]},{"title":"L01 Web 开发实战入门","url":"/2018/05/09/laravel-essential-training-reading-notes/","content":"\n> [Laravel 教程 - Web 开发实战入门](https://laravel-china.org/courses?rf=10678)\n\n## 基础信息\n\n### Laravel 与 PHP\n\n`Ruby on Rails` 有以下原则:\n\n- 强调与注重敏捷开发;\n- 约定高于配置(Convention over configuration);\n- DRY(Don't repeat yourself)不要重复自己,提倡代码重用;\n- 重视「编码愉悦性」。\n\n### 如何正确阅读本书\n\n随后你会有很多机会来学习它们。现在最重要的是保持『训练』的连贯性。\n\n编程是技能,不是知识,技能只有在不断刻意练习下才会有进步。\n\n<!-- more -->\n\n## 开发环境布置\n\n### 第一个应用\n\n```bash\ncomposer create-project laravel/laravel Laravel --prefer-dist \"5.5.*\"\n```\n\n### Git 与 GitHub\n\n设置 push 的默认模式为 simple\n\n```bash\ngit config --global push.default simple\n```\n\n### 部署上线\n\n注册 Heroku 后:\n\n```bash\nheroku login\n\n# 添加 SSH Key 到 Heroku 上\nheroku keys:add\n\n# 创建配置文件来告诉 Heroku 应当使用什么命令来启动 Web 服务器\necho web: vendor/bin/heroku-php-apache2 public/ > Procfile\ngit add -A\ngit commit -m \"Procfile for Heroku\"\n\n# 创建一个新应用\nheroku create\n\n# 对应用名称进行更改,保证未被其它人占用\nheroku rename imzyf-laravel-essential\n\n# 声明应用是用 PHP 写的\nheroku buildpacks:set heroku/php\n\n# 设置 APP key\nphp artisan key:generate\nheroku config:set APP_KEY=base64:wuWj8Kicza6I9YxgWczviNVcueVN2RroqiUILreyNmA=\n\n# 部署上线\ngit push heroku master\n\n# 快速打开线上应用\nheroku open\n\n# 输出生产环境上的日志\nheroku logs\n```\n\n## 构建页面\n\n### 静态页面\n\n生成静态页面控制器:\n\n```bash\nphp artisan make:controller StaticPagesController\n```\n\n## 页面优化\n\n### 样式美化\n\n```bash\n# 升级 yarn\nbrew upgrade yarn\n\nyarn install --no-bin-links\nyarn add cross-env\n```\n\n每次检测到 `.scss` 文件发生更改时,自动将其编译为 `.css` 文件:\n\n```bash\nnpm run watch-poll\n```\n\n### Laravel 前端工作流\n\nLaravel Mix 一款前端任务自动化管理工具。Mix 提供了简洁流畅的 API,让你能够为你的 Laravel 应用定义 Webpack 编译任务。\n\n`_header.blade.php` 为局部视图增加前缀下划线是『约定俗成』的做法。\n\n### 布局中的链接\n\n```bash\n<li><a href=\"/help\">帮助</a></li>\n\n// 可以改写为\n\n<li><a href=\"{{ route('help') }}\">帮助</a></li>\n```\n\n路由中修改:\n\n```bash\nRoute::get('/help', 'StaticPagesController@help')->name('help');\n```\n\n`route('help')` 将被渲染为 `http://sample.test/help`。\n\n## 用户模型\n\n### 数据库迁移\n\n- 当我们运行迁移时,`up` 方法会被调用\n- 当我们回滚迁移时,`down` 方法会被调用\n\n### 查看数据库表\n\n```bash\nphp artisan migrate\n\n# 回滚\nphp artisan migrate:rollback\n```\n\n### 模型文件\n\n创建模型命令指定命名空间,同时顺便创建数据库迁移使用 `--migration` 或 `-m` 选项\n\n```\nphp artisan make:model Models/Article -m\n```\n\n『约定优于配置』(convention over configuration),也称作按约定编程,这是一种软件设计范式,旨在减少软件开发人员需做决定的数量,获得简单的好处,而又不失灵活性。如果所用工具的约定与你的期待相符,便可省去配置;反之,你可以配置来达到你所期待的方式。\n\n### 创建用户对象\n\n```\nphp artisan tinker\n\n>>> App\\Models\\User::create(['name'=> 'Aufree', 'email'=>'[email protected]','password'=>bcrypt('password')])\n```\n\n## 用户注册\n\n### 显示用户的信息\n\nLaravel 遵从 RESTful 架构的设计原则,将数据看做一个资源,由 URI 来指定资源。\n\n```php\nRoute::resource('users', 'UsersController');\n\n上面代码将等同于:\n\nRoute::get('/users', 'UsersController@index')->name('users.index');\nRoute::get('/users/{user}', 'UsersController@show')->name('users.show');\nRoute::get('/users/create', 'UsersController@create')->name('users.create');\nRoute::post('/users', 'UsersController@store')->name('users.store');\nRoute::get('/users/{user}/edit', 'UsersController@edit')->name('users.edit');\nRoute::patch('/users/{user}', 'UsersController@update')->name('users.update');\nRoute::delete('/users/{user}', 'UsersController@destroy')->name('users.destroy');\n```\n\n### 注册表单\n\n全局辅助函数 old 来帮助我们在 Blade 模板中显示旧输入数据\n\n```\n{{ old('name') }}\n```\n\n### 用户数据验证\n\n为了安全考虑,会让我们提供一个 token(令牌)来防止我们的应用受到 CSRF(跨站请求伪造)的攻击。\n\n```\n{{ csrf_field() }}\n```\n\n会被转换为:\n\n```\n<input type=\"hidden\" name=\"_token\" value=\"fhcxqT67dNowMoWsAHGGPJOAWJn8x5R5ctSwZrAq\">\n```\n\n### 注册失败错误消息\n\n```\ncomposer require \"overtrue/laravel-lang:~3.0\"\n```\n\n`config/app.php` 修改:\n\n```\n 'locale' => 'zh-CN',\n```\n\n### 注册成功\n\n临时保存用户数据的方法 - 会话(Session),并附带支持多种会话后端驱动,可通过统一的 API 进行使用。\n\n```\nsession()->flash('success', '欢迎,您将在这里开启一段新的旅程~');\n\nsession()->get('success')\n```\n\n## 会话管理\n\n### 用户登录\n\n`Auth::check()` 方法用于判断当前用户是否已登录,已登录返回 true,未登录返回 false。\n\n### 记住我\n\n`Auth::attempt()` 方法可接收两个参数,第一个参数为需要进行用户身份认证的数组,第二个参数为是否为用户开启『记住我』功能的布尔值。\n\n## 用户 CRUD\n\n### 更新用户\n\n```html\n<form method=\"POST\" action=\"{{ route('users.update', $user->id )}}\">\n\n// 将转为:\n\n<form method=\"POST\" action=\"http://sample.test/users/1\">\n```\n\n### 权限系统\n\n在 Laravel 中可以使用 授权策略 (Policy) 来对用户的操作权限进行验证,在用户未经授权进行操作时将返回 403 禁止访问的异常。\n\n`redirect()` 实例提供了一个 intended 方法,该方法可将页面重定向到上一次请求尝试访问的页面上,并接收一个默认跳转地址参数,当上一次请求记录为空时,跳转到默认地址上。\n\n```php\nreturn redirect()->intended(route('users.show', [Auth::user()]));\n```\n\n### 列出所有用户\n\n假数据的生成分为两个阶段:\n\n1. 对要生成假数据的模型指定字段进行赋值 - 『模型工厂』\n2. 批量生成假数据模型 - 『数据填充』\n\n数据库的重置和填充操作:\n\n```php\nphp artisan migrate:refresh --seed\n```\n\n## 邮件发送\n\n### 账户激活\n\n1. 用户注册成功后,自动生成激活令牌\n1. 将激活令牌以链接的形式附带在注册邮件里面,并将邮件发送到用户的注册邮箱上\n1. 用户点击注册链接跳到指定路由,路由收到激活令牌参数后映射给相关控制器动作处理\n1. 控制器拿到激活令牌并进行验证,验证通过后对该用户进行激活,并将其激活状态设置为已激活\n1. 用户激活成功,自动登录\n\n使用 log 邮件驱动的方式来调试邮件发送功能,这么做的好处是邮件并不会真正被发送出去,而是会出现在 `storage/logs/laravel.log` 文件中:\n\n```env\nMAIL_DRIVER=log\n```\n\n### 在生产环境中发送邮件\n\nQQ 邮箱的账号设置里开启 `POP3` 和 `SMTP` 服务。\n\n```env\nMAIL_DRIVER=smtp\nMAIL_HOST=smtp.qq.com\nMAIL_PORT=25\[email protected]\nMAIL_PASSWORD=xxxxxxxxx // 密码是我们第一步拿到的授权码\nMAIL_ENCRYPTION=tls\[email protected]\nMAIL_FROM_NAME=SampleApp\n```\n\n## 微博 CRUD\n\n### 显示微博\n\n`diffForHumans()` 该方法的作用是将日期进行友好化处理:\n\n```bash\n>>> $created_at->diffForHumans()\n=> \"17 years ago\"\n```\n","tags":["php"]},{"title":"【Modern PHP】笔记","url":"/2018/05/08/modern-php-reading-notes/","content":"\n又回到 PHP Web 开发,使用 Laravel 框架,重读《Modern PHP》。\n\n<!-- more -->\n\n> PHP 正在重生。\n\n## 特性\n\n### 命名空间\n\n声明命名空间:\n\n```php\n<?php\nnamespace Oreilly\\ModernPHP;\n```\n\n导入和别名:\n\n```php\n<?php\nuse Symfony\\Component\\HttpFoundation\\Response as Res;\n\n$r = new Res('Oops', 400);\n$r->send();\n```\n\nPHP 5.6 开始可以导入函数和常量:\n\n```php\n<?php\nuse func Namespace\\functionName;\nuse constant Namespace\\CONST_NAME;\n\nfunctionName();\necho CONST_NAME;\n```\n\n### 使用接口\n\n接口是两个 PHP 对象之间的契约,其目的不是让一个对象依赖另一个对象的身份,而是依赖另一个对象的能力。\n\n使用接口编写更加灵活,能委托别人实现细节。\n\n### 性状 trait\n\n性状是类的部分实现,可以混入一个或者多个现有的 PHP 类中。性状有两个作用:表明类可以做什么(像是接口);提供模块化实现(像是类)。\n\n如果想让两个无关的 PHP 类具有类似的行为,应该怎么呢?性状就是为了解决这种问题而诞生的。性状能把模块化的实现方式注入多个无关的类中。而且性状还能促进代码的重用。\n\n这与创建一个接口,两个无关的类实现这个接口的优势在于:不用写相同的实现代码,符合 DRY 原则。\n\nPHP 解释器在编译时会把性状复制粘贴到类的定义体中,但是不会处理这个操作引入的不兼容问题。如果性状假定类中有特定的属性和方法(在性状中没有定义),要确保相应的类中有对应的属性和方法。\n\n### 生成器\n\nGenerator 是 PHP 5.5.0 引入的功能。生成器是简单的迭代器,仅此而已。\n\nPHP 生成器不要求类实现 Iterator 接口,从而减轻了类的负担。生成器会根据需求计算并产生要迭代的值。这对应该的性能有重大影响。假如标准的 PHP 迭代器经常在内存中执行迭代操作,这要预先计算出数据集,性能低;此时我们可以使用生成器,即时计算并产出后续值,不占用宝贵的内存资源。\n\nPHP 生成器不能满足所有迭代操作的需求,因为如果不查询,生成器永远不知道下一个要迭代的值是什么,在生成器中无法后退和快进。生成器还是一次性,无法多次迭代同一个生成器。不过,如果需要,可以重建或克隆生成器。\n\nPHP 生成器是 PHP 函数,只不过要在函数中一次或者多次使用 yield 关键字。生成器从不返回值,值产出值。\n\n```php\nfunction myGenerator() {\n yield 'value1';\n yield 'value2';\n yield 'value3';\n}\n\nforeach (myGenerator() as $yieldedValue) {\n echo $yieldedValue, PHP_EOL;\n}\n\nvalue1\nvalue2\nvalue3\n```\n\n使用生成器处理 CSV:\n\n```php\nfunction getRows($file) {\n $handle = fopen($file, 'rb');\n if ($handle === false) {\n throw new Exception();\n }\n while (feof($handle) === false) {\n yield fgetcsv($handle);\n }\n fclose($handle);\n}\n\nforeach (getRows('data.csv') as $row) {\n print_r($row);\n}\n```\n\n### 闭包\n\n理论上讲,闭包和匿名函数是不同的概念。不过,PHP 将其视作相同的概念。\n\n```php\n$closure = function ($name) {\n return sprintf('Hello %s', $name);\n}\n\necho $closure(\"Josh\");\n```\n\n我们之所以能调用 \\$closure 变量,是因为这个变量的值是一个闭包,而且闭包对象实现了 `__invoke()` 魔术方法。只要变量名后有(),PHP 就会查找并调用 `__invoke()` 方法。\n\nPHP 闭包常被当做函数和方法的回调使用。\n\n```php\n$numbersPlusOne = array_map(function ($number) {\n return $number + 1;\n}, [1,2,3]);\n\nprint_r($numbersPlusOne);\n\n// [2,3,4]\n```\n\n在有闭包之前,只能单独创建具名函数,然后使用名称引用那个函数:\n\n```php\n$numbersPlusOne = array_map('incrementNumber', [1,2,3]);\n// 如果只需要使用一次回调,没必要单独定义。把闭包当成回调使用,写出的代码更整洁、更清晰。\n```\n\n使用 use 关键字附加闭包状态:\n\n```php\nfunction enclosePerson($name) {\n return function ($doCommand) use ($name) {\n return sprintf('%s, %s', $name, $doCommand);\n }\n}\n\n// 把字符串 Clay 封装到闭包里\n$clay = enclosePerson('Clay');\n\n// 传入参数,调用闭包\necho $clay('get me sweet tea!');\n\n// \"Clay, get me sweet tea!\"\n```\n\n具名函数 enclosePerson() 有个名为 $name 的参数,这个函数返回一个闭包对象,而且这个闭包封装了 $name 参数。即便返回的闭包对象跳出了 enclosePerson() 函数的作用域,它也会记住 $name 参数的值,因为 $name 变量仍在闭包中。\n\nPHP 闭包是对象。闭包对象的默认状态没什么用,不过有一个 `__invoke()` 魔术方法和 `bindTo()` 方法。\n\n### Zend OPcache\n\n字节码缓存能存储预先编译好的 PHP 字节码。这意味着,请求 PHP 脚本时,PHP 解释器不用每次都读取、解析和编译 PHP 代码。\n\n### 内置的 HTTP 服务器\n\n启动这个服务器:\n\n```php\nphp -S localhost:4000\n\n// 让 PHP Web 服务器监听所有接口\nphp -S 0.0.0.0:4000\n```\n\n## 标准\n\n### PSR 是什么\n\nPHP Standards Recommendation.\n\n- PSR-1 基本的代码风格\n- PSR-2 严格的代码风格\n- PSR-3 日志记录器接口\n- PSR-4 自动加载\n\n## 组件\n\n### 查找组件\n\n- [Awesome PHP](https://github.com/ziadoz/awesome-php#text-editors-and-ides)\n- [Packagist](https://packagist.org)\n\n## 良好实践\n\n### 流\n\n流式数据的种类各异,每种类型需要独特的协议,以便读写数据。称这些协议为流封装协议。\n\n1. 开始通信\n2. 读取数据\n3. 写入数据\n4. 结束通信\n\n指定协议和目标的方法是使用流标识符:\n\n`<scheme>://<target>`\n\n使用 HTTP 流封装协议创建了一个与 Flickr API 通信的 PHP 流:\n\n```php\n<?php\n$json = file_get_contents(\n 'http://api.flickr.com/services/feeds/photos_public.gne?format=json'\n);\n```\n\n不要误以为这是普通的网页 URL,file_get_contents() 函数的字符串参数其实是一个流标识符。http 协议会让 PHP 使用 HTTP 流封装协议。在这个参数中,http 之后是流的目标。很多 PHP 开发者不知道普通的 URL 其实是 PHP 流封装协议标识的伪装。\n\n我们使用 file_get_contents() fopen() fwrite() 和 fclose() 函数读写文件系统。因为 PHP 默认使用的流封装协议是 file://,使用我们很少认为这些函数使用的是 PHP 流。\n\n隐式使用 file:// 流封装协议:\n\n```php\n$handle = fopen('/etc/hosts', 'rb');\nwhile (feof($handle) !== ture) {\n echo fgets($handle);\n}\nfclose($handle);\n```\n\n显示使用 file:// 流封装协议:\n\n```php\n...\n$handle = fopen('file://etc/hosts', 'rb');\n...\n```\n\n我们通常会省略 file:// 封装协议,这是 PHP 使用的默认值。\n\n编写命令行脚本的 PHP 开发者会感激 php:// 流封装协议。这个流封装协议的作用是与 PHP 脚本的标准输入、标准输出和标准错误文件描描述符通信。\n\nphp://stdin 只读 PHP 流,其中的数据来自标准输入。例如,接收命令行传入脚本的信息。\n\nphp://stdout 把数据写入当前的缓冲区。这个流只能写,无法读或寻址。\n\nphp://memory 从系统内存中读取数据,或者把数据写入系统内存。缺点是,可用内存是有限的。使用 php://temp 流更安全。\n\nphp://temp 和 php://memory 类似,不过没有可以内存时,PHP 会把数据写入临时文件。\n\n### 错误和异常\n\n提到了 [Monolog](https://github.com/Seldaek/monolog) 记录日志。\n\n## 调优\n\n### 内存\n\n_一共能分配给 PHP 多少内存?_\n\nLinode 2GB 的 sever 留 512MB 给 PHP。\n\n_单个 PHP 进程平均消耗多少内存?_\n\n使用 top 命令查看。一般 PHP 进程消耗 5 ~ 20MB 内存。\n\n_能负担的起多少个 PHP-FPM 进程?_\n\n假设 PHP 分配了 512MB 内存,每个 PHP 平均消耗 15MB 内存,从而确定能负担的起 34 个进程。\n\n压力测试工具:\n\n- [ab - Apache HTTP server benchmarking tool](https://httpd.apache.org/docs/2.4/programs/ab.html)\n- [Siege](https://www.joedog.org/siege-home/)\n\n### Zend OPcache\n\n```php\nopcache.memory_consumption = 64\n# 为操作码缓存分配的内存量(单位 MB)。\n\nopcache.interned_strings_buffer = 16\n# 用来存储驻留字符串的内存量(单位 MB,默认 4MB)。\n\nopcache.max_accelerated_file = 4000\n# 操作码缓存中最多能存储多少个 PHP 脚本。这个值一定比 PHP 应用中的文件数量大。\n\nopcache.validate_timestamps = 1\n# 为 1 时,一段时间后 PHP 会检查 PHP 脚本的内容是否变化。检查的时间间隔由 revalidate_freq 指定。\n# 开发环境设为 1,在生成环境中为 0。\n\nopcache.revalidate_freq = 0\n# 设置多久检查一次 PHP 脚本的内容是否有变化。\n\nopcache.fast_shutdown = 1\n# 这么设置能让操作码使用更快的停机步骤,把对象析构和内存释放交给 Zend Engine 的内存管理器完成。\n```\n\n### 文件上传\n\n```php\nfile_uploads = 1\nupload_max_filesize = 10M\nmax_file_uploads = 3\n```\n\n如果需要上传非常大的文件,还要调整 nginx 虚拟主机配置中的 client_max_body_size 设置。\n\n### 会话处理\n\n```php\nsession.save_handler = 'memcached'\nsession.save_path = '127.0.0.2:11211'\n```\n\n### 缓冲输出\n\n```php\noutput_buffering = 4096\nimplicit_flush = false\n```\n\n确保使用的值是 4(32 位系统)或者 8(64 位系统)的倍数。\n\n### 真实路径缓存\n\nrealpath cache,PHP 会缓存应用使用的文件路径,这样每次包含或者导入文件时就无需不断搜索包含路径了。\n\n```php\nrealpath_cache_size = 64k\n```\n\n## 部署\n\n提到了 [Capistrano](http://capistranorb.com/) 待研究。\n\n## 测试\n\n- PHPUnit\n- Xdebug\n- 使用 Travis CI 持续测试\n\n## 分析\n\n- XHProf 较新的 PHP 应用分析器\n- XHGUI\n- New Relic\n- Blackfire\n\n## HHVM 和 Hack\n\nHip-Hop Virtual Machine.\n\nHack 是一门建立在 PHP 之上的编程语音,引入了静态类型,新的数据结构和额外的接口,同时还能向后兼容现有的动态类型 PHP 代码。\n\n动态类型和静态类型,二者之间的区别在于何时检查 PHP 类型。动态类型在运行时检查类型,而静态类型在编译时检查类型。\n\n-- EOF --\n","tags":["php"]},{"title":"PhpStorm 使用经验","url":"/2018/05/05/phpstorm-using-experience/","content":"\n> 文章基本适用于 [Jetbrains 全家桶](https://www.jetbrains.com/)。\n\n## 主题配置\n\n自己主要使用下面两个插件,安装并启用:\n\n- Material Theme UI\n- Atom Material Icons\n\n<!-- more -->\n\n还有个 `One Dark theme` 但是 `Material Theme UI` 已经包含这个主题。\n\n配置:\n\n`Preferences > Appearance & Behavior > Appearance` 下,右侧配置:`Theme: Darcula`,勾选 `User custom font: .AppleSystemUIFont` `Size: 18`。\n\n`Preferences > Editor > Font` 下,右侧配置:`Font: Menlo` `Size: 18` `Line spacing: 1.2`。\n\n`Preferences > Editor > Color Scheme` 下,右侧配置:`Seheme: Atom One Dark(Material)` 点击右侧的三个点 `Duplicate` 复制一份。\n\n`Preferences > Editor > Color Scheme > General` 下,右侧配置:\n\n- Editor > Gutter background,右侧 `Background: 292929`(设置行号背景色)\n- Editor > Vertical Scrollar > Thumb 与 Thumb while scrolling,右侧 `Background: 305599C0`(设置垂直滑块色)\n- Editor > Text > Default text,右侧 `Background: 292929`(设置面板背景色)\n\n## 优秀插件推荐\n\n- PHP Annotation\n- Php Inspections (EA Ultimate)\n- String Manipulation\n\n框架支持:\n\n- Laravel\n- Swoole IDE Helper\n- Symfony Support\n- Yii2 Support\n\n## 技巧\n\n### 围绕选择输入\n\n设置选择了一个词后,再按单引号或双引号,将选中的单词用引号括起来。\n\n`Preferences` 中搜索 `Surround Selection on typing quote or brace` 将其勾选(`Editor > General > Smart Keys` 下)。\n\n### 关闭文档提示\n\n鼠标放在方法上会出现文档提示,想关闭。\n\n`Preferences` 中搜索 `Show quick documentation on mouse move` 取消勾选(`Editor > Code Editing` 下)。\n\n### 使用 PHP-CS-Fixer\n\n> [The PHP Coding Standards Fixer](https://github.com/FriendsOfPHP/PHP-CS-Fixer) (PHP CS Fixer) tool fixes your code to follow standards.\n\n工作环境:MacBook。\n\n打开 PhpStorm `Preferences > Tools > External Tools` 添加:\n\n![180416-use-php-cs-fixer-in-phpstorm-001](https://user-images.githubusercontent.com/9289792/88664953-715fb100-d110-11ea-970e-9dcb72945ebb.png)\n\n- Program: `/usr/local/bin/php-cs-fixer`\n- Arguments: `--verbose fix \"$FileDir$/$FileName$\" --dry-run --rules=@PSR1,@PSR2,@Symfony`(Note that previous verions of PHP-CS-Fixer used --levels instead of --rules. 未找到)\n- Working directory: `$ProjectFileDir$`\n- 我取消勾选了 `Open console for tool output`,可以不输出日志信息\n\n为了方便使用,保存文件时就可以格式化,设置快捷键 `Preferences > Keymap > Macros`:\n\n![180416-use-php-cs-fixer-in-phpstorm-002](https://user-images.githubusercontent.com/9289792/88665170-c996b300-d110-11ea-8acf-62dad3694f2d.png)\n\n设置 php-cs-fix 单独的快捷键 `Preferences > Keymap > External Tools`:\n\n![180416-use-php-cs-fixer-in-phpstorm-003](https://user-images.githubusercontent.com/9289792/80202900-e5de0c00-8658-11ea-826f-b4d058fa2209.png)\n\n### 关闭不常用的插件\n\n`Preferences > Plugins > Installed` 向下滚动,`Bundled` 中有不少预装但不常用的可以禁掉。\n\n## 遇到过的一些问题\n\n### 文件类型错误\n\n一个文件被新建后,明明扩展名没有错,但是却没有语法高亮,删除文件后也不解决问题。\n\n解决办法:`Editor > File Types` 找 `Text` 将里面涉及的文件删除掉。\n\n- [phpstorm 文件类型错误](https://segmentfault.com/q/1010000004495692)\n\n### Undefined function XXX\n\n出现 PHP 的原生方法未定义的警告。\n\n解决方法:`File > Invalidate Caches / Restart`\n\n### Typo: In word XXX\n\n提示单词拼写错误,但是其中没有问题,比如全拼的名字。\n\n解决方法:option + enter -> Save to dictionary\n\n- [Spellchecking | jetbrains](https://www.jetbrains.com/help/phpstorm/spellchecking.html)\n\n### warning: Multiple definitions exists for class\n\nI resolved this by going to `Preferences-> Languages & Frameworks-> PHP`; and then under `Include path`, remove the conflicting path. (In my case a package reference in the vendor directory to a package I was developing inside my Laravel project)\n\n## References\n\n- [打造漂亮的 PhpStorm 界面](https://laravel-china.org/articles/4172/create-beautiful-phpstorm-interface)\n- [大牛们的 PHPstorm 使用技巧和建议](http://www.pilishen.com/posts/phpstorm-tips-and-tricks)\n- [Use PHP-CS-Fixer in PHPStorm](https://gist.github.com/nienkedekker/3ddb9ece42233698c0e3f3e42cf1ff34)\n","tags":["php","phpstorm"]},{"title":"使用 Let's Encrypt 通配符证书","url":"/2018/04/26/lets-encrypt-wildcard-certificates/","content":"\n一直在使用 [Let's Encrypt](https://letsencrypt.org/) 的免费 SSL 证书,但是一直没做笔记。今天看到 Let's Encrypt 支持了通配符证书(Wildcard Certificates),也就是说二级子域名和主域名可以共用一个证书。\n\n<!-- more -->\n\n## 申请证书\n\n```bash\n# 下载证书申请客户端\ncd /opt\ngit clone https://github.com/certbot/certbot\ncd /opt/certbot\n\n# 注意通配符并不包含主域名,所以要配置两个\n./certbot-auto certonly -d *.zyf.im -d zyf.im --manual --preferred-challenges dns --server \"https://acme-v02.api.letsencrypt.org/directory\"\n```\n\n`-preferred-challenges dns` 使用 DNS 方式校验域名所有权,所以会遇到:\n\n```bash\n-------------------------------------------------------------------------------\nPlease deploy a DNS TXT record under the name\n_acme-challenge.zyf.im with the following value:\n\nYZ2unEViXH8nYZ2unEViIbW52LhIEViIbW52Lh\n\nBefore continuing, verify the record is deployed.\n-------------------------------------------------------------------------------\nPress Enter to Continue\n```\n\n要在域名服务商那里将 `_acme-challenge.zyf.im` 配置 DNS TXT 记录,从而校验域名所有权。\n\n使用 `host -t txt _acme-challenge.zyf.im` 验证记录是否已经生效,如果看到对应的值,按 Enter 继续。\n\n```bash\n...\n - Congratulations!\n...\n```\n\n生成的证书会放置在 `/etc/letsencrypt/live/`,可以使用 openssl 验证一下:\n\n```bash\nopenssl x509 -in /etc/letsencrypt/live/zyf.im/cert.pem -noout -text | grep zyf.im\n\nSubject: CN=zyf.im\n DNS:*.zyf.im DNS:zyf.im\n```\n\n## 配置证书\n\n```conf\nserver {\n listen 80;\n server_name zyf.im design.zyf.im;\n return 301 https://$host$request_uri;\n}\n```\n\n```conf\nserver {\n listen 443 ssl http2;\n listen [::]:443 ssl http2;\n\n server_name design.zyf.im;\n root /project/imzyf;\n index index.html index.htm;\n\n ssl_certificate /etc/letsencrypt/live/zyf.im/fullchain.pem;\n ssl_certificate_key /etc/letsencrypt/live/zyf.im/privkey.pem;\n ssl_session_timeout 1d;\n\n # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits\n ssl_dhparam /etc/nginx/ssl/dhparam.pem;:q!\n\n # intermediate configuration. tweak to your needs.\n ssl_protocols TLSv1 TLSv1.1 TLSv1.2;\n ssl_ciphers '...';\n ssl_prefer_server_ciphers on;\n\n # HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)\n add_header Strict-Transport-Security max-age=15768000;\n\n # OCSP Stapling ---\n # fetch OCSP records from URL in ssl_certificate and cache them\n ssl_stapling on;\n ssl_stapling_verify on;\n}\n```\n\n## 证书续期\n\n证书的有效期只有三个月,所以要利用 `crontab` 定时续期:\n\n```conf\n30 2 * * 1 /opt/certbot/certbot-auto renew >> /var/log/le-renew.log 2>&1\n35 2 * * 1 /etc/init.d/nginx reload\n```\n\n## References\n\n- [配置使用免费的通配符证书 | laisky](https://blog.laisky.com/p/letsencrypt/)\n- [我是如何将网站全站启用 Https 的?| freehao123](https://www.freehao123.com/quanzhan-https-ssl/)\n\n-- EOF --\n","tags":["nginx"]},{"title":"Docker UFW 失效","url":"/2018/04/24/docker-ufw-not-work/","content":"\n今日遇到 Docker 中的项目绕过了宿主机 UFW 的配置,可以被任意 IP 访问,甚是奇怪。查找资料发现:\n\n如果你在 Linux 使用 Docker,很可能你的系统防火墙降级为 Uncomplicated Firewall (UFW)。如果是这样的话,你有一点可能不知道,Docker 和 UFW 的组合带来了一些安全问题。为什么呢?因为 Docker 实际上绕过了 UFW 并直接修改了 iptables,所以一个容器可以绑定一个端口。这就意味着,所有你设置的 UFW 规则都将在 Docker 容器中失效。\n\n如何修复:\n\n```bash\nsudo vi /etc/default/docker\n\n# add the following line:\n\nDOCKER_OPTS=\"--iptables=false\"\n```\n\nRestart the docker daemon:\n\n```bash\nsudo systemctl restart docker\n\n# or\n\ndocker/etc/init.d/docker restart\n```\n\n> When a problem arises, it only takes a bit of digging to discover the solution was already there, waiting for you to make it so. Don't let this issue with Docker stop you from using this incredible technology.\n\n相关阅读:\n\n- [Ubuntu 下使用 UFW 管理防火墙服务 | Yifans_Z](/2016/10/10/manage-iptables-using-ufw-in-ubuntu/)\n- [docker and ufw serious problems | moby](https://github.com/moby/moby/issues/4737)\n- [无视系统防火墙的 docker | binss](https://www.binss.me/blog/docker-pass-through-system-firewall/)\n\n## References\n\n- [How to fix the Docker and UFW security flaw | techrepublic](https://www.techrepublic.com/article/how-to-fix-the-docker-and-ufw-security-flaw/)\n\n-- EOF --\n","tags":["docker"]},{"title":"中文文案排版规范","url":"/2018/04/08/chinese-copywriting-style-guide/","content":"\n2019-12-04 更新:参考文档 [ruanyf/document-style-guide | github](https://github.com/ruanyf/document-style-guide)\n\n2019-05-09 更新:参考文档\n\n- [sparanoid/chinese-copywriting-guidelines](https://github.com/sparanoid/chinese-copywriting-guidelines)\n- [Ant Design Copywriting](https://ant.design/docs/spec/copywriting)\n\n---\n\n> 原文见于:Coding 开放平台,因为原始链接失效了,所以进行转载发布。\n\n本文旨在帮助提升大家的文案排版素养,形成良好的排版习惯。\n\n<!-- more -->\n\n1. 名词\n 1.1. 专有名词使用正确的大小写\n 1.2. 错误及不正式的缩写\n\n2. 空格\n 2.1. 中英文混排时需要增加空格以提高可读性\n 2.2. 中文和数字混排时需要增加空格以提高可读性\n 2.3. 数字与单位混排时时需要增加空格以提高可读性\n 2.4. 锚文本链接前后需要增加空格以提高可读性\n\n3. 标点\n 3.1. 不重复使用标点符号\n 3.2. 尽量避免使用感叹号\n\n4. 全角和半角\n 4.1. 中文使用全角中文标点\n 4.2. 数字使用半角字符\n 4.3. 完整的英文整句、特殊名词使用半角标点\n 4.4. 横排文稿使用弯引号\n\n## 名词\n\n### 专有名词使用正确的大小写\n\nCODING 是国内专业的一站式云端软件服务平台,旗下有两大产品:云端软件开发协作平台 —— Coding.net ,基于云技术的软件外包服务平台 —— 码市。\n\n“Coding” 狭义上指代 Coding.net (云端软件开发协作平台),广义上指代 Coding.net 和码市构成的一站式云端软件服务平台。\n\n正式用法:\n\n```txt\nCODING 旗下的产品有:Coding.net,码市。\n```\n\n非正式用法:\n\n```txt\nCoding 旗下的产品有:Coding, 码市。\n```\n\n错误用法:\n\n```txt\ncoding 旗下的产品有:coding, 码市。\n```\n\n### 错误及不正式的缩写\n\n正确示例:\n\n```txt\n我们需要熟悉 JavaScript、HTML5,至少理解一种框架(如 Backbone.js、AngularJS、React 等)的前端开发者。\n```\n\n错误示例:\n\n```txt\n我们需要熟悉 Js、H5,至少理解一种框架(如 backbone、angular、RJS 等)的 FED。\n```\n\n## 空格\n\n“有研究显示,打字的时候不喜欢在中文和英文之间加空格的人,感情路都走得很辛苦,有七成的比例会在 34 岁的时候跟自己不爱的人结婚,而其余三成的人最后只能把遗产留给自己的猫。毕竟爱情跟书写都需要适时地留白。\n\n与大家共勉之。”——vinta/paranoid-auto-spacing\n\n### 中英文混排时需要增加空格以提高可读性\n\n正确示例:\n\n```txt\n在使用 Coding 之前需要注册 Coding。\n```\n\n错误示例:\n\n```txt\n在使用Coding之前需要注册Coding。\n\n在使用 Coding之前需要注册 Coding。\n```\n\n例外:“豆瓣 FM”等产品名词,按照官方所定义的格式书写。\n\n### 中文和数字混排时需要增加空格以提高可读性\n\n正确示例:\n\n```txt\nCoding 目前有 30 万注册开发者。\n```\n\n错误示例:\n\n```txt\nCoding 目前有30万注册开发者。\n\nCoding 目前有 30万注册开发者。\n```\n\n### 数字与单位混排时时需要增加空格以提高可读性\n\n正确示例:\n\n```txt\nCoding 用户 clone 代码时的速度可达到 10 Mbps。\n```\n\n错误示例:\n\n```txt\nCoding 用户 clone 代码时的速度可达到 10Mbps。\n\nCoding 用户 clone 代码时的速度可达到10 Mbps。\n```\n\n例外:`°(度)` `/` `%` 与数字之间不需要增加空格:\n\n正确示例:\n\n```txt\n新 MacBook Pro 有 15% 的 CPU 性能提升。\n```\n\n错误示例:\n\n```txt\n新 MacBook Pro 有 15 % 的 CPU 性能提升。\n```\n\n### 锚文本链接前后需要增加空格以提高可读性\n\n正确示例:\n\n[码市](https://mart.coding.net/) 为不同的行业领域解决了软件开发的需求。\n\n错误示例:\n\n[码市](https://mart.coding.net/)为不同的行业领域解决了软件开发的需求。\n\n## 标点\n\n### 不重复使用标点符号\n\n正确示例:\n\n```txt\n码市已完成 3000 万元的交易额!\n```\n\n错误示例:\n\n```txt\n码市已完成 3000 万元的交易额!!!\n```\n\n### 尽量避免使用感叹号\n\n推荐示例:\n\n```txt\n码市已完成 3000 万元的交易额。\n```\n\n不推荐示例:\n\n```txt\n码市已完成 3000 万元的交易额!\n```\n\n## 全角和半角\n\n### 中文使用全角中文标点\n\n正确示例:\n\n```txt\n新建一个 Coding 帐户(如果您还没有):请访问 Coding.net,点击注册按钮(右上角),并按屏幕上显示的说明操作。\n```\n\n错误示例:\n\n```txt\n新建一个 Coding 帐户 (如果您还没有):请访问 Coding.net,点击注册按钮(右上角),并按屏幕上显示的说明操作.\n```\n\n### 数字使用半角字符\n\n正确示例:\n\n```txt\n码市已完成 3000 万元的交易额。\n```\n\n错误示例:\n\n```txt\n码市已完成 3000 万元的交易额。\n```\n\n### 完整的英文整句、特殊名词使用半角标点\n\n正确示例:\n\n```txt\n《Hackers & Painters: Big Ideas from the Computer Age》这本书很好看。\n\n“Do things that Don’t Scale” 显然已成为创业公司的口头禅。\n```\n\n错误示例:\n\n```txt\n《Hackers&Painters:Big Ideas from the Computer Age》这本书很好看。\n\n“Do things that Don't Scale” 显然已成为创业公司的口头禅。\n```\n\n### 横排文稿使用弯引号\n\n正确示例:\n\n```txt\nCODING CEO 说:“CODING 的愿景是 ‘Coding Anytime Anywhere’。”\n```\n\n错误示例:\n\n```txt\nCODING CEO 说:「CODING 的愿景是 『Coding Anytime Anywhere』。」\n```\n\n注意:[《中华人民共和国国家标准标点符号用法》](http://www.moe.gov.cn/ewebeditor/uploadfile/2015/01/13/20150113091548267.pdf) 指出该标准适用于汉语书面语(包括汉语与外语混合排版的汉语部分),在大陆简体中文和繁体中文均属于汉语文字,横排文稿均使用弯引号,而竖排文稿改用双引号 “﹄” “﹃” 和单引号 “﹂” “﹁”。\n\n## 结语\n\n从各个小标题可以看出:“以提高可读性” 是排版的第一要义。抱着这个思路去编写文档,这些规范就不难理解了。\n\n这种规定到空格级别的格式有意义吗?在我看来这种对文档排版的苛求体现了个人行事风格,如果团队有此要求,就更应该严格遵守。\n\n-- EOF --\n"},{"title":"Swift 初始化","url":"/2018/03/31/swift-init/","content":"\n因为自己是直接从 Swift 进入的 iOS 开发,Swift 与 Objective-C 初始化的对比就不多提了。感觉上 Swift 初始化的方式像 Java,自己也只这样套着 Java 去理解,但也发现了不相同的地方。\n\n## 初始化顺序\n\n```swift\nclass Blog: NSObject {\n let param: String\n\n override init() {\n }\n}\n```\n\n这里有条错误 `error: property 'self.param' not initialized at implicitly generated super.init call` 说明:`param` 参数没有在隐式生成 `super.init` 调用之前完成初始化。\n\nSwift 中并不是不调用 `super.init` 而是为了方便开发者由编译器完成了这一步,但是要求调用 `super.init` 之前要完成成员变量的初始化。\n\n<!-- more -->\n\n```swift\nclass Blog: NSObject {\n let param: String\n\n override init() {\n param = \"swift init\"\n }\n}\n```\n\n对于需要修改父类中成员变量值,我们需要在调用 `super.init` 之后再进行修改:\n\n```swift\nclass Cat {\n var name: String\n\n init() {\n name = \"cat\"\n }\n}\n\nclass Tiger: Cat {\n let power: Int\n\n override init() {\n power = 10\n super.init()\n name = \"tiger\"\n }\n}\n```\n\nSwift 中类的初始化顺序:\n\n1. 初始化自己的成员变量,必须\n2. 调用父类初始化方法,如无需第三步,则这一步也可省略\n3. 修改父类成员变量,可选\n\n补充说明:\n\n- `let` 声明的常量是可以在初始化方法中进行赋值,Swift 中的 init 方法只会被调用一次,这与 Objective-C 不同\n- 即使成员变量是可选类型,如:`let power: Int?` 仍然是需要进行初始化的,`var power: Int?` 则可以不用\n\n## 关键字\n\n例子 1:\n\n```swift\nclass CustomView: UIView {\n let param: Int\n\n override init() { // error 1\n self.param = 1\n super.init() // error 2\n }\n} // error 3\n```\n\n将父类从 `NSObject` 修改为 `UIView`,竟然收到 3 条错误:\n\n1. initializer does not override a **designated** initializer from its superclass\n2. must call a designated initializer of the superclass 'UIView'\n3. **'required'** initializer 'init(coder:)' must be provided by subclass of 'UIView'\n\n例子 2:\n\n```swift\nclass CustomView: UIView {\n convenience init(param: Int, frame: CGRect) {\n super.init(frame: frame) // error\n }\n\n required init?(coder aDecoder: NSCoder) {\n fatalError(\"init(coder:) has not been implemented\")\n }\n}\n```\n\nerror:\n\n- **convenience** initializer for 'CustomView' must delegate (with 'self.init') rather than chaining to a superclass\n\n上面俩个例子出现了 3 个关键字:`designated`、`convenience` 和 `required`。\n\n### designated\n\nSwift 定义了两种类初始化器类型,用来保证所有成员属性能够获得一个初始化值。即 designated initializers [i'niʃəlaizə] 和 convenience initializers。\n\n> Designated initializers are the primary initializers for a class. A designated initializer fully initializes all properties introduced by that class and calls an appropriate superclass initializer to continue the initialization process up the superclass chain.\n\n1. primary initializers:designated initializers 是一个类的主初始化器,理论上来说是一个类初始化的必经之路。(不同的初始化路径可能调用不同的 designated initializers)\n2. fully initializes all properties:必须在 designated initializers 中完成所有成员属性的初始化\n3. calls an appropriate superclass initializer:需要调用合适的父类初始化器完成初始化,不能随意调用\n\n_在 Swift 中 designated initializers 的写法和一般的初始化方法无异_,Sample 1 中,我们试图去 override init,可以理解为我们就是在 override 一个 designated initializers,然后我们收到了错误 Initializer does not override a designated initializer from its superclass,可见我们并没有找到合适的 designated initializers,我们进入父类 UIView,可以看到下面两个初始化方法:\n\n```swift\npublic init(frame: CGRect)\npublic init?(coder aDecoder: NSCoder)\n```\n\n原来,这两个类才是父类的 designated initializers,那我们改改试试:\n\n```swift\nclass CustomView: UIView {\n let param: Int\n\n override init(frame: CGRect) { // error 1 fixed\n self.param = 1\n super.init(frame: frame) // error 2 fixed\n }\n} // error 3\n```\n\nerror 1 fixed 由此可见:我们去 override 一个不是 designated initializers 的初始化器时,是不满足定义中所说的 primary initializers,这就可能导致这个初始化器不被执行,成员变量没有初始化,这样创建的“半成品”实例可能存在一些不安全的情况。\n\n第二条 fully initializes all properties,这点我们并没有犯错,因为我们已经初始化了 CustomView 类中引入的 param 变量。\n\n第三条 calls an appropriate superclass initializer 很明显就对应了 error 2,我们 override init(frame:),那我们就必须调用对应的父类初始化方法。\n\nerror 3 提示我们 init(coder:) 是一个 'required' initializer,子类必须提供\n\n### required\n\n> Write the required modifier before the definition of a class initializer to indicate that every subclass of the class must implement that initializer.\n\n通过添加 required 关键字强制子类对某个初始化方法进行重写。\n\n```swift\nclass CustomView: UIView {\n let param: Int\n\n override init(frame: CGRect) { // error 1 fixed\n self.param = 1\n super.init(frame: frame) // error 2 fixed\n }\n\n required init?(coder aDecoder: NSCoder) {\n fatalError(\"init(coder:) has not been implemented\")\n }\n} // error 3 fixed\n```\n\nerror 3 fixed 插入的这个方法很奇怪,方法体里直接写 `fatalError(\"init(coder:) has not been implemented\")`,那岂不是走到这里就 crash 了?\n\ndesignated initializers 是一个类的主初始化器,理论上来说是一个类初始化的必经之路(不同的初始化路径可能调用不同的 designated initializers),其实,这个 `init(coder:)` 与 `init(frame: frame)` 就是不同的初始化路径,当我们使用 xib 方式初始化一个 view 时,就会走到 init(coder:)。此时,如果我们没有真正实现这个方法,就会出现 fatal crash。\n\n完整初始方案:\n\n```swift\nclass CustomView: UIView {\n let param: Int\n\n override init(frame: CGRect) {\n self.param = 1\n super.init(frame: frame)\n }\n\n required init?(coder aDecoder: NSCoder) {\n self.param = 1\n super.init(coder: aDecoder)\n }\n}\n```\n\n### convenience\n\n> Convenience initializers are secondary, supporting initializers for a class. You can define a convenience initializer to call a designated initializer from the same class as the convenience initializer with some of the designated initializer’s parameters set to default values. You can also define a convenience initializer to create an instance of that class for a specific use case or input value type.\n\nconvenience initializers 是对类初始化方法的补充,用于为类提供一些快捷的初始化方法,可以不创建这类方法,但如果创建了,就需要遵循原则:call a designated initializer from the same class,也就是说要调用该类自己的 `designated initializer`,那么我们应该 `override init(frame:)` ,然后修改为:\n\n```swift\nclass CustomView: UIView {\n convenience init(param: Int, frame: CGRect) {\n self.init(frame: frame) // error fixed\n }\n\n override init(frame: CGRect) {\n super.init(frame: frame)\n }\n\n required init?(coder aDecoder: NSCoder) {\n fatalError(\"init(coder:) has not been implemented\")\n }\n}\n```\n\n然后对成员变量 `param` 赋值:\n\n```swift\nclass CustomView: UIView {\n var param: Int\n\n convenience init(param: Int, frame: CGRect) {\n self.param = param // error\n self.init(frame: frame)\n }\n\n override init(frame: CGRect) {\n super.init(frame: frame) // error\n }\n\n required init?(coder aDecoder: NSCoder) {\n fatalError(\"init(coder:) has not been implemented\")\n }\n}\n```\n\n来个两个错误:\n\n- Use of 'self' in property access 'param' before self.init initializes self\n- Property 'self.param' not initialized at super.init call\n\n第二个错误我们清楚,是需要在调用 super.init 之前初始化本类成员属性。\n\n第一个错误其实,这是 Swift 编译器提供的安全检查,文档原文如下:\n\n> A convenience initializer must delegate to another initializer before assigning a value to any property (including properties defined by the same class). If it doesn’t, the new value the convenience initializer assigns will be overwritten by its own class’s designated initializer.\n\n原来 Swift 防止 convenience initializers 中赋值之后又被该类自己的 designated initializer 覆盖而做了检查。因此,正确的方式应该是调用该类的其他初始化方法之后再修改属性值,最终修改如下:\n\n```swift\nclass CustomView: UIView {\n var param: Int\n\n convenience init(param: Int, frame: CGRect) {\n self.init(frame: frame)\n self.param = param // error fixed\n }\n\n override init(frame: CGRect) {\n self.param = 0 // error fixed\n super.init(frame: frame)\n }\n\n required init?(coder aDecoder: NSCoder) {\n self.param = 0\n super.init(coder: aDecoder)\n }\n}\n```\n\n### 小结\n\n1. 子类中初始化方法必须覆盖全部初始化路径,以保证对象完全初始化\n2. 子类中 `designated initializer` 必须调用父类中对应的 `designated initializer`,以保证父类也能完成初始化\n3. 子类中如果重写父类中 `convenience initializer` 所需要的全部 `init` 方法,就可以在子类中使用父类的 `convenience initializer` 了\n4. 子类如果没有定义任何 `designated initializer`,则默认继承所有父类的 `designated initializer` 及 `convenience initializer`\n5. 子类中必须实现的 `designated initializer`,可以通过添加 `required` 关键字强制子类重写其实现,以保证依赖该方法的 `convenience initializer` 始终可以使用\n6. `convenience initializer` 必须调用自身类中的其他初始化方法,并在最终必须调用一个 `designated initializer`\n7. 在构造器完成初始化之前, 不能调用任何实例方法,或读取任何实例属性的值,`self` 本身也不能被引用\n\n## 可失败初始化器\n\n可失败初始化器 `Failable Initializers` ,即可能返回 `nil` 的初始化方法。\n\n> A failable initializer creates an _optional value_ of the type it initializes. You write _return nil within a failable initializer_ to indicate a point at which initialization failure can be triggered.\n\n就是将初始化返回值变成 optional value,并在不满足初始化条件的地方 return nil。通过调用处判断是否有值即可知道是否初始化成功。\n\n```swift\nclass Product {\n let name: String\n init?(name: String) {\n if name.isEmpty { return nil }\n self.name = name\n }\n}\n\nclass CartItem: Product {\n let quantity: Int\n init?(name: String, quantity: Int) {\n if quantity < 1 { return nil }\n self.quantity = quantity\n super.init(name: name)\n }\n}\n```\n\nCartItem 类的初始化方法先对传入参数 `quantity` 的值进行判断,小于 `1` 则为无效参数,然后 `return nil`(初始化失败),大于或等于 `1` 则继续调用父类 `Product` 的初始化方法,再次判断传入参数 `name`,为空则 `return nil`(初始化失败),否则继续初始化。\n\n总的来说,可失败初始化器的设定,是在保证安全性的基础上提供了逻辑上更清晰的初始化方式。`Failable Initializers` 所有的结果都将是 `T?` 类型,通过 `Optional Binding` 方式,我们就能知道初始化是否成功,并安全地使用它们了。\n\n## References\n\n- [从 Swift 初始化说起](http://huizhao.win/2016/11/13/swift-init/)\n- [The Swift Programming Language (Swift 4.1) Initialization](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Initialization.html)\n- [Swift 类构造器的使用](https://draveness.me/swift-zhong-init-de-shi-yong)\n\n-- EOF --\n","tags":["swift","ios"]},{"title":"iOS Safe Area 我所知道的全部","url":"/2018/03/29/ios-safe-area/","content":"\n在 iOS 7 Apple 在 UIViewController 中引入了 `topLayoutGuide` 和 `bottomLayoutGuide` 属性来描述没有被覆盖(status bar, navigation bar, toolbar, tab bar, etc.)屏幕的区域。在 iOS 11 中,Apple 已经弃用了这些属性,并引入了 safe area。Apple 建议我们不要在 safe area 操作,在 iOS 11 中,当在 iOS App 中定位视图时,你必须使用新的 safe area API。\n\n## UIView\n\n在 iOS 11 UIViewController `topLayoutGuide` 和 `bottomLayoutGuide` 属性已经被替换成了新的 UIView 中的 safe area:\n\n```swift\n@available(iOS 11.0, *)\nopen var safeAreaInsets: UIEdgeInsets { get }\n\n@available(iOS 11.0, *)\nopen var safeAreaLayoutGuide: UILayoutGuide { get }\n```\n\n`safeAreaInsets` 属性意味着屏幕可以覆盖从四个方向,而不仅仅是顶部和底部。当被 iPhone X 呈现时,我们就明白了为什么我们需要左右 insets。\n\n<img src=\"https://tva1.sinaimg.cn/large/006tNbRwly1fptmugww5wj30zs0x0jw8.jpg\" alt=\"ios-safe-area\" width=\"400\" />\n\n_iPhone 8 vs iPhone X safe area (portrait orientation)_\n\n<!-- more -->\n\n<img src=\"https://tva1.sinaimg.cn/large/006tNbRwly1fptmv9csg7j30z0152ae6.jpg\" alt=\"ios-safe-area\" width=\"400\" />\n\n_iPhone 8 vs iPhone X safe area (landscape orientation)_\n\niPhone X 在 portrait orientation 有 top 和 bottom 的 safe area,在 landscape orientation 有 left right 和 bottom。\n\n让我们来看一个例子。在 ViewController 的 View 的顶部和底部添加了两个带有文本标签和固定高度的 custom subviews,并附加 attached 到视图的边缘 edges。\n\n<img src=\"https://tva1.sinaimg.cn/large/006tNbRwly1fptnobrau6j30no14i13p.jpg\" alt=\"ios-safe-area\" width=\"300\" />\n\n_Subviews are attached to the view’s edges_\n\n正如所看到的,subviews 内容与顶部的 notch 和底部的 home indicator 指示器重叠 overlapped。为了正确地定位 subviews,我们可以使用手动布局将它们附加到 safe area:\n\n```swift\ntopSubview.frame.origin.x = view.safeAreaInsets.left\ntopSubview.frame.origin.y = view.safeAreaInsets.top\ntopSubview.frame.size.width = view.bounds.width - view.safeAreaInsets.left - view.safeAreaInsets.right\ntopSubview.frame.size.height = 300\n```\n\n或者使用 Auto Layout:\n\n```swift\nbottomSubview.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true\nbottomSubview.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true\nbottomSubview.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true\nbottomSubview.heightAnchor.constraint(equalToConstant: 300).isActive = true\n```\n\n<img src=\"https://tva1.sinaimg.cn/large/006tNbRwly1fptqg9si13j30no14iwq1.jpg\" alt=\"ios-safe-area\" width=\"300\" />\n\n_Subviews are attached to the superview safe area_\n\n上面看起来好很多。此外可以在 subview subclass 添加 subviews content 到 safe area。\n\n<img src=\"https://tva1.sinaimg.cn/large/006tNbRwly1fptquigdxjj30no14iqdh.jpg\" alt=\"ios-safe-area\" width=\"300\" />\n\n_Subviews are attached to the view’s edges. Labels are attached to the superview safe area._\n\n在 subviews 层次结构 hierarchy 的任何地方都可以将 view 添加到 safe area。\n\n## UIViewController\n\n在 iOS 11 UIViewController 有了一个新属性:\n\n```swift\n@available(iOS 11.0, *)\nopen var additionalSafeAreaInsets: UIEdgeInsets\n```\n\n当 view controller subviews 覆盖嵌入的 child view controller views 时,将使用它。例如,Apple 在 UINavigationController 和 UITabBarController 中使用额外的 additional safe area insets,当这些条是半透明的。\n\n`additionalSafeAreaInsets` 是对现有 safearea 的扩展附加。\n\n当你改变 additional safe area insets 或者 safe area insets 被系统改变,UIView 和 UIViewController 中的方法将被调用:\n\n```swift\n// UIView\n@available(iOS 11.0, *)\nopen func safeAreaInsetsDidChange()\n\n//UIViewController\n@available(iOS 11.0, *)\nopen func viewSafeAreaInsetsDidChange()\n```\n\n### Simulate iPhone X safe area\n\nAdditional safe area insets 也可以用来测试你的 app 是如何支持 iPhone X,如果你不能在模拟器上测试你的 app,而且没有 iPhone X,那就很有用了。\n\n```swift\n//portrait orientation, status bar is shown\nadditionalSafeAreaInsets.top = 24.0\nadditionalSafeAreaInsets.bottom = 34.0\n\n//portrait orientation, status bar is hidden\nadditionalSafeAreaInsets.top = 44.0\nadditionalSafeAreaInsets.bottom = 34.0\n\n//landscape orientation\nadditionalSafeAreaInsets.left = 44.0\nadditionalSafeAreaInsets.bottom = 21.0\nadditionalSafeAreaInsets.right = 44.0\n```\n\n<img src=\"https://tva1.sinaimg.cn/large/006tNc79ly1fpw2l9aa64j31kw0z0q9t.jpg\" alt=\"ios-safe-area\" width=\"400\" />\n\n## UIScrollView\n\n> 待续\n\n## Refereneces\n\n- [iOS Safe Area](https://medium.com/rosberryapps/ios-safe-area-ca10e919526f)\n","tags":["ios"]},{"title":"UITableViewCell 自适应 UITextView 高度","url":"/2018/03/27/self-sizing-uitextview-in-a-uitableviewcell/","content":"\n使用 Auto Layout 让 UITableViewCell 自适应 UITextView 高度,效果演示:\n\n<img src=\"https://user-images.githubusercontent.com/9289792/37953137-931860e0-31d4-11e8-8809-c871b09f9519.gif\" alt=\"Self-sizing UITextView in cell\" width=\"200\" />\n\n[99-projects-of-swift/029-tableviewcell-self-adaption](https://github.com/imzyf/99-projects-of-swift/tree/master/029-tableviewcell-self-adaption)\n\n<!-- more -->\n\n## 预备步骤\n\n1. 给 textView 上下左右建立相对于 cell 的约束\n2. 取消 textView 的 `Scrolling Enabled`\n3. 设置 tableView 估算高度 `tableView.estimatedRowHeight = 70`\n4. 设置 `textView.delegate = self`\n\n## 关键点\n\n如果在 `textViewDidChange(textView:)` 调用 `tableView.reloadData()` 会造成 textView 失去焦点,键盘隐藏。\n\n解决方法:\n\n```\nfunc textViewDidChange(textView: UITextView) {\n tableView.beginUpdates()\n tableView.endUpdates()\n}\n```\n\n这里带来了一个问题,当 textView 长度超过一屏或者过长时,在输入时 tableView 会跳动滚动 jumping and stuttering。\n\n更好的解决方法:\n\n```\nfunc textViewDidChange(textView: UITextView) {\n let currentOffset = tableView.contentOffset\n UIView.setAnimationsEnabled(false)\n tableView.beginUpdates()\n tableView.endUpdates()\n UIView.setAnimationsEnabled(true)\n tableView.setContentOffset(currentOffset, animated: false)\n}\n```\n\n禁用动画和重建表视图的内容偏移修正抖动。\n\n## References\n\n- [Self-sizing UITextView in a UITableView using Auto Layout - like Reminders.app](http://candycode.io/self-sizing-uitextview-in-a-uitableview-using-auto-layout-like-reminders-app/)\n\n-- EOF --\n","tags":["ios"]},{"title":"【译】iOS 单元测试和 UI 测试入门教程","url":"/2018/03/15/ios-unit-testing-and-ui-testing-tutorial/","content":"\n> 原文链接:[iOS Unit Testing and UI Testing Tutorial - Ray Wenderlich](https://www.raywenderlich.com/150073/ios-unit-testing-and-ui-testing-tutorial)\n\n编写测试并不迷人 (glamorous),但是既然测试能让你闪闪发光 (sparkling) 的应用程序变成 (from turning into) 一堆乱七八糟的垃圾,那么说明测试是必要的。如果你正在阅读 iOS 单元测试和 UI 测试入门教程,那么你已经知道 _应该_ 为代码和 UI 编写测试,但是你不知道如何在 Xcode 中进行测试。\n\n<!-- more -->\n\n也行你已经有一个 “可以运行” 的 app 但是没有为它编写任何测试,并且当你想拓展 app 时,你能够测试任何的改变。也许你已经编写了一些测试,但是不确定他们是否是 _正确_ 的测试。或者你现在正在开发你的 app,想在你离开的时候进行测试。\n\n这篇入门教程将告诉你如何使用 Xcode 的测试导航栏去测试一个 app 模块和异步方法 (asynchronous methods),如何模拟交互 fake interactions 或者系统对象通过 subs and mocks,如何测试 UI 和性能 performance,和如何使用代码覆盖率工具 code coverage tool。在此过程中 Along the way,您将会学到一些用于测试忍者 testing ninjas 的词汇 vocabulary,在本教程的最后,你将使用依赖注入对您的系统进行测试 you’ll be injecting dependencies into your System Under Test (SUT) with aplomb!。\n\n## 测试,测试\n\n### 测试什么\n\n在编写任何测试之前,一个开始时非常重要的基础是:你需要测试什么?如果你的目标是拓展一个已经存在的 app,你首先应该为 **任何你计划改变的组件** 编写测试。\n\n更普遍的讲,测试应该覆盖:\n\n- 核心功能 Core functionality:模型、类和方法,和与他们相互作用的控制器\n- 最常见的 UI 工作流\n- 边界条件 Boundary conditions\n- Bug 修复\n\n### 第一件事 FIRST:测试的最佳实践\n\nFIRST 首字母缩写词描述了有效单元测试的一套简明标准。这些条件是:\n\n- Fast:运行测试应该快速,这样人们就不会介意运行它们了。\n- Independent/Isolated:测试不应该对彼此进行设置或拆解 setup or teardown。\n- Repeatable:每次运行测试时,都应该得到相同的结果。外部数据提供者和并发问题可能导致间歇性故障。\n- Self-validating:测试应完全自动化;输出应该是“通过”或“失败”,而不是程序员对日志文件的解释。\n- Timely:理想情况下,测试应该在编写生产代码之前编写。\n\n遵循 FIRST 原则,你的测试将保持清晰和有帮助,而不是为你的 app 设置障碍。\n\n## 开始\n\n下载,解压,打开,检查 [初始项目](https://koenig-media.raywenderlich.com/uploads/2016/12/Starters.zip) BullsEye 和 HalfTunes。\n\nBullsEye 是一个基于 [iOS Apprentice](https://www.raywenderlich.com/store/ios-apprentice) 的示例 app。已经将游戏逻辑提取到一个 BullsEyeGame 类中,并添加了另一种游戏风格。\n\n在右下角,有一个分段控制,让用户选择游戏风格:Slide,移动滑块尽可能接近目标值,或者 Type,来猜测滑块的位置。控件的操作还将用户的游戏样式选择存储为用户默认值。\n\nHalfTunes 是 [NSURLSession Tutorial](https://www.raywenderlich.com/110458/nsurlsession-tutorial-getting-started) 中的示例 app,更新至 Swift 3。 用户可以通过 iTunes API 查询歌曲,然后下载并播放歌曲片段。\n\n让我们开始测试!\n\n## Xcode 中的单元测试\n\n### 创建一个 Unit Test Target\n\nXcode Test Navigator 提供了使用测试的最简单的方法;你将使用它创建 test targets 和在 app 运行测试。\n\n打开 BullsEye 项目然后敲击 Command + 6 打开 test navigator。\n\n点击右下角的 + 按钮,然后从菜单中选择 New Unit Test Target…\n\n![](https://koenig-media.raywenderlich.com/uploads/2016/12/TestNavigator1.png)\n\n接受默认的名字 BullsEyeTests。当 test navigator 中出现 test bundle,点击打开。如果 BullsEyeTests 没有自动出现,通过单击其他 navigators 中的一个来解决问题 trouble-shoot by,然后返回到 test navigator。\n\n![](https://koenig-media.raywenderlich.com/uploads/2016/12/TestNavigator2.png)\n\n模板导入了 XCTest 并定义了 XCTestCase 的一个 BullsEyeTests 子类,还有 setup() tearDown() 和 testExample()。\n\n有三种方法运行 test class:\n\n1. Product\\Test or Command-U. 将运行所有的 test classes\n2. 在 test navigator 中点击 arrow 按钮\n3. 在边沿点击菱形按钮\n\n![](https://koenig-media.raywenderlich.com/uploads/2016/12/TestNavigator3.png)\n\n您还可以通过单击菱形按钮来运行单个测试方法,无论是在 test navigator 中还是在 gutter 中。\n\n当所有的 tests 成功后,菱形按钮就会变绿并显示出检查的痕迹。单击 testPerformanceExample() 末尾的灰色菱形,打开性能结果:\n\n![](https://koenig-media.raywenderlich.com/uploads/2016/12/TestNavigator4.png)\n\n你不需要 testPerformanceExample(),所以删除它。\n\n### 使用 XCTAssert 测试 Models\n\n首先,将使用 XCTAssert 去测试 BullsEye model 中的一个核心方法:BullsEyeGame 对象是否正确计算一个回合的分数。\n\n在 BullsEyeTests.swift 添加\n\n```\n@testable import BullsEye\n```\n\n这使得 unit tests 可以访问 BullsEye 中的类和方法。\n\n在 BullsEyeTests 顶部添加属性\n\n```\nvar gameUnderTest: BullsEyeGame!\n```\n\n在 setup() 创建一个新 BullsEyeGame 对象,在 super 之后\n\n```\ngameUnderTest = BullsEyeGame()\ngameUnderTest.startNewGame()\n```\n\n这将在类级别创建一个 SUT (System Under Test) object,因此这个 test class 中的所有测试都可以访问 SUT 对象的属性和方法。\n\n在这里你也可以调用 startNewGame,它将创建一个 targetValue。许多测试将使用 targetValue,来测试游戏是否正确地计算了分数。\n\n不要忘记释放你的 SUT object 在 tearDown(),在你调用 super 之前\n\n```\ngameUnderTest = nil\n```\n\n> Note: It’s good practice to create the SUT in setup() and release it in tearDown(), to ensure every test starts with a clean slate. For more discussion, check out Jon Reid’s post on the subject.\n\n现在你已经准别写你的第一个 test!\n\n用一些代码替换 testExample()\n\n```\n// XCTAssert to test model\nfunc testScoreIsComputed() {\n // 1. given\n let guess = gameUnderTest.targetValue + 5\n\n // 2. when\n _ = gameUnderTest.check(guess: guess)\n\n // 3. then\n XCTAssertEqual(gameUnderTest.scoreRound, 95, \"Score computed from guess is wrong\")\n}\n\n```\n\n一个 test 方法总是以 test 开头,接下来是对它测试的描述。\n\n将测试格式化为 given when then 是很好的做法:\n\n1. 在 given 部分,设置需要的值:在此例你创建一个 guess 值,所以你可以指定它与 targetValue 有多大的不同。\n2. 在 when 部分,执行正在测试的代码:调用 `gameUnderTest.check(_:)`。\n3. 在 then 部分,断言你所预期的结果(在本例,gameUnderTest.scoreRound is 100 – 5)如果测试失败,则会打印一条消息。\n\n> Note: To see a full list of XCTestAssertions, Command-click XCTAssertEqual in the code to open XCTestAssertions.h, or go to Apple’s Assertions Listed by Category.\n\n<img src=\"https://koenig-media.raywenderlich.com/uploads/2016/12/givenWhenThen.png\" width=\"260px\" />\n\n> Note: The Given-When-Then structure of a test originated with Behavior Driven Development (BDD) as a client-friendly, low-jargon nomenclature. Alternative naming systems are Arrange-Act-Assert and Assemble-Activate-Assert.\n\n### Debugging a Test\n\n在 BullsEyeGame 故意留下了 bug,所以现在你要练习找到它。为了找到 bug 将 testScoreIsComputed 改名为 testScoreIsComputedWhenGuessGTTarget。在这个 test 中,在 given 部分从 targetValue 减去 5\n\n```\nfunc testScoreIsComputedWhenGuessLTTarget() {\n // 1. given\n let guess = gameUnderTest.targetValue - 5\n\n // 2. when\n _ = gameUnderTest.check(guess: guess)\n\n // 3. then\n XCTAssertEqual(gameUnderTest.scoreRound, 95, \"Score computed from guess is wrong\")\n}\n```\n\nguess 与 targetValue 之间仍然是 5,所以分数应该还是 95。在 breakpoint navigator 添加 Test Failure Breakpoint,这将在断言失败时停止测试运行。\n\n![](https://koenig-media.raywenderlich.com/uploads/2016/12/AddTestFailureBreakpoint.png)\n\n运行你的 test:当测试失败时它将停在 XCTAssertEqual 这行。检查 gameUnderTest 和 guess 在 debug console\n\n![](https://koenig-media.raywenderlich.com/uploads/2016/12/TestFailure.png)\n\nguess 是 trgetValue - 5 但是 scoreRound is 105 不是 95!\n\n进一步研究,使用正常的调试过程:在 when 声明处和在 BullsEyeGame.swift 设置断点,在 `check(_:)` 中产生了差异。然后再次运行测试,并且 step-over let difference 检查 difference 的差异值:\n\n![](https://koenig-media.raywenderlich.com/uploads/2016/12/DebugConsole.png)\n\n问题是 difference 是负的,所以得分是 100 – (-5),使用 difference 绝对值来修复这里。\n\n移除两个断点然后再次运行测试以确认它现在成功了。\n\n> 待续\n","tags":["ios"]},{"title":"【Git 权威指南】读书笔记 - 协同模型","url":"/2018/01/19/got-git-reading-notes-model/","content":"\n主要内容:【Git 协同模型】\n\n## 经典 Git 协同模型\n\n### 集中式协同模型\n\n可以像集中式版本控制系统那样使用 Git,在一个大家都可以访问到的服务器上架设 Git 服务器,每个人从该服务器克隆代码,本地提交推送到服务器上。\n\n## 金字塔式协同模型\n\n虽然理论上每个开发者的版本库都是平等的,但是会有一个公认的权威的版本库,这个版本库由一个或者多个核心开发者负责维护(具有推送的权限)。\n\n开源社区逐渐发展出金字塔模型,而这也是必然之选。\n\n<!-- more -->\n\n## Topgit 协同模型\n\n> 笔者注:Topgit 是否已经过时?\n\n`卖主分支 Vendor Branch` 是在版本库中专门创建一个和上游同步的分支,一旦有上游代码发布就捡入到卖主分支中。\n\n## 子模组协同模型\n\n### 创建子模组\n\n```bash\ngit submodule add /path/to/repos/libA.git lib/lib_a\n```\n\n`.gitmodules` 的内容:\n\n```bash\ncat .gitmodules\n[submodule \"lib/lib_a\"]\n path = lib/lib_a\n url = /path/to/repos/libA.git\n```\n\n### 克隆带子模组的版本库\n\n```bash\ngit clone /path/to/repos/super.git /path/to/my/workspace/super-clone\n```\n\n子模组的版本库并不会默认克隆,如果需要克隆出子模组型式引用的外部库,需要执行:\n\n```bash\ngit submodule init\ngit submodule update\n```\n\n### 在子模组中修改和子模组的更新\n\n修改更新的方式和普通仓库一样。如果修改了子模块,要先推送子模块的修改,再推送主仓库,以防止其他人克隆 super 版本库、更新模组时因为找不到该子模组版本库相应的提交而导致出错。\n\n查看子模组状态:\n\n```bash\ngit submodule status\n```\n\n## 子树合并\n\n### 引入外部版本库\n\n```bash\n# 注册外部版本库\ngit remote add util /path/to/repos/util.git\n\ngit fetch util\n\n# 查看所有分支\ngit branch -a\n\n# 从 util/master 远程分支创建一个本地分支 util-branch\ngit checkout -b util-branch util/master\n```\n\n### 子目录方式合并外部版本库\n\n```bash\n# 在主分支,将分支 util-branch 读取到当前分支的一个子目录下\ngit read-tree --prefix=lib util-branch\n\n# 将 lib 目录下的文件更新出来\ngit checkout -- lib\n```\n\n现在还不能忙着提交,因为如果现在进行提交就体现不出来两个分支的合并关系。需要使用 Git 底层的命令进行数据提交。\n\n```bash\n# 将暂存区的目录树保存下来\n\ngit write-tree\n```\n\n要手工创建一个合并提交,即新的提交要有两个父提交。这两个父提交分别是 master 分支和 util-branch 分支的最新提交。\n\n```bash\necho \"subtree merge\" | \\\n git commit-tree 2153518409d218609af40babededec6e8ef51616 \\\n -p 911b1af2e0c95a2fc1306b8dea707064d5386c2e \\\n -p 12408a149bfa78a4c2d4011f884aa2adb04f0934\n62ae6cc3f9280418bdb0fcf6c1e678905b1fe690\n```\n\n需要把当前的 master 分支重置到此提交 ID:\n\n```bash\ngit reset 62ae6cc3f9280418bdb0fcf6c1e678905b1fe690\n```\n\n操作繁琐,可使用下面 `subtree` 命令进行代替。\n\n### 利用子树合并跟踪上游改动\n\n```bash\ngit checkout util-branch\n\ngit pull\n\ngit checkout master\n\ngit merge -Xsubtree=lib util-branch\n```\n\n## Git Subtree\n\n### 管理子项目\n\n假设 `P1 项目`、`P2 项目` 共用 `S 项目`\n\n- 关联 S 项目\n\n```bash\ngit subtree add --prefix=<S项目的相对路径> <S项目git地址> <分支> --squash\n```\n\n`--squash` 意思是把 `subtree` 的改动合并成一次 `commit`,这样就不用拉取子项目完整的历史记录。`--prefix` 之后的 `=` 等号也可以用空格。\n\n- 更新代码\n\nP1、P2 项目里各种提交 commit,其中有些 commit 会涉及到 `S 目录` 的更改,但是没关系。\n\n- 提交更改到子项目\n\n```bash\ngit subtree push --prefix=<S项目的相对路径> <S项目git地址> <分支>\n```\n\nGit 会遍历 `步骤 2` 中所有的 `commit`,从中找出针对 `S 目录` 的更改,然后把这些更改记录提交到 `S 项目` 的 Git 服务器上,并保留 `步骤 2` 中的相关 `S 的提交` 记录到 `S仓库` 中。\n\n- 更新子目录\n\n```bash\ngit subtree pull --prefix=<S项目的相对路径> <S 项目 git 地址> <分支> --squash\n```\n\n### 拆分已有项目\n\n需要从现有项目中抽取公共模块单独进行 Git 管理,假设已有 `项目 P` 抽取 `项目 S`。\n\n- 提交日志分离\n\n```bash\ngit subtree split -P <S项目的相对路径> -b <临时branch>\n```\n\nGit 会遍历所有的 commit,分离出与 S 项目的相对路径相关的 commit,并存入临时 branch 中。\n\n- 创建子 repo\n\n```bash\nmkdir <S项目新路径>\ncd S项目新路径\ngit init\ngit pull <P项目的路径> <临时branch>\ngit remote add origin <S项目的git仓库>\ngit push origin -u master\n```\n\n- 清理数据\n\n```bash\ncd P项目的路径\ngit rm -rf <S项目的相对路径>\ngit commit -m '移除相应模块' # 提交删除申请\ngit branch -D <临时branch> # 删除临时分支\n```\n\n- 添加 subtree\n\n```bash\ngit subtree add --prefix=<S项目的相对路径> <S项目git地址> <分支> --squash\ngit push origin master\n```\n\n执行完第 2 步时,对应的目录已经剥离出来形成独立的项目了。第 3,4 步主要是把当前项目的对应的文件给删除,重新在 P 项目建立 Subtree。\n\n_tip:_\n推送到 GitHub Page:\n\n```bash\ngit subtree push --prefix=dist origin gh-pages\n```\n\n## References\n\n- [4. 协同模型 — GotGit](http://www.worldhello.net/gotgit/04-git-model/)\n- [Git Subtree 的使用](https://www.jianshu.com/p/3096069e9b72)\n\n-- EOF --\n","tags":["git"]},{"title":"【Git 权威指南】读书笔记 - 和声","url":"/2018/01/17/got-git-reading-notes-harmony/","content":"\n主要内容:【Git 协议与工作协同】、【冲突解决】、【Git 里程碑】、【Git 分支】、【远程版本库】、【补丁文件交互】\n\n## Git 协议与工作协同\n\n### Git 支持的协议\n\nSSH、GIT、HTTP、HTTPS、FTP、FTPS、RSYNC 及前面已经看到的本地协议。\n\nSSH 协议:\n\n```\nssh://[user@]example.com[:port]/path/to/repo.git/\n\n[user@]example.com:path/to/repo.git/\n```\n\nGIT 协议,最常用的只读协议:\n\n```\ngit://example.com[:port]/path/to/repo.git/\n```\n\nHTTP[S] 协议:\n\n```\nhttp[s]://example.com[:port]/path/to/repo.git/\n```\n\n<!-- more -->\n\n### 强制非快进式推送\n\n```\ngit push -f\n```\n\n强制推送,会强制刷新服务器中的版本。\n\n### 禁止非快进式推送\n\n```\ngit --git-dir=/path/to/repos/shared.git config receive.denyNonFastForwards true\n```\n\n## 冲突解决\n\n### 拉回操作中的合并\n\n```\ngit pull = git fetch + git merge\n```\n\n### 合并策略\n\n> [Merge Strategis](https://git-scm.com/docs/git-merge#_merge_strategies)\n\nGit 合并操作支持很多合并策略,默认会选择最适合的合并策略。例如,和一个分支进行合并时会选择 recursive 合并策略,当和两个或两个以上的其他分支进行合并时采用 octopus 合并策略。\n\n```\ngit merge [-s <strategy>] [-X <strategy-option>] [<commit>...]\n```\n\nThis option forces conflicting hunks to be auto-resolved cleanly by favoring our version.\n\n```\ngit merge -s recursive -X ours [<commit>...]\n```\n\nMerge branch obsolete into the current branch, using ours merge strategy:\n\n```\ngit merge -s ours obsolete\n```\n\n## Git 里程碑\n\n### 显示里程碑\n\n```\ngit tag\n```\n\n显示说明。`-n<num>` 显示最多 `<num>` 行里程碑的说明:\n\n```\ngit tag -n1\n```\n\n使用统配:\n\n```\ngit tag -l v1.*\n```\n\n查看提交对应的里程碑及其他引用:\n\n```\ngit log --oneline --decorate\n```\n\n### 创建里程碑\n\n当创建了里程碑 mytag 后,会在版本库的 .git/refs/tags 目录下创建一个新文件。实际上指向的是一个提交:\n\n```\ngit tag mytag\n\ngit cat-file -t mytag\ncommit\n```\n\n带说明的 tag 指向的不再是一个提交,而是一个 tag 对象:\n\n```\ngit tag -m \"My first annotated tag.\" mytag2\n\ngit cat-file -t mytag2\ntag\n```\n\n为里程碑对象添加 GnuPG 签名:\n\n```\ngit tag -s -m \"My first GPG-signed tag.\" mytag3\n\n```\n\n### 删除里程碑\n\n```\ngit tag -d mytag\nDeleted tag 'mytag' (was 60a2f4f)\n```\n\n### 共享里程碑\n\n创建的里程碑,默认只在本地版本库中可见,不会因为对分支执行推送而将里程碑也推送到远程版本库。\n\n将 mytag 里程碑共享到上游版本库:\n\n```\ngit push origin mytag\n```\n\n所有里程碑全部推送到远程版本库:\n\n```\ngit push origin refs/tags/*\n```\n\n- 里程碑共享,必须显式的推送。\n- 执行获取或拉回操作,自动从远程版本库获取新里程碑,并在本地版本库中创建。\n- 如果本地已有同名的里程碑,默认不会从上游同步里程碑,即使两者里程碑的指向是不同的。\n\n### 删除远程版本库的里程碑\n\n```\ngit push origin :mytag2\n```\n\n### 里程碑命名规范\n\n[语义化版本 2.0.0](https://semver.org/lang/zh-CN/)\n\n版本格式:主版本号.次版本号.修订号,版本号递增规则如下:\n\n- 主版本号:当你做了不兼容的 API 修改,\n- 次版本号:当你做了向下兼容的功能性新增,\n- 修订号:当你做了向下兼容的问题修正。\n\n先行版本号及版本编译信息可以加到 `主版本号.次版本号.修订号` 的后面,作为延伸。\n\n## Git 分支\n\n分支是代码管理的利器。如果没有有效的分支管理,代码管理就适应不了复杂的开发过程和项目的需要。\n\n### 分支命令概述\n\n```\n# 显示\n1. git branch\n\n# 创建\n2. git branch <branchname>\n3. git branch <branchname> <start-point>\n\n# 删除,-d 在删除分支 <branchname> 时会检查所要删除的分支是否已经合并到其他分支中,否则拒绝删除。\n4. git branch -d <branchname>\n5. git branch -D <branchname>\n\n# 重命名,-m 如果版本库中已经存在名为 <newbranch> 的分支,拒绝执行重命名,而 7 会强制执行。\n6. git branch -m <oldbranch> <newbranch>\n7. git branch -M <oldbranch> <newbranch>\n```\n\n创建并切换到新分支:\n\n```\ngit checkout -b <new_branch> [<start_point>]\n```\n\n### 分支变基\n\n- master\n- dev(开发完成,领先 master)\n\n```\n# 先切换到 dev\ngit checkout dev\n\n# 变基操作\ngit rebase master\n\n# 遇到冲突,解决冲突\n\n# 添加到暂存区\ngit add -u\n\n# 继续变基\ngit rebase --continue\n\n# 直接将 dev 推送到远程 master\ngit push origin dev:master\n\n# 切换到 master\ngit checkout master\n\n# 拉取最新代码\ngit pull\n\n# 删除 dev 分支\ngit branch -d dev\n```\n\n## 远程版本库\n\n### 远程分支\n\n查看远程分支:\n\n```\ngit branch -r\n```\n\n在向远程版本库执行获取操作时,不是把远程版本库的分支原封不动地复制到本地版本库的分支中,而是复制到另外的命名空间。\n\n远程分支不是真正意义上的分支,是类似于里程碑一样的引用。如果针对远程分支执行检出命令,会看到大段的错误警告。\n\n远程分支类似于里程碑,如果检出就会使得头指针 HEAD 处于分离头指针状态。实际上除了以 refs/heads 为前缀的引用之外,如果检出任何其他引用,都将使工作区处于分离头指针状态。如果对远程分支进行修改就需要创建新的本地分支。\n\n### 分支追踪\n\n为了能够在远程分支 `origin/hello-1.x` 上进行工作,需要基于该远程分支创建本地分支:\n\n```\ngit checkout hello-1.x\n```\n\n从远程分支创建本地分支,自动建立了分支间的跟踪,而从一个本地分支创建另外一个本地分支则没有。\n\n从 `hello-1.x` 分支中创建新的本地分支 `hello-jx`,并与远程建立联系。\n\n```\ngit checkout -b hello-jx hello-1.x\n```\n\n```\ncat .git/config\n```\n\n### 远程版本库\n\n```\n# 设置\ngit remote add new-remote file:///path/to/repos/hello-user1.git\n\n# 修改 url\ngit remote set-url new-remote file:///path/to/repos/hello-user2.git\n\n# 单独修改 push 地址\ngit remote set-url --push new-remote /path/to/repos/hello-user2.git\n\n# 修改 版本库名称\ngit remote rename new-remote user2\n\n# 当注册了多个远程版本库并希望获取所有远程版本库的更新\ngit remote update\n\n# 如果某个远程版本库不想在执行 git remote update 时获得更新\ngit config remote.user2.skipDefaultUpdate true\n\n# 删除\ngit remote rm\n```\n\n### PUSH 和 PULL 操作与远程版本库\n\n在执行 `git pull` 操作的时候可以通过参数 `--rebase` 设置使用变基而非合并操作,将本地分支的改动变基到跟踪分支上。为了避免因为忘记使用 `--rebase` 参数导致分支的合并,可进行设置:\n\n```\ngit config branch.<branchname>.rebase true\n```\n\n这样在遇到冲突(本地和远程分支出现偏离)的情况下,会采用变基操作,而不是默认的合并操作。\n\n如果为本地版本库设置参数 `branch.autosetuprebase` ,值为 `true`,则在基于远程分支建立本地追踪分支时,会自动配置 `branch.<branchname>.rebase` 参数。\n\n### 分支和里程碑的安全性\n\n- 用 `reflog` 记录对分支的操作历史。默认创建的带工作区的版本库都会包含 `core.logallrefupdates` 为 `true` 的配置,这样在版本库中建立的每个分支都会创建对应的 `reflog`。但是创建的裸版本库默认不包含这个设置,也就不会为每个分支设置 `reflog`。可能因为分支误操作导致数据丢失,可以考虑为裸版本库添加此配置。\n\n- 关闭非快进式提交。配置 `receive.denyNonFastForwards` 设置为 `true`,则禁止一切非快进式推送。更好的方法是通过架设基于 SSH 协议的 Git 服务器,配置强制提交的用户权限。\n\n- 关闭分支删除功能。配置 `receive.denyDeletes` 设置为 `true`,则禁止删除分支。更好的方法是:配置分支删除的用户权限。\n\n## 补丁文件交互\n\n将最近三个提交转换为补丁文件:\n\n```\ngit format-patch -s HEAD~3..HEAD\n```\n\n`-s` 会在导出的补丁文件中添加当前用户的签名。\n\n应用补丁:\n\n```\ngit am user1-mail-archive\n```\n\n`git apply` 可以应用一般格式的补丁文件,但是不能执行提交,也不能保持补丁中的作者信息。\n\n## References\n\n- [3. Git 和声 — GotGit](http://www.worldhello.net/gotgit/03-git-harmony)\n","tags":["git"]},{"title":"R.swift 强类型引用资源文件","url":"/2018/01/17/r-swift-getting-started/","content":"\n[R.swift](https://github.com/mac-cain13/R.swift) 获取强类型、自动编译的图片、字体、segues 等资源。\n\n以避免无法编译时检查 `字符串` 的形式引用资源所导致的错误。\n\n```swift\nlet icon = UIImage(named: \"settings-icon\")\nlet font = UIFont(name: \"San Francisco\", size: 42)\nlet color = UIColor(named: \"indictator highlight\")\nlet viewController = CustomViewController(nibName: \"CustomView\", bundle: nil)\nlet string = String(format: NSLocalizedString(\"welcome.withName\", comment: \"\"), locale: NSLocale.current, \"Arthur Dent\")\n```\n\nWith R.swift\n\n```swift\nlet icon = R.image.settingsIcon()\nlet font = R.font.sanFrancisco(size: 42)\nlet color = R.color.indicatorHighlight()\nlet viewController = CustomViewController(nib: R.nib.customView.name)\nlet string = R.string.localizable.welcomeWithName(\"Arthur Dent\")\n```\n\n<!-- more -->\n\n## 配置\n\n其中 GitHub 上有的就不再累述了,主要注意:\n\n- 添加 `R.generated.swift` 不要勾选 `Copy items if needed`,软引用就好。\n- 要在 `.gitignore` 添加 `*.generated.swift` 以避免不必要的冲突。\n\n添加新的资源文件后需要 `command + b` 编译下,才可以使用。\n\n## .clr 颜色文件的创建和使用\n\n使用 Xcode 创建 .clr 文件:\n\n<img src=\"https://user-images.githubusercontent.com/9289792/80204310-36566900-865b-11ea-864b-89a8e5cc8e75.jpg\" alt=\"Create a Color Palette\" width=\"300px\" />\n\n之后可以在其中添加颜色,选择颜色回车可以对颜色从新命名。\n\n生成的 `.clr` 文件保存在 `~/library/Colors` 下,可以导入到项目后再通过 `R.color` 使用。\n\nTips 打开、关闭隐藏文件:\n\n```bash\ncommand + shift + .\n```\n\n## References\n\n- [XCode Tip: Color Palette](https://www.natashatherobot.com/xcode-color-palette/)\n","tags":["swift","ios"]},{"title":"Xcode 不知名的实用技巧","url":"/2018/01/12/xcode-tips/","content":"\n## 状态栏\n\n### Help\n\n`Search` 框可以很方便的检索到相关的设置项。\n\n<img src=\"https://user-images.githubusercontent.com/9289792/80204076-c811a680-865a-11ea-8c0b-5e3b70ae22bd.jpg\" alt=\"xcode-unknown-tips-Search\" width=\"960px\" />\n\n<!-- more -->\n\n## Navigator\n\n`⌘ 1` .. `⌘ 7` 可以切换窗口。\n\n### Show the Symbol navigator\n\n在类不多时,可以方便看所有类的结构,类多时时使用下面的 filter 功能。\n\n<img src=\"https://user-images.githubusercontent.com/9289792/80204084-ca740080-865a-11ea-8fb0-e102827a3123.jpg\" alt=\"xcode-unknown-tips-Show the Symbol navigator\" width=\"260px\" />\n\n### Show the Breakpoint navigator\n\n点击右下角 `+` 选择 `Exception Breakpoint..`,可以在添加更明确的报错点,可以选择语言是 obj-c 还是 Swfit。\n\n<img src=\"https://user-images.githubusercontent.com/9289792/80204086-cb0c9700-865a-11ea-93b5-7fc0edd67daf.jpg\" alt=\"xcode-unknown-tips-Show the Breakpoint navigator\" width=\"260px\" />\n\n## Utilities\n\n`⌘ ⌥ 1` .. `⌘ ⌥ 7` 可以切换窗口。\n\n### Show the Code Snippet Library\n\n代码片段模板,也可以将自己的写好的片段拖入。\n\n<img src=\"https://user-images.githubusercontent.com/9289792/80204090-cba52d80-865a-11ea-9d1f-d6f772fc7e1c.jpg\" alt=\"xcode-unknown-tips-Show the Code Snippet Labrary\" width=\"260px\" />\n\n### Show the Media Library\n\n可以直接从中拖拽图片成 UIImageView 放入 xib 中。\n\n## xib\n\n### 快捷键\n\n按住 `option` 拖拽可以快速复制组件。\n\n## 快捷键\n\n### option ⌥\n\n- 按住 `⌥` 并点击代码或方法时,可查看行内文档帮助。\n- 按住 `⌥` 点击文件可以辅助编辑窗模式打开文件(多栏显示,可以用于 xib 与 code 绑定数据)。_好用_\n- 按住 `⌥ ⇧` 点击文件可以选择文件打开的窗口位置。\n\n### 显示和隐藏栏\n\n- `⌘ ⇧ y` 显示/隐藏调试区域。\n- `⌘ ⌥ ⏎` 显示辅助编辑栏,Open the assistant editor\n- `⌘ ⏎` 隐藏辅助编辑器栏。\n\n### 组合键\n\n> ^ 为 control,⌥ 为 option\n\n- `⌘ ⌥ {` `⌘ ⌥ }` 整行上下移动代码。\n- 将光标放在方法名上任一位置,点开 `Eidt`,点击 `⌃ ⇧` 将看到 `Copy Qualified Symbol Name`,点击 `⌃ ⇧ ⌥` 将看到 `Copy Symbol Name`,eg `[UIColor colorWithRed:255/255.0f green:127/255.0f blue:80/255.0f alpha:1]` 将会被拷贝为 `+[UIColor colorWithRed:green:blue:alpha:]`。\n- 自动缩进代码 `^ i` 或者全选 `⌘ a` 后 `^ i`。\n- `⌘ ⇧ o` Open Quickly 查找文件,或者使用 `⌘ space`。\n\n## command line\n\n- `Security find-identity -v` 显示出已经安装的有效身份。\n\n## References\n\n- [\\[译\\]每个 iOS 开发者都该知道的 17 个 Xcode 小技巧](https://juejin.im/post/5a7198ac51882573505189c8)\n","tags":["ios"]},{"title":"回顾 2017","url":"/2017/12/31/review-2017/","content":"\n2017 关键词:Birds、离职、狼人杀、白洋淀、iOS、自如。\n\n<iframe frameborder=\"no\" border=\"0\" marginwidth=\"0\" marginheight=\"0\" width=298 height=52 src=\"//music.163.com/outchain/player?type=2&id=423314746&auto=0&height=32\"></iframe>\n\n## Birds\n\nBirds 是自己编写时间最长的一个 Web 项目。项目接手、改版、重构、迭代、盈利,上半年每天工作都是面对它。\n\n<!-- more -->\n\nBirds 良好的表现与需求互相推动,这点很重要,假如有需求而项目没有成长,或项目成长没了需求,对工程来说都只能是纸上谈兵、无事可做。项目中的规范的重要性,无论是代码还是数据库,只要项目不是夭折终将显现,对于规范性我绝不会开倒车,做任何妥协,种种经历只说明:这里欠的东西总会还。如何应对需求的千变万化,小结几点:模块化、组件化思想,功能多可配置,功能可拔插,功能方法粒度要小;不要对程序肆意进行打补丁式的修改,减少对流程入侵;当某一处的逻辑增加时要及时重新修改、定义流程;对需求目的的正确理解更是能高效开发。\n\n对前端重新认识的一年。现在的前端和大学时刚刚接触时,已经大不一样了。前端能做的事情越来越多,很多业务逻辑都在前端处理,服务端只需要提供接口,分工合作更容易。浅尝 React Vue Webpack 后,也许前端才是自己的归宿。\n\n## 离职\n\n离职的不是我。老人走,新人来,新人走。离职一词在我身边跟了一年都没有停下来。同事离职的原因很多,离开北京、公司发展、自我发展... 马云说离职的原因就两个:1. 钱没给够。2. 受了委屈。我起初是认同这一说法的,但是后来觉得 “人” 或者说 “人的思想” 或者说 “人性” 在其中影响作用非常的大。相同的事情不同的角度看,结果相去甚远,如果基本的理念不同,那事情基本是无法调和的。\n\n对做的事情的认同感,对自我的定位与认识,对现实情况的冷静思考,这几点需要反复咀嚼。\n\n分享一篇文章:[公司没大牛带,需要离职么?](http://stormzhang.com/2017/06/26/leave-if-no-experts/)。对于刚入门的我们,这是一个很有代表性的问题。太多时候我们期待别人,忘了自己。公司有大牛,能跟着学是幸运,没有是常态。先接受了这个设定,会更容易找提高自己的方法。有没有大牛并不重要,我们是为了那个更好的自己。\n\n## 狼人杀\n\n2017 火的游戏很多:狼人杀、守望先锋、王者荣耀、吃鸡。狼人杀这一杀,好像杀到了毕业前和同学舍友在一起臭 high 的日子。可说骚话、互相 diss、互相吹捧,真真假假其乐无穷。最赤鸡的是鱼炸出了个女盆友,各路同学亲上加亲,无法克说。\n\n## 白洋淀\n\n> 我想 你说\n> 你不要在孤单 让我做你的伴\n\n白洋淀去过四次,那里的温泉过去两次,年中 Team Building 再喜加一,而这一次让这里有了美丽故事。喷泉、沙发,出现了对的人。\n\n## iOS\n\nReact Native 的一波尝试后,走上了移动开发的道。Swift 让上 iOS 车的门槛降低不少,Swift API 趋向稳定,可以说这是最好的时候。GitHub 应该是再也放不下了,外文的书籍、文档确实靠谱啊,推荐一个教程网站 [iOS Tutorials - Ray Wenderlich](https://www.raywenderlich.com/category/ios) 真的零基础入门。\n\n我相信:一个人语言的界限,就是他世界的边界。\n\n## 自如\n\n一场大火让无数人无家可归,年末换房也受牵连。被自如圈粉,其他中介的房子真的太 low 了。自如订房子居然也要抢,一波三折、失而复得。在没抢到房子时,真的想走,想离开这里了,很凄凉。最终是在公司对面住下了,过上了在家吃中饭晚饭的日子。\n\n## 2018 Happy New Year\n\n2017 自己是幸运的,2018 猥琐发育,不要浪,胜利属于伏地魔。\n\n-- EOF --\n","categories":["review"]},{"title":"【Git 权威指南】读书笔记 - 独奏 - Part 4","url":"/2017/12/25/got-git-reading-notes-solo-part4/","content":"\n主要内容:【历史穿梭】、【改变历史】、【Git 克隆】\n\n## 历史穿梭\n\n查看条件个数:\n\n```\ngit rev-list HEAD | wc -l\n```\n\n### 版本表示法:git rev-parse\n\n`git rev-parse` pick out and massage parameters for other git commands.\n\n- `--git-dir` 可以显示 Git 版本库的位置\n- `--show-cdup` 当前工作区目录的深度\n- `--parseopt` 可以用于被 Git 无关应用用于解析命令行参数\n\n```\n# 显示分支,tag\ngit rev-parse --symbolic --branches\n\ngit rev-parse --symbolic --branches\n\n# 显示定义的所有引用\ngit rev-parse --symbolic --glob=refs/*\n```\n\n<!-- more -->\n\n```\n# 显示多个表达式的 SHA1 哈希值:\n\ngit rev-parse master refs/heads/master\n6652a0dce6a5067732c00ef0a220810a7230655e\n6652a0dce6a5067732c00ef0a220810a7230655e\n\n^后面的数字代表该提交的第几个父提交,~<n>就相当于连续<n>个符号^\n\ngit rev-parse A~3 A^^^\ne80aa7481beda65ae00e35afc4bc4b171f9b0ebf\ne80aa7481beda65ae00e35afc4bc4b171f9b0ebf\n\n# 暂存区里的文件和HEAD中的文件相同\n\ngit rev-parse :gitg.png HEAD:gitg.png\nfc58966ccc1e5af24c2c9746196550241bc01c50\nfc58966ccc1e5af24c2c9746196550241bc01c50\n\n# 在提交日志中查找字串的方式显示提交\ngit rev-parse :/\"Commit A\"\n81993234fc12a325d303eccea20f6fd629412712\n```\n\n### 版本范围表示法:git rev-list\n\n`git rev-list` 可以帮助研究 Git 的各种版本范围语法。\n\n```\n# 从开始到 tag:A 的所有历史提交\ngit rev-list --oneline A\n\n# 每个 tag 历史提交的并集\ngit rev-list --oneline D F\n\n# ^ 排除这个版本及其历史版本\ngit rev-list --oneline ^G D\n\n# ^G D 等价于 G..D\ngit rev-list --oneline G..D\n\n# 含 ^ 的参数顺序不重要,..\n# ^B C 相当于 B..C\n# C ^B 相当于 B..C\n# C..B 相当于 ^C B\n```\n\n### 浏览日志:git\n\n`--graph` 参数调用 `git log` 可以显示字符界面的提交关系图。\n\n```\ngit config --global alias.glog \"log --graph\"\n```\n\n- `--oneline` 单行显示\n- `-<n>` 显示最近的 <n> 条日志\n- `-p` 显示变动\n- `--stat` 显示变动摘要\n\n查看、分析某一个提交:\n\n```\ngit show D --stat\ntag D\n...\n\ngit cat-file -p D^0\n```\n\n### 差异比较:git diff\n\n- 比较里程碑 B 和里程碑 A,用命令:git diff B A\n- 比较工作区和里程碑 A,用命令:git diff A\n- 比较暂存区和里程碑 A,用命令:git diff -cached A\n- 比较工作区和暂存区,用命令:git diff\n- 比较暂存区和 HEAD,用命令:git diff -cached\n- 比较工作区和 HEAD,用命令:git diff HEAD\n\n显示不同版本下的文件差异:\n\n```\ngit diff <commit1> <commit2> -- <paths>\n```\n\n非 Git 目录/文件的差异比较,可在版本库之外使用:\n\n```\ngit diff <path1> <path2>\n```\n\n- `--word-diff` 差异逐 _词_ 比较,而非缺省的逐 _行_ 比较\n\n### 文件追溯:git blame\n\n逐行显示文件,在每一行的行首显示此行最早是在什么版本引入的,由谁引入。\n\n只想查看某几行,使用 -L n,m 参数:\n\n```\ngit blame -L 6,+5 README\n```\n\n### 二分查找:git bisect\n\n定位问题代码。\n\n## 改变历史\n\n作为分布式版本控制系统,一旦版本库被多人共享,改变历史就可能是无法完成的任务。\n\n### 悔棋\n\n`git commit –amend` 单步悔棋,修补式提交。\n\n检出文件到前一版:\n\n```\ngit checkout HEAD^ -- src/hello.h\n```\n\n### 多步悔棋\n\n```\ngit reset --soft HEAD^^\n```\n\n### 回到未来\n\n拣选指令 `git cherry-pick` 从众多的提交中挑选出一个提交应用在当前的工作分支中。\n\n该命令需要提供一个提交 ID 作为参数,操作过程相当于将该提交导出为补丁文件,然后在当前 HEAD 上重放形成无论内容还是提交说明都一致的提交。\n\n操作例子:`A B C D E F` 6 次提交。\n\n例子 1.1:出掉 `D`:\n\n```\n# 将 HEAD 指针切换到 C\ngit checkout C\n\n# 拣选将 E 提交在当前 HEAD 上\ngit cherry-pick E\n\n# 拣选操作将 F 提交在当前 HEAD 上\ngit cherry-pick master\n\n# 将 master 分支指向新的提交 ID(f677821)上\ngit checkout master\ngit reset --hard HEAD@{1}\n```\n\n例子 1.2:D 融入 C:\n\n```\ngit checkout D\n\n# 将 C 和 D 融合\ngit reset --soft HEAD^^\n\n# 提交说明重用C提交的提交说明\ngit commit -C C\n\ngit cherry-pick E\ngit cherry-pick F\n```\n\ngit rebase 对提交执行变基操作,即可以实现将指定范围的提交“嫁接”到另外一个提交之上。\n\n```\ngit rebase --onto <newbase> <since> <till>\n```\n\n变基操作的过程:\n\n1. 首先执行 git checkout <till>,如果 till 不是一个分支,则变基操作是在 detached HEAD 分离头指针 状态的,当变基结束后,对 master 分支执行重置以实现把变基结果记录在分支中。\n2. 将<since>..<till>所标识的提交范围写到一个临时文件中。(<since>..<till>是指包括<till>的所有历史提交排除<since>以及<since>的历史提交后形成的版本范围)\n3. 当前分支强制重置(git reset --hard)到<newbase>。\n4. 从保存在临时文件中的提交列表中,一个一个将提交按照顺序重新提交到重置之后的分支上。\n5. 如果遇到提交已经在分支中包含,跳过该提交。\n6. 如果在提交过程遇到冲突,变基过程暂停。用户解决冲突后,执行 git rebase –continue 继续变基操作。或者执行 git rebase –skip 跳过此提交。或者执行 git rebase –abort 就此终止变基操作切换到变基前的分支上。\n\n例子 2.1:出掉 `D`:\n\n```\ngit rebase --onto C D F\n# or\ngit rebase --onto C E^ F\n```\n\n例子 2.2:D 融入 C:\n\n```\ngit checkout D\n\n# C和D融合\ngit reset --soft HEAD^^\n\ngit add .\n\n# 复用 C 的提交信息\ngit commit -C C\n\n# 记住这个提交 ID,可以用 tag 的方法\ngit tag newbase\n\ngit rebase --onto newbase E^ master\n```\n\n`-i` 交互式变基方法。\n\n例子 3.1:出掉 `D`:\n\n```\ngit rebase -i D^\n\n# d, drop = remove commit\n# 提交 D 标示修改为 d\n```\n\n例子 3.2:D 融入 C:\n\n```\ngit rebase -i C^\n\n# 提交 D 标示修改为 s\n```\n\n### 丢弃历史\n\n**重点内容第一次 Get**\n\n历史有的时候会成为负担。只保留最近的 100 次提交,抛弃之前的历史提交。那么应该如何操作呢?\n\n例:清除 tag A 之前的提交历史:\n\n```\n# 查看里程碑A指向的目录树\ngit cat-file -p A^{tree}\n\n# 使用git commit-tree命令直接从该目录树创建提交\necho \"Commit from tree of tag A.\" | git commit-tree A^{tree}\n8f7f94ba6a9d94ecc1c223aa4b311670599e1f86\n\n# 命令git commit-tree的输出是一个提交的SHA1哈希值。\n# 会发现这个提交没有历史提交,可以称之为孤儿提交。\ngit log 8f7f94ba6a9d94ecc1c223aa4b311670599e1f86\n\n# 将master分支从里程碑到最新的提交全部迁移到刚刚生成的孤儿提交上。\ngit rebase --onto 8f7f94ba6a9d94ecc1c223aa4b311670599e1f86 A master\n```\n\n### 反转提交\n\n```\ngit revert HEAD\n```\n\n## Git 克隆\n\n### 鸡蛋不装在一个篮子里\n\n```\n1. git clone <repository> <directory>\n2. git clone --bare <repository> <directory.git>\n3. git clone --mirror <repository> <directory.git>\n```\n\n一般约定俗成裸版本库的目录名以 `.git` 为后缀。\n\n用法 3 区别于用法 2 之处在于用法 3 克隆出来的裸版本对上游版本库进行了注册,这样可以在裸版本库中使用 git fetch 命令和上游版本库进行持续同步。\n\n### 克隆生成裸版本库\n\n```\ngit clone --bare /path/to/my/workspace/demo /path/to/repos/demo.git\n```\n\ndemo.git 目录就是版本库目录,不含工作区。\n\n```\ngit --git-dir=/path/to/repos/demo.git config core.bare\ntrue\n\n# 向其 push\ngit push /path/to/repos/demo.git\n```\n\n### 创建生成裸版本库\n\n```\ngit init --bare /path/to/repos/demo-init.git\n```\n\n## References\n\n- [2. Git 独奏 — GotGit](http://www.worldhello.net/gotgit/02-git-solo/index.html)\n","tags":["git"]},{"title":"iOS 招聘总结","url":"/2017/11/25/ios-recruitment-summary/","content":"\n自己也做了一次面试官。简历看到了很多,内容总结如下:\n\n## 一些感受\n\n- 一些基本的技能感觉不用写,不会吸引什么注意力。所以要精懂一些特别的技能,技术点。\n- 一些 “与产品沟通” “安排工作”,我觉的也不用写,也没什么吸引力。\n- 简历要体出自己擅长的地方,特别的地方,其他的一笔带过就可以。\n- 简历上的字词的细节我比较在意,iOS & IOS,Xcode & XCode。\n- 有 GitHub Blog 绝对的好评,但要有内容。\n- 基础知识、业务能力、性格、气场。\n\n<!-- more -->\n\n## 技能\n\n### 基础\n\n1. Objective-C Swift\n2. 内存管理机制,MRC & ARC\n3. 代理、通知、Block 回调机制 闭包\n4. KVC,KVO 机制\n5. CoreData FMBD 归档存储 SQLite NSUserDefaults\n6. runtime 运行机制,runloop 运行机制\n7. GCD NSOperation NSThread 多线程编程\n8. Socket 通信\n9. 单例 观察者\n10. MVVM\n11. RESTful\n\n### 业务\n\n1. 瀑布流 抽屉\n2. 断点续传 媒体流 瀑布流\n3. SSO 单点登陆\n\n### Framework\n\n1. Cocoapods\n2. 支付宝 高德 二维码扫描 友盟 极光推送 短信验证\n3. 微信 微博登陆 支付 in-app purchase\n4. SDWebImage AFNetworking Alamofire SnapKit MBProgressHUD\n5. IM 环信\n6. Spring 动画\n7. ICSDrawer 侧滑菜单\n8. AVOS Cloud SDK\n9. ZXing ZBar 二维码\n10. XMPP\n\n### 其他\n\n1. Axure\n2. 良好英文文档阅读能力\n\n## 职责\n\n1. 模块封装\n2. 上架\n3. 迭代\n4. 沟通\n5. 整体框架\n6. KVO 监听实现自定义下拉刷新\n7. 针对不同的网络状态 设置不同的缓存策略\n8. 各种 SDK 集成\n9. HTML5 Native 相互调用\n\n## 奇巧淫技\n\n自己对 iOS 的理解还并不深,如何面试更有经验的人?\n\n答:准备的问题可以自己并不太懂,只要问题靠谱,要做的就是多听。多听面试者的回答的状态,是含糊?是自信?是心虚?而且多听几位后也就大概其知道答案了。\n\n## 奇事\n\n- 因为有机试题所以不止一位怀疑这是个圈套,面试是为了做功能。\n\n## References\n\n- [招聘一个靠谱的 iOS](http://blog.sunnyxx.com/2015/07/04/ios-interview/)\n- [卓同学的 Swift 面试题](http://www.jianshu.com/p/7c7f4b4e4efe)\n","tags":["ios"]},{"title":"Photoshop 编辑 GIF","url":"/2017/11/23/photoshop-edit-gif/","content":"\n## 录制 GIF\n\n推荐使用 [LICEcap](https://www.cockos.com/licecap/) 小巧好用。\n\n## GIF 查看方法\n\n在 Mac 上双击 GIF 将是按帧查看一幅一幅的图片,按住 `空格` 可以动起来。也可以选择 GIF 图片直接点击空格预览。\n\n<!-- more -->\n\n## GIF 编辑\n\n### 预览\n\n在 Mac 中可以直接使用 `预览` 对 GIF 进行删除帧的操作。但是发现再次保存的 GIF 只播放一次,不会循环播放。\n\n### Photoshop\n\n使用 Photoshop 打开 GIF,点击 `窗口 -> 时间轴`,可在时间轴中对图片进行编辑。`文件 -> 导出 -> 存储为 Web 所用格式` 然后在 `循环选项` 选择 `永久`。\n\n在这里也可以降低 `图像大小` 对图片进行压缩。\n\n点击 `存储` 导出新的 GIF。\n\n-- EOF --\n","tags":["photoshop"]},{"title":"关于 GitHub README.md 中图片加载失败","url":"/2017/11/22/github-readme-content-length-exceeded/","content":"\n## 遇到的问题\n\n在编写 GitHub 的 README.md 后,其中引用的网络图片无法正常显示,点击 `alt` 的文字提示:`Content length exceeded`。\n\n<!-- more -->\n\n## 分析\n\n根据 [About anonymized image URLs](https://help.github.com/articles/about-anonymized-image-urls/) 这篇文章:上传的图片 URL 将被修改,所以个人信息将不会被跟踪。GitHub 将使用 [开源项目 Camo](https://github.com/atmos/camo)。Camo 将为每一个图片生成一个以 `https://camo.githubusercontent.com/` 匿名代理 URL 同时隐藏来自其他用户的浏览器详细信息和相关信息。\n\n我引用的 GIF 图片有 **7MB** 多,那么图片大小的限制是多少?\n\n[camo - server.coffee#L18](https://github.com/atmos/camo/blob/master/server.coffee#L18)\n\n```txt\ncontent_length_limit = parseInt(process.env.CAMO_LENGTH_LIMIT || 5242880, 10)\n```\n\n换算后大小正好是 **5MB**。\n\n## References\n\n- [关于 GitHub 无法图片加载的问题](http://soyaine.cn/blog/2016/12/31/soyaine-daily-070)\n","tags":["git"]},{"title":"Linux Mac 使用代理连接 SSH","url":"/2017/10/28/linux-or-mac-ssh-by-proxy/","content":"\n## Ubuntu\n\n```bash\nssh -oProxyCommand=\"nc -x 127.0.0.1:1080 %h %p\" [email protected]\n```\n\n## Mac\n\n```bash\nssh -o \"ProxyCommand nc -X 5 -x 127.0.0.1:1080 %h %p\" [email protected]\n```\n\n<!-- more -->\n\n## SecoureCRT\n\nSession Options - Connection - SSH2 - Firewall,创建、选择代理。\n\n## 参数\n\n- `-o ProxyCommand`:SSH 命令选项,你可以理解成使用 “在 SSH 中使用代理”。\n- `nc`:netcat 命令。\n- `127.0.0.1:1080`:本地 Shadowsocks 的监听地址和监听端口。\n\n## 命令行 HTTP 代理\n\n```bash\nexport http_proxy=http://127.0.0.1:1087;export https_proxy=http://127.0.0.1:1087;\n```\n\n## 鉴别自己是否真的使用了代理来登陆服务器\n\n```bash\nroot@ubuntu:~# who\nroot pts/2 2017-05-13 18:13 (xxx.xxx.xxx.xxx)\n```\n\n## References\n\n- [Mac OS 使用 shadowsock 来代理 ssh 访问服务器](https://www.goodspb.net/mac-os-%E4%BD%BF%E7%94%A8-shadowsock-%E6%9D%A5%E4%BB%A3%E7%90%86-ssh-%E8%AE%BF%E9%97%AE%E6%9C%8D%E5%8A%A1%E5%99%A8/)\n\n-- EOF --\n","tags":["linux","mac"]},{"title":"MacBook 使用小感","url":"/2017/10/28/simple-macbook-report/","content":"\n现在自己的开发主力机是:2015 版的 MacBook Pro。对于工程师来讲,MacBook 绝对是用来提高生产力的,不是装杯的。如果条件允许十分推荐,反正我是回不来了。\n\n<!-- more -->\n\n## 一些感受\n\n- 购买型号选择:15 寸 i7 Pro 没的说,屏大看的很爽。\n- F1... 等键并有没有原来想象的那么重要,游览器刷新什么的都有代替的键。我感觉 F 系列的键默认为功能键挺好的。Filco 键盘开始吃灰。\n- 续航时间是真的长,待机功耗很小,不用关机。扣上盖子装包带走,不拿充电线。\n- 触控板真的好用。原来都是没鼠标时不得已才用触控板。(后来发现老司机们都爱触控板)\n- 鼠标 - 滚动方向:自然,需要适应。\n- Chrome Tab 随便开。\n- 很安静。\n- 装包时边沿有些割手。\n\n## 软件推荐\n\n**2020-03-25 更新:** [My MacBook | Yifans_Z](/2019/05/20/my-macbook/)。\n\n-- EOF --\n","tags":["mac"]},{"title":"Git checkout --theirs --ours 解决冲突文件","url":"/2017/08/30/git-checkout-theirs-resolve-conflict/","content":"\n在代码合并时遇到 `conflict` 是常有的事情,有些内容是自动生成的资源文件,手工处理起来很麻烦,某一文件如何全部以某一分支的内容为准?\n\n使用 `checkout --theirs .` `checkout --ours .`。\n\n<!-- more -->\n\n## 场景实例\n\nEngineer A、B 同时从 master checkout 自己的功能分支:\n\n```bash\n# Engineer A\ngit checkout -b feature-a\n\n# Engineer B\ngit checkout -b feature-b\n```\n\nEngineer A 开发比较快先进行了 push:\n\n```bash\ngit add .\ngit commit -m \"feat: feature-a\"\ngit checkout master\ngit merge feature-a\ngit push\n```\n\nEngineer B 再 push 前需要拉最新的 master,因为改到了同一行代码,出现代码冲突。\n\n### merge 合并代码\n\n使用 merge 命令导致的代码冲突。\n\n```bash\ngit checkout master\ngit pull\ngit merge feature-a\n```\n\n在处理冲突的过程中:\n\n```bash\n# 以自己(Engineer B)的代码为准\ngit checkout --ours .\ngit checkout --ours file.txt\n\n# 以他人(Engineer A)的代码为准\ngit checkout --theirs .\n```\n\n## rebase 合并代码\n\n使用 rebase 命令导致的代码冲突。\n\n```bash\ngit checkout master\ngit merge feature-a\ngit pull --rebase\n```\n\n在处理冲突的过程中:\n\n```bash\n# 以自己(Engineer B)的代码为准;与 merge 相反,可以理解为 rebase 时主干是他人的分支\ngit checkout --theirs .\n\n# 以他人(Engineer A)的代码为准;与 merge 相反,可以理解为 rebase 时主干是他人的分支\ngit checkout --ours .\n```\n\n## References\n\n- [化解冲突:git merge 与 git rebase 中的 ours 和 theirs](https://bitmingw.com/2017/02/16/git-merge-rebase-ours-and-theirs/)\n","tags":["git"]},{"title":"【Git 权威指南】读书笔记 - 独奏 - Part 3","url":"/2017/08/03/got-git-reading-notes-solo-part3/","content":"\n主要内容:【Git 基本操作】\n\n## Git 基本操作\n\n### 先来合个影\n\n在 Git 里,“留影”用的命令叫做 `tag`,更加专业的术语叫做“里程碑”(打 tag,或打标签)。\n\n```bash\ngit tag -m \"Say bye-bye to all previous practice.\" old_practice\n```\n\n里程碑无非也是一个引用,通过记录提交 ID(或者创建 Tag 对象)来为当前版本库状态进行“留影”。\n\n```bash\ngit rev-parse refs/tags/old_practice\n41bd4e2cce0f8baa9bb4cdda62927b408c846cd6\n```\n\n`git describe` 显示当前版本库的最新提交的版本号。格式:`最近的 tag - 距离此 tag 的个数 - 该提交的 SHA1`\n\n```bash\ngit describe\nold_practice\n\n# .. commit something\n\ngit describe\nold_practice-2-g8861c65\n```\n\n<!-- more -->\n\n### 删除文件\n\n`rm *.txt` 针对的是 `工作区`,对 `暂存区` 和 `版本库` 没有任何影响。\n\n`git rm detached-commit.txt hack-1.txt new-commit.txt welcome.txt` 删除动作加入了暂存区,commit 后在版本库罪行提交中删除了,在历史提交中尚在。\n\n可以通过下面命令查看历史版本的文件列表:\n\n```\ngit ls-files --with-tree=HEAD^\ndetached-commit.txt\nnew-commit.txt\nwelcome.txt\n```\n\n查看在历史版本中尚在的删除文件的内容:\n\n```\ngit cat-file -p HEAD^:welcome.txt\nHello.\nNice to meet you.\n```\n\n`git add -u` 命令会对工作区中 _所有改动_ 以及 _删除文件_ 添加到暂存区。\n\n### 恢复删除的文件\n\n```\ngit cat-file -p HEAD~1:welcome.txt > welcome.txt\n```\n\n然后加入暂存区,提交到版本库:\n\n```\ngit add -A\ngit commit -m \"restore file: welcome.txt\"\n```\n\n这是通过再次添加的方式恢复被删除的文件。\n\n`git add -A` 命令会对工作区中 _所有改动_ 以及 _新增文件_ 添加到暂存区。\n\n### 移动文件\n\n```\ngit mv welcome.txt README\n```\n\n### 交互界面\n\n```\ngit add -i\n```\n\n### 文件忽略\n\n```\ncat > .gitignore << EOF\n> hello\n> *.o\n> *.h\n> EOF\n```\n\n查看被忽略的文件:\n\n```\ngit status --ignored -s\n```\n\n将忽略的文件强制添加到仓库中:\n\n```\ngit add -f hello.h\ngit commit -m \"add hello.h\"\n[master 48456ab] add hello.h\n 1 files changed, 1 insertions(+), 0 deletions(-)\n create mode 100644 src/hello.h\n```\n\n_忽略只对未跟踪文件有效,对于已加入版本库的文件无效。_ 将 `hello.h` 添加到版本库后,对 `hello.h` 的修改都会立刻被跟踪到。`.gitignore` 只是对未入库的文件起作用。\n\n**本地独享式忽略文件**\n\n.gitignore 设置的文件忽略是共享式的,是因为.gitignore 被添加到版本库后成为了版本库的一部分,文件忽略在他人的工作区中同样生效。\n\n享式忽略有两种方式:\n\n- 在版本库.git 目录下的一个文件.git/info/exclude 来设置文件忽略。\n- 通过 Git 的配置变量 core.excludesfile 指定的一个忽略文件,其设置的忽略对所有文件均有效。\n\n```\ngit config --global core.excludesfile /home/jiangxin/_gitignore\ngit config core.excludesfile\n/home/jiangxin/_gitignore\n\ncat /home/jiangxin/_gitignore\n*~ # vim 临时文件\n*.pyc # python 的编译文件\n.*.mmx # 不是正则表达式哦,因为 FreeMind-MMX 的辅助文件以点开头\n```\n\n**Git 忽略语法**\n\n- `#` 表示开始的行被忽略。\n- 通配符。\\* 任意多字符,? 一个字符,[abc] 可选字符范围。\n- `最后面是一个 /` 表示忽略的是整个目录,同名文件不忽略,否则同名的文件和目录都忽略。\n- `最前面 !` 表示不忽略。\n\n```\n# 这是注释行 —— 被忽略\n*.a # 忽略所有以 .a 为扩展名的文件。\n!lib.a # 但是 lib.a 文件或者目录不要忽略,即使前面设置了对 *.a 的忽略。\n/TODO # 只忽略根目录下的 TODO 文件,子目录的 TODO 文件不忽略。\nbuild/ # 忽略所有 build/ 目录下的文件。\ndoc/*.txt # 忽略文件如 doc/notes.txt,但是文件如 doc/server/arch.txt 不被忽略。\n```\n\n### 文件归档\n\n如果想压缩工作区文件归档,可能一不小心会把版本库(.git 目录)包含其中,甚至将工作区中的忽略文件、临时文件也包含其中。所以 Git 提供了一个归档命令 `git archive`。\n\n```\n# 基于最新提交建立归档文件latest.zip\ngit archive -o latest.zip HEAD\n\n# 只将目录 src 和 doc 建立到归档 partial.tar 中\ngit archive -o partial.tar HEAD src doc\n\n# 基于里程碑v1.0建立归档,并且为归档中文件添加目录前缀1.0\ngit archive --format=tar --prefix=1.0/ v1.0 | gzip > foo-1.0.tar.gz\n```\n\n## References\n\n- [2. Git 独奏 — GotGit](http://www.worldhello.net/gotgit/02-git-solo/index.html)\n","tags":["git"]},{"title":"【Git 权威指南】读书笔记 - 独奏 - Part 2","url":"/2017/07/20/got-git-reading-notes-solo-part2/","content":"\n主要内容:【Git 重置】、【Git 检出】、【恢复进度】\n\n## Git 重置\n\n### 分支游标 master 的探秘\n\n```bash\ngit log --graph --oneline\n* e695606 which version checked in?\n* a0c641e who does commit?\n* 9e8a761 initialized.\n```\n\n引用 `refs/heads/master` 就好像是一个游标,在有新的提交发生的时候指向了新的提交。\n\nGit 提供了 `git reset` 命令,可以将“游标”指向任意一个存在的提交 ID。注意下面的命令中使用了 `--hard` 参数,会破坏工作区未提交的改动,慎用。\n\n```bash\ngit reset --hard HEAD^\nHEAD is now at e695606 which version checked in?\n```\n\n<!-- more -->\n\n### 用 reflog 挽救错误的重置\n\n通过 `.git/logs` 目录下日志文件记录了分支的变更。默认非裸版本库(带有工作区)都提供分支日志功能,这是因为带有工作区的版本库都有如下设置:\n\n```bash\ngit config core.logallrefupdates\ntrue\n```\n\n查看一下 `master` 分支的日志文件 `.git/logs/refs/heads/master` 中的内容。\n\n```bash\ntail -5 .git/logs/refs/heads/master\n```\n\nGit 提供了一个 `git reflog` 命令,对这个文件进行操作。\n\n```bash\ngit reflog show master | head -5\n9e8a761 master@{0}: 9e8a761: updating HEAD\ne695606 master@{1}: HEAD^: updating HEAD\n4902dc3 master@{2}: commit: does master follow this new commit?\n...\n```\n\n重置 master 为两次改变之前的值。\n\n```bash\ngit reset --hard master@{2}\n```\n\n### 深入了解 git reset 命令\n\n```bash\ngit reset [-q] [<commit>] [--] <paths>...\ngit reset [--soft | --mixed | --hard | --merge | --keep] [-q] [<commit>]\n```\n\n为了避免路径和引用(或者提交 ID)同名而冲突,可以在 `<paths>` 前用两个连续的短线(减号)作为分隔。\n\n![20-got-git-reading-notes-solo-git-reset](https://user-images.githubusercontent.com/9289792/80202770-b5966d80-8658-11ea-92e4-c348b8f12313.png)\n\n- `--hard` 会执行上图中的 1、2、3 全部的三个动作。\n\n1. 替换引用的指向。引用指向新的提交 ID。\n2. 替换暂存区。替换后,暂存区的内容和引用指向的目录树一致。\n3. 替换工作区。替换后,工作区的内容变得和暂存区一致,也和 HEAD 所指向的目录树内容相同。\n\n- `--soft` 会执行上图中的操作 1。\n- `--mixed`(缺省)会执行上图中的操作 1 和操作 2。\n\n```bash\ngit reset\ngit reset HEAD\n# 仅用 HEAD 指向的目录树重置暂存区,工作区不会受到影响,相当于将之前用 git add 命令更新到暂存区的内容撤出暂存区。引用也未改变,因为引用重置到 HEAD 相当于没有重置。\n\ngit reset -- filename\ngit reset HEAD filename\n# 仅将文件 filename 撤出暂存区,暂存区中其他文件不改变。相当于对命令 git add filename 的反向操作。\n\ngit reset --soft HEAD^\n# 工作区和暂存区不改变,但是引用向前回退一次。当对最新提交的提交说明或者提交的更改不满意时,撤销最新的提交以便重新提交。git commit 的反向操作。\n# 在之前曾经介绍过一个修补提交命令 git commit --amend,用于对最新的提交进行重新提交以修补错误的提交说明或者错误的提交文件。修补提交命令实际上相当于执行了下面两条命令。(注:文件 .git/COMMIT_EDITMSG 保存了上次的提交日志)\ngit reset --soft HEAD^\ngit commit -e -F .git/COMMIT_EDITMSG\n\ngit reset HEAD^\n# 工作区不改变,但是暂存区会回退到上一次提交之前,引用也会回退一次。\n\ngit reset --hard HEAD^\n# 彻底撤销最近的提交。引用回退到前一次,而且工作区和暂存区都会回退到上一次提交的状态。自上一次以来的提交全部丢失。\n```\n\n## Git 检出\n\n重置命令 `git reset` 的一个用途就是修改引用(如 master)的游标。如果 HEAD 要改变该如何改变呢?检出命令 `git checkout` 该命令的实质就是修改 HEAD 本身的指向,该命令不会影响分支“游标”(如 master)。\n\n### HEAD 的重置即检出\n\n```bash\ngit co HEAD^\nNote: checking out 'HEAD^'.\n\nYou are in 'detached HEAD' state. You can look around, make experimental\nchanges and commit them, and you can discard any commits you make in this\nstate without impacting any branches by performing another checkout.\n# 您现在处于 '分离头指针' 状态。您可以检查、测试和提交,而不影响任何分支。\n# 通过执行另外的一个 checkout 检出指令会丢弃在此状态下的修改和提交。\n\nIf you want to create a new branch to retain commits you create, you may\ndo so (now or later) by using -b with the checkout command again. Example:\n# 如果想保留在此状态下的修改和提交,使用 -b 参数调用 checkout 检出指令以\n# 创建新的跟踪分支。如:\n\n git checkout -b <new-branch-name>\n\nHEAD is now at 3175afd...\n```\n\n什么叫做 `detached HEAD` “分离头指针”状态?查看一下此时 HEAD 的内容就明白了。\n\n```\ncat .git/HEAD\n3175afde9450a1dc40b09d05a012b45e967cb80f\n```\n\n原来“分离头指针”状态指的就是 HEAD 头指针指向了一个具体的提交 ID,而不是一个引用(分支)。注意上面的 `reflog` 是 `HEAD` 头指针的变迁记录,而非 `master` 分支。\n\n查看一下 HEAD 和 master 对应的提交 ID,会发现现在它们指向的不一样。\n\n```bash\ngit rev-parse HEAD master\n3175afde9450a1dc40b09d05a012b45e967cb80f\nbd08cb462d38b54b930cf1934b0c33f2e4592390\n```\n\n在“分离头指针”模式仍然可以进行提交:\n\n```bash\ngit status\nHEAD detached at 3175afd\n...\n```\n\n但是在 checkout 到其他分支时,刚才的提交会丢失,但是这个提交仍然在版本库中,由于这个提交没有被任何分支跟踪到,因此并不能保证这个提交会永久存在。\n\n实际上当 reflog 中含有该提交的日志过期后,这个提交随时都会从版本库中彻底清除。\n\n### 挽救分离头指针\n\n在“分离头指针”模式下进行的测试提交除了使用提交 ID `acc2f69` 访问之外,不能通过 master 分支或其他引用访问到。使用合并操作 `git merge` 将提交 acc2f69 合并到 master 分支中来。\n\n```bash\ngit merge acc2f69\nMerge made by recursive.\n...\n```\n\n## 恢复进度\n\n### 继续暂存区未完成的实践\n\n```bash\n# 保存当前工作进度\ngit stash\n\n# 查看保存的进度用命令\ngit stash list\n\n# 最近保存的进度进行恢复\ngit stash pop\ngit stash pop [--index] [<stash>]\n# --index 除了恢复工作区的文件外,还尝试恢复暂存区\n# 从该 <stash> 中恢复\n```\n\n```\ngit stash [save [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet]\n [-u|--include-untracked] [-a|--all] [<message>]]\n```\n\n- `--patch` 会显示工作区和 HEAD 的差异,通过对差异文件的编辑决定在进度中最终要保存的工作区的内容,通过编辑差异文件可以在进度中排除无关内容。\n- 使用 `-k` 或者 `--keep-index` 参数,在保存进度后不会将暂存区重置。\n\n```bash\n# 不删除恢复的进度之外,其余和 git stash pop 命令一样\ngit stash apply [--index] [<stash>]\n\n# 删除一个存储的进度\ngit stash drop [<stash>]\n\n# 删除所有存储的进度\ngit stash clear\n\n# 基于进度创建分支\ngit stash branch <branchname> <stash>\n```\n\n### 探秘 git stash\n\n在执行 `git stash` 命令时,Git 实际调用了一个脚本文件实现相关的功能。\n\n```bash\ngit --exec-path\n/usr/lib/git-core\n\nfile /usr/lib/git-core/git-stash\n/usr/lib/git-core/git-stash: POSIX shell script text executable\n```\n\n本地没有被版本控制系统跟踪的文件并不能保存进度。因此本地新文件需要执行添加 `add` 再执行 git stash 命令。\n\n在用 `git stash` 命令保存进度时,提供说明更容易找到对应的进度文件。\n\n每个进度的标识都是 `stash@{<n>}` 格式,像极了前面介绍的 `reflog` 的格式。`git stash` 的就是用到了前面介绍的引用和引用变更日志 `reflog` 来实现的。\n\n用 git stash 保存进度,实际上会将进度保存在引用 refs/stash 所指向的提交中。多次的进度保存,实际上相当于引用 refs/stash 一次又一次的变化,而 refs/stash 引用的变化由 reflog(即.git/logs/refs/stash)所记录下来。\n\n### 如何在引用 refs/stash 中同时保存暂存区的进度和工作区中的进度\n\n```\ngit log --graph --pretty=raw refs/stash -2\n* commit e5c0cdc2dedc3e50e6b72a683d928e19a1d9de48\n|\\ tree 780c22449b7ff67e2820e09a6332c360ddc80578\n| | parent 2b31c199d5b81099d2ecd91619027ab63e8974ef\n| | parent c5edbdcc90addb06577ff60f644acd1542369194\n| | author Jiang Xin <[email protected]> 1291623066 +0800\n| | committer Jiang Xin <[email protected]> 1291623066 +0800\n| |\n| | WIP on master: 2b31c19 Merge commit 'acc2f69'\n| |\n| * commit c5edbdcc90addb06577ff60f644acd1542369194\n|/ tree 780c22449b7ff67e2820e09a6332c360ddc80578\n| parent 2b31c199d5b81099d2ecd91619027ab63e8974ef\n| author Jiang Xin <[email protected]> 1291623066 +0800\n| committer Jiang Xin <[email protected]> 1291623066 +0800\n|\n| index on master: 2b31c19 Merge commit 'acc2f69'\n```\n\n最新的提交说明中有 `WIP`(Work In Progess)字样,说明代表了工作区进度。而最新提交的第二个父提交(上图中显示为第二个提交)有 index on master 字样,说明这个提交代表着暂存区的进度。\n\n## References\n\n- [2. Git 独奏 — GotGit](http://www.worldhello.net/gotgit/02-git-solo/index.html)\n","tags":["git"]},{"title":"【Git 权威指南】读书笔记 - 独奏 - Part 1","url":"/2017/07/19/got-git-reading-notes-solo-part1/","content":"\n主要内容:【Git 初始化】、【Git 暂存区】、【Git 对象】\n\n## Git 初始化\n\n设置一下 Git 的环境变量,这个设置是一次性的工作。即这些设置会在全局文件(用户主目录下的 `~/.gitconfig`)或系统文件(`/etc/gitconfig`)中做永久的记录。\n\n配置的用户名和邮件地址将在版本库提交时作为提交者的用户名和邮件地址。\n\n```bash\ngit config --global user.name \"Jiang Xin\"\ngit config --global user.email [email protected]\n```\n\n### 设置一些 Git 别名,以便可以使用更为简洁的子命令\n\n只在本用户的全局配置中添加 Git 命令别名:\n\n```bash\ngit config --global alias.br branch\ngit config --global alias.ci \"commit -s\"\ngit config --global alias.co checkout\ngit config --global alias.st \"-p status\"\n```\n\n<!-- more -->\n\n### 版本库的初始化\n\n```bash\nmkdir demo\ncd demo\ngit init\n```\n\n初始化空的 Git 版本库于 `/path/to/my/workspace/demo/.git/`\n\ngit init 命令的后面直接输入目录名称\n\n```bash\ncd /path/to/my/workspace\ngit init demo\n```\n\n```bash\nls -aF\n./ ../ .git/\n```\n\n这个隐藏的 `.git` 目录就是 Git 版本库(又叫仓库,repository)。\n\n`.git` 版本库目录所在的目录,即 `/path/to/my/workspace/demo` 目录称为 **工作区**。\n\n将新建立的文件添加到版本库\n\n```bash\ngit add welcome.txt\n```\n\n再执行一次提交操作,使用 `-m` 参数直接给出了提交说明。\n\n```bash\ngit ci -m \"initialized\"\n[master (root-commit) 7f0b2be] init\n 1 file changed, 0 insertions(+), 0 deletions(-)\n create mode 100644 welcome.txt\n```\n\n`git ci` 是上面配置的别名,我个人觉得 `-s` 这个参数比较冗余。\n\n### 思考:为什么工作区下有一个 `.git` 目录?\n\nGit 的这种设计,将版本库放在工作区根目录下,所有的版本控制操作(除了和其他远程版本库之间的互操作)都在本地即可完成,不像 Subversion 只有寥寥无几的几个命令才能脱离网络执行。而且 Git 也没有 CVS 和 Subversion 的安全泄漏问题(只要保护好 .git 目录),也没有 Subversion 在本地文件搜索时出现搜索结果混乱的问题,甚至 Git 还提供了一条 `git grep` 命令来更好地搜索工作区的文件内容。\n\n```\ngit grep \"工作区文件内容搜索\"\n```\n\n### 当工作区中包含了子目录,在子目录中执行 Git 命令时,如何定位版本库呢?\n\n当在 Git 工作区目录下执行操作的时候,会对目录依次向上递归查找 `.git` 目录,找到的 `.git` 目录就是工作区对应的版本库,`.git` 所在的目录就是工作区的根目录,文件 `.git/index` 记录了工作区文件的状态(实际上是 **暂存区** 的状态)。\n\n如果跟踪一下执行 `git status` 命令时的磁盘访问,会看到沿目录依次向上递归的过程。\n\n```\nstrace -e 'trace=file' git status\n```\n\n### 那么有什么办法知道 Git 版本库的位置,以及工作区的根目录在哪里呢?\n\n显示版本库 `.git` 目录所在的位置。\n\n```bash\ngit rev-parse --git-dir\n/path/to/my/workspace/demo/.git\n```\n\n显示工作区根目录。\n\n```bash\ngit rev-parse --show-toplevel\n/path/to/my/workspace/demo\n```\n\n### 把版本库 `.git` 目录放在工作区,是不是太不安全了?\n\nGit 克隆可以降低因为版本库和工作区混杂在一起导致的版本库被破坏的风险。在本机另外的磁盘/目录中建立 Git 克隆,并在工作区有改动提交时,手动或自动地执行向克隆版本库的推送 `git push` 操作。如果使用网络协议,还可以实现在其他机器上建立克隆,这样就更安全了(双机备份)。\n\n### 思考:`git config` 命令参数的区别?\n\n将打开 /path/to/my/workspace/demo/.git/config 文件进行编辑:\n\n```bash\ncd /path/to/my/workspace/demo/\ngit config -e\n```\n\n将打开 /home/jiangxin/.gitconfig(用户主目录下的 .gitconfig 文件)全局配置文件进行编辑:\n\n```bash\ngit config -e --global\n```\n\n将打开 /etc/gitconfig 系统级配置文件进行编辑:\n\n```bash\ngit config -e --system\n```\n\nGit 的三个配置文件分别是 `版本库级别的配置文件`、`全局配置文件`(用户主目录下)和 `系统级配置文件`(/etc 目录下)。\n\n其中 `版本库级别配置文件` 的优先级最高,`全局配置文件` 其次,`系统级配置文件` 优先级最低。\n\nGit 配置文件采用的是 INI 文件格式。\n\n```ini\ncat /path/to/my/workspace/demo/.git/config\n[core]\n repositoryformatversion = 0\n filemode = true\n bare = false\n logallrefupdates = true\n```\n\n例如读取 `[core]` 小节的 `bare` 的属性值\n\n```bash\ngit config <section>.<key>\n\ngit config core.bare\nfalse\n```\n\n更改或设置 INI 文件中某个属性\n\n```bash\ngit config <section>.<key> <value>\n\ngit config a.b something\ngit config x.y.z others\n```\n\n打开 `.git/config` 文件\n\n```bash\n[a]\n b = something\n[x \"y\"]\n z = others\n```\n\n可以用 `git config` 命令操作任何其他的 INI 文件\n\n```bash\nGIT_CONFIG=test.ini git config a.b.c.d \"hello, world\"\n```\n\n```bash\nGIT_CONFIG=test.ini git config a.b.c.d\nhello, world\n```\n\n### 思考:是谁完成的提交?\n\n当最新的提交删除了 `user.name` 和 `user.email`,提交时 Git 对提交者的用户名和邮件地址做了大胆的猜测,这个猜测可能是错的。\n\n重新设置 `user.name` 和 `user.email`,然后执行下面的命令,重新修改最新的提交,改正作者和提交者的错误信息。\n\n```bash\ngit commit --amend --allow-empty --reset-author\n```\n\n- 参数 `--amend` 是对刚刚的提交进行修补,这样就可以改正前面错误的提交(用户信息错误),而不会产生另外的新提交。\n- 参数 `--allow-empty` 是因为要进行修补的提交实际上是一个空白提交,Git 默认不允许空白提交。\n- 参数 `--reset-author` 的含义是将 Author(提交者)的 ID 重置,否则只会影响最新的 Commit(提交者)的 ID。这条命令也会重置 AuthorDate 信息。\n\n### 思考:随意设置提交者姓名,是否太不安全?\n\nGit 可以随意设置提交的用户名和邮件地址信息,这是分布式版本控制系统的特性使然,每个人都是自己版本库的主人,很难也没有必要进行身份认证从而使用经过认证的用户名作为提交的用户名。\n\n但是可以使用 GitLab 等服务管理权限。\n\n### 思考:命令别名是干什么的?\n\n命令别名可以帮助用户解决从其他版本控制系统迁移到 Git 后的使用习惯问题。\n\n### 备份本章的工作成果\n\n```bash\ncd /path/to/my/workspace\ngit clone demo demo-step-1\nCloning into demo-step-1...\ndone.\n```\n\n## Git 暂存区\n\n```bash\ngit log --stat\n```\n\n可以用 `git log` 查看提交日志,附加的 `--stat` 参数看到每次提交的文件变更统计。\n\n### 修改不能直接提交?\n\n现在就将修改的文件“添加”到提交暂存区:\n\n```bash\ngit add welcome.txt\n```\n\n这时如果和 HEAD(当前版本库的头指针)或者 master 分支(当前工作分支)进行比较,会发现有差异。这个差异才是正常的,因为尚未真正提交么。\n\n```bash\ngit diff HEAD\n```\n\n用简洁方式显示状态\n\n```bash\ngit status -s\nM welcome.txt\n```\n\n通过参数 `--cached` 或者 `--staged` 参数调用 git diff 命令,看到的是提交暂存区 `stage` 和版本库中文件的差异。不然看到的是工作区的变动。\n\n```bash\ngit diff --cached\n```\n\n现在执行 git commit 命令进行提交。\n\n```bash\ngit commit -m \"which version checked in?\"\n```\n\n如何证明提交成功了呢?通过查看提交日志,看到了新的提交。\n\n```bash\ngit log --pretty=oneline\n```\n\n### 理解 Git 暂存区 stage\n\n当执行 `git status` 命令(或者 `git diff` 命令)扫描工作区改动的时候,先依据 `.git/index` 文件中记录的(工作区跟踪文件的)时间戳、长度等信息判断工作区文件是否改变。\n\n文件 `.git/index` 实际上就是一个包含文件索引的目录树,像是一个虚拟的工作区。在这个虚拟工作区的目录树中,记录了文件名、文件的状态信息(时间戳、文件长度等)。文件的内容并不存储其中,而是保存在 Git 对象库 `.git/objects` 目录中,文件索引建立了文件和对象库中对象实体之间的对应。\n\n![got-git-reading-notes-solo-git-stage](https://user-images.githubusercontent.com/9289792/80202431-33a64480-8658-11ea-8771-07b2bf81f657.png)\n\n- 图中可以看出此时 HEAD 实际是指向 master 分支的一个“游标”。所以图示的命令中出现 HEAD 的地方可以用 master 来替换。\n- 图中的 objects 标识的区域为 Git 的对象库,实际位于 `.git/objects` 目录下,会在后面的章节重点介绍。\n- 当执行 `git reset HEAD` 命令时,暂存区的目录树会被重写,被 master 分支指向的目录树所替换,但是工作区不受影响。\n- 当执行 `git rm --cached <file>` 命令时,会直接从暂存区删除文件,工作区则不做出改变。\n- 当执行 `git checkout .` 或者 `git checkout -- <file>` 命令时,会用暂存区全部或指定的文件替换工作区的文件。这个操作很危险,会清除工作区中未添加到暂存区的改动。\n- 当执行 `git checkout HEAD .` 或者 `git checkout HEAD <file>` 命令时,会用 HEAD 指向的 master 分支中的全部或者部分文件替换暂存区和以及工作区中的文件。这个命令也是极具危险性的,因为不但会清除工作区中未提交的改动,也会清除暂存区中未提交的改动。\n\n### Git diff 魔法\n\n有什么办法能够像查看工作区一样的,直观的查看暂存区以及 HEAD 当中的目录树么?\n\n```bash\ngit ls-tree -l HEAD\n100644 blob fd3c069c1de4f4bc9b15940f490aeb48852f3c42 25 welcome.txt\n```\n\n要显示暂存区的目录树,可以使用 `git ls-files` 命令。\n\n```bash\ngit ls-files -s\n100644 18832d35117ef2f013c4009f5b2128dfaeff354f 0 a/b/c/hello.txt\n```\n\n![got-git-reading-notes-solo-git-diff](https://user-images.githubusercontent.com/9289792/80202327-06599680-8658-11ea-8329-93b424bc3726.png)\n\n### 不要使用 git commit -a\n\n提交命令 `git commit` 可以带上 `-a` 参数,对本地所有变更的文件执行提交操作,包括本地修改的文件,删除的文件,但不包括未被版本库跟踪的文件。\n\n这个“偷懒”的提交命令,就会丢掉 Git 暂存区带给用户最大的好处:对提交内容进行控制的能力。\n\n## Git 对象\n\n什么是 `HEAD`?什么是 `master`?为什么它们二者可以相互替换使用?为什么 Git 中的很多对象像提交、树、文件内容等都用 40 位的 `SHA1` 哈希值来表示?\n\n### Git 对象库探秘\n\n40 位十六进制数字组成的 `SHA1` 哈希值\n\n```bash\ngit log -1 --pretty=raw\n# 这是本次提交的唯一标识。\ncommit e695606fc5e31b2ff9038a48a3d363f4c21a3d86\n# 这是本次提交所对应的目录树。\ntree f58da9a820e3fd9d84ab2ca2f1b467ac265038f9\n# 这是本地提交的父提交(上一次提交)。\nparent a0c641e92b10d8bcca1ed1bf84ca80340fdefee6\n\n which version checked in?\n```\n\n研究 Git 对象 ID 的命令是 `git cat-file`,用下面的命令可以查看一下这三个 ID 的类型。\n\n```bash\ngit cat-file -t e695606\ncommit\ngit cat-file -t f58d\ntree\ngit cat-file -t fd3c06\nblob\n```\n\n<!-- more -->\n\n再用 `git cat-file` 命令查看一下这几个对象的内容。对于 `blob` 对象,这个对象保存着文件 welcome.txt 的内容。\n\n```bash\ngit cat-file -p fd3c06\nHello.\nNice to meet you.\n```\n\n这个写对象都存在 Git 库中的 `objects` 目录下,ID 的前两位作为目录名,后 38 位作为文件名。\n\n```bash\nfor id in e695606 f58da9a a0c641e fd3c069; do \\\n ls .git/objects/${id:0:2}/${id:2}*; done\n.git/objects/e6/95606fc5e31b2ff9038a48a3d363f4c21a3d86\n.git/objects/f5/8da9a820e3fd9d84ab2ca2f1b467ac265038f9\n.git/objects/a0/c641e92b10d8bcca1ed1bf84ca80340fdefee6\n.git/objects/fd/3c069c1de4f4bc9b15940f490aeb48852f3c42\n```\n\n![got-git-reading-notes-solo-git-objects](https://user-images.githubusercontent.com/9289792/80202330-08235a00-8658-11ea-9040-b280b00b7e43.png)\n\n### HEAD 和 master 的奥秘\n\n```bash\ngit log -1 HEAD\ngit log -1 master\ngit log -1 refs/heads/master\n```\n\n在当前版本库中,`HEAD`、`master` 和 `refs/heads/master` 具有相同的指向。现在到版本库 `.git` 中一探它们的究竟:\n\n```bash\nfind .git -name HEAD -o -name master\n.git/HEAD\n.git/logs/HEAD\n.git/logs/refs/heads/master\n.git/refs/heads/master\n```\n\n显示一下 `.git/HEAD`\n\n```bash\ncat .git/HEAD\nref: refs/heads/master\n```\n\n指向一个引用:`refs/heads/master`\n\n```bash\ncat .git/refs/heads/master\ne695606fc5e31b2ff9038a48a3d363f4c21a3d86\n```\n\n显示该提交的内容\n\n```bash\ngit cat-file -p e695606fc5e31b2ff9038a48a3d363f4c21a3d86\ntree f58da9a820e3fd9d84ab2ca2f1b467ac265038f9\nparent a0c641e92b10d8bcca1ed1bf84ca80340fdefee6\n\nwhich version checked in?\n```\n\n原来分支 `master` 指向的是一个提交 ID(最新提交)。\n\n这样的分支实现是多么的巧妙啊:既然可以从任何提交开始建立一条历史跟踪链,那么用一个文件指向这个链条的最新提交,那么这个文件就可以用于追踪整个提交历史了。\n\n这个文件就是 `.git/refs/heads/master` 文件。\n\n![got-git-reading-notes-solo-git-repos-detail](https://user-images.githubusercontent.com/9289792/80202610-78ca7680-8658-11ea-9b85-651555b8f005.png)\n\n目录 `.git/refs` 是保存引用的命名空间,其中 `.git/refs/heads` 目录下的引用又称为分支。对于分支既可以使用正规的长格式的表示法,如 `refs/heads/master`,也可以去掉前面的两级目录用 `master` 来表示。Git 有一个底层命令 `git rev-parse` 可以用于显示引用对应的提交 ID。\n\n### 问题:SHA1 哈希值到底是什么,如何生成的?\n\n哈希(hash)是一种数据摘要算法(或称散列算法),是信息安全领域当中重要的理论基石。该算法将任意长度的输入经过散列运算转换为固定长度的输出。固定长度的输出可以称为对应的输入的数字摘要或哈希值。\n\n```bash\necho -n Git |sha1sum\n5819778898df55e3a762f0c5728b457970d72cae -\n```\n\n提交的 SHA1 哈希值生成方法:\n\n```bash\ngit cat-file commit HEAD | wc -c\n234\n\n# 在提交信息的前面加上内容 `commit 234<null>`(`<null>`为空字符),然后执行 SHA1 哈希算法。\n( printf \"commit 234\\000\"; git cat-file commit HEAD ) | sha1sum\ne695606fc5e31b2ff9038a48a3d363f4c21a3d86 -\n\n# 上面命令得到的哈希值和用 `git rev-parse` 看到的是一样的。\ngit rev-parse HEAD\ne695606fc5e31b2ff9038a48a3d363f4c21a3d86\n```\n\n文件内容的 SHA1 哈希值生成方法:\n\n```bash\n# 文件总共包含 25 字节的内容。\ngit cat-file blob HEAD:welcome.txt | wc -c\n25\n\n# 在文件内容的前面加上blob 25<null>的内容,然后执行SHA1哈希算法。\n( printf \"blob 25\\000\"; git cat-file blob HEAD:welcome.txt ) | sha1sum\nfd3c069c1de4f4bc9b15940f490aeb48852f3c42 -\n\n# 上面命令得到的哈希值和用git rev-parse看到的是一样的。\ngit rev-parse HEAD:welcome.txt\nfd3c069c1de4f4bc9b15940f490aeb48852f3c42\n```\n\n树的 SHA1 哈希值的形成方法:\n\n```bash\n# HEAD对应的树的内容共包含39个字节。\ngit cat-file tree HEAD^{tree} | wc -c\n39\n\n# 在树的内容的前面加上tree 39<null>的内容,然后执行SHA1哈希算法。\n( printf \"tree 39\\000\"; git cat-file tree HEAD^{tree} ) | sha1sum\nf58da9a820e3fd9d84ab2ca2f1b467ac265038f9 -\n\n# 上面命令得到的哈希值和用git rev-parse看到的是一样的。\ngit rev-parse HEAD^{tree}\nf58da9a820e3fd9d84ab2ca2f1b467ac265038f9\n```\n\n### 问题:为什么不用顺序的数字来表示提交?\n\n集中式版本控制系统因为只有一个集中式的版本库,可以很容易的实现依次递增的全局唯一的提交号。Git 作为分布式版本控制系统,开发可以是非线性的。这就要求提交的编号不能仅仅是本地局部有效,而是要“全球唯一”。\n\n采用部分的 SHA1 哈希值。不必写全 40 位的哈希值,只采用开头的部分,不和现有其他的冲突即可。\n\n使用 `master` 代表分支 `master` 中最新的提交,使用全称 `refs/heads/master` 亦可。\n\n使用 `HEAD` 代表版本库中最近的一次提交。\n\n符号 `^` 可以用于指代父提交。例如:\n\n- `HEAD^` 代表版本库中上一次提交,即最近一次提交的父提交。\n- `HEAD^^` 则代表 `HEAD^` 的父提交。\n\n对于一个提交有多个父提交,可以在符号 `^` 后面用数字表示是第几个父提交。例如:\n\n- `a573106^2` 含义是提交 `a573106` 的多个父提交中的第二个父提交。\n- `HEAD^1` 相当于 `HEAD^` 含义是 HEAD 多个父提交中的第一个。\n- `HEAD^^2` 含义是 `HEAD^`(HEAD 父提交)的多个父提交中的第二个。\n\n符号 `~<n>` 也可以用于指代祖先提交。效果等同:\n\n```bash\na573106~5\na573106^^^^^\n```\n\n提交所对应的树对象:`a573106^{tree}`\n\n某一此提交对应的文件对象:`a573106:path/to/file`\n\n暂存区中的文件对象:`:path/to/file`\n\n```bash\ngit rev-parse HEAD\ngit cat-file -p e695\ngit cat-file -p e695^\ngit rev-parse e695^{tree}\n```\n\n## References\n\n- [2. Git 独奏 — GotGit](http://www.worldhello.net/gotgit/02-git-solo/index.html)\n","tags":["git"]},{"title":"【Git 权威指南】读书笔记 - 初识 Git","url":"/2017/07/12/got-git-reading-notes-meet-git/","content":"\nGit 是一款分布式版本控制系统,有别于 CVS 和 SVN 等集中式版本控制系统,Git 可以让研发团队更加高效地协同工作、提高生产率。使用 Git,开发人员的工作不会因为频繁地遭遇提交冲突而中断,管理人员也无须为数据的备份而担心。经过 Linux 这样庞大的项目的考验之后,Git 被证明可以胜任任何规模的团队,即便这个团队的成员分布于世界各地。\n\nGit 是开源社区奉献给每一个人的宝贝,用好它可以实现个人的知识积累、保护好自己的数据,而且还能与他人分享自己的成果。\n\n## 版本控制的前世和今生\n\n即便是在 CVS 出现之前的“史前时代”,也已经有了非常好用的源码比较和打补丁的工具:`diff` 和 `patch`,他们今天生命力依然顽强。\n\n<!-- more -->\n\n对这 `hello` `world` 两个文件执行 diff 命令,查看两个文件的差异。如下所示:\n\n```bash\ndiff -u hello world | less -N\n```\n\n上面执行 `diff` 命令的 `-u` 参数很重要,使得差异输出中带有上下文。管道后面带有 `-N` 参数的 `less` 命令(按字母 `q` 退出)会在输出的每一行前面添加行号,便于对输出结果进行说明。\n\n命令 `patch` 相当于 `diff` 的反向操作\n\n分布式版本控制系统最大的反传统之处在于,可以不需要集中式的版本库,每个人都工作在通过克隆操作建立的本地版本库中,也就是说每个人都拥有一个完整的版本库。分布式版本控制系统的几乎所有操作包括查看提交日志、提交、创建里程碑和分支、合并分支、回退等都直接在本地完成而不需要网络连接。每个人都是本地版本库的主人,不再有谁能提交谁不能提交的限制,加之多样的协同工作模型(版本库间推送、拉回,及补丁文件传送等)让开源项目的参与度有爆发式增长。\n\n## 爱上 Git 的理由\n\n- 每日的工作备份。鸡蛋不全放在一个篮子里。\n- 异地协同工作。通过一个远程版本库,同步数据。\n- 现场版本控制。在部署的现场,进行源代码的修改,能够将修改结果甚至修改过程一并带走,并能够将修改结果合并至项目对应的代码库中。\n- 避免引入辅助目录。只在工作区的顶级目录下创建名为 `.git` 的目录(版本库目录),如果认为唯一的一个 `.git` 目录也过于碍眼,可以将其放到工作区之外的任意目录。一旦这么做了,你在执行 Git 命令时,要通过命令行 `--git-dir` 或环境变量 `GIT_DIR` 为工作区指定版本库目录,甚至还要指定工作区目录。\n- 重写提交说明。这个命令如果不带 `-m` 参数,会进入提交说明编辑界面。\n\n```bash\ngit commit --amend\n```\n\n- 想吃后悔药。假如提交的数据中不小心包含了一个不应该检入的虚拟机文件——大约有 1 个 GB。\n\n```bash\ngit rm --cached winxp.img\ngit commit --amend\n```\n\n- 更好用的提交列表。正确的版本控制系统的使用方法是:一次提交只干一件事。而不要在下班时才想起来要提交,那样的话版本控制系统就被降格为文件备份系统了。\n- 更好的差异比较。`git diff`\n- 工作进度保存。\n\n```bash\ngit stash\ngit checkout <new_branch>\n# do something\ngit checkout <orignal_branch>\ngit stash pop\n```\n\n- 代理 SVN 提交实现移动式办公。\n- 无处不在的分页器。`-p`\n- 快。Git 作为分布式版本控制系统几乎所有的操作都在本地进行。\n\n## 安装 Git\n\n```bash\nsudo aptitude install git\nsudo aptitude install git-doc git-svn git-email gitk\n```\n\n软件包 git-svn、git-email、gitk 本来也是 Git 软件包的一部分,但是因为有着不一样的软件包依赖(如更多 perl 模组,tk 等),所以单独作为软件包发布。\n\n软件包 git-doc 则包含了 Git 的 HTML 格式文档,可以选择安装。如果安装了 Git 的 HTML 格式的文档,则可以通过执行 `git help -w <sub-command>` 命令,自动用 Web 浏览器打开相关子命令 `<sub-command>` 的 HTML 帮助。\n\n## 中文支持\n\n```bash\ngit config --global core.quotepath false\n```\n\n## References\n\n- [1. 初识 Git — GotGit](http://www.worldhello.net/gotgit/01-meet-git/index.html)\n","tags":["git"]},{"title":"Linux | Mac 安装 Node.js 与常见问题","url":"/2017/07/06/install-node-js-in-ubuntu-and-faq/","content":"\n## Node.js 安装\n\n推荐使用 nvm 安装管理 node.js\n\n> the nvm method is definitely much more flexible.\n\n[creationix/nvm: Node Version Manager - Simple bash script to manage multiple active node.js versions](https://github.com/creationix/nvm#installation)\n\nTo install or update nvm.\n\n```bash\ncurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash\n```\n\n<!-- more -->\n\n```bash\n# Uses automatic LTS (long-term support) alias `lts/*`, if available.\nnvm ls-remote --lts\n\n# 安装最新 lts\nnvm install --lts\n# v10 lts\nnvm install --lts=Dubnium\n\n# 安装指定版本\nnvm install v6.11.0\n\n# Usually, nvm will switch to use the most recently installed version\nnvm use v6.11.0\n\n# You can see the version currently being used\nnode -v\n\n# 其他命令可以查看帮助\nnvm help\n```\n\n## nrm 源管理\n\nnrm can help you easy and fast switch between different npm registries.\n\n```bash\nnpm install -g nrm\n\nnrm ls\nnrm use taobao\n```\n\n## node-sass sh: node: command not found\n\n> [config unsafe-perm | npmjs](https://docs.npmjs.com/misc/config#unsafe-perm)\n\n解决办法:\n\n```bash\nnpm install --unsafe-perm node-sass\n```\n\n`npm` 出于安全考虑不支持以 `root` 用户运行,即使你用 `root` 用户身份运行了,npm 会自动转成一个叫 `nobody` 的用户来运行,而这个用户几乎没有任何权限。这样的话如果你脚本里有一些需要权限的操作,比如写文件(尤其是写 /root/.node-gyp),就会报错了。\n\n为了避免这种情况,要么按照 npm 的规矩来,专门建一个用于运行 npm 的高权限用户。要么加 `--unsafe-perm` 参数,这样就不会切换到 `nobody` 上,运行时是哪个用户就是哪个用户,即使是 `root`。\n\n再或尝试:\n\n```bash\nnpm rebuild node-sass\n```\n\n## version \"N/A -> N/A\" is not yet installed\n\n出现了这个报错提示:\n\n```bash\nN/A: version \"N/A -> N/A\" is not yet installed.\n\nYou need to run \"nvm install N/A\" to install it before using it\n```\n\nList installed versions\n\n```bash\nnvm ls\n-> v6.11.0\n v8.2.1\ndefault -> 4 (-> N/A)\nnode -> stable (-> v8.2.1) (default)\nstable -> 8.2 (-> v8.2.1) (default)\niojs -> N/A (default)\nlts/* -> lts/boron (-> N/A)\nlts/argon -> v4.8.4 (-> N/A)\nlts/boron -> v6.11.1 (-> N/A)\n```\n\nTry nvm install in that directory to ensure it's installed.\n\n```bash\nnvm install --lts\n\nnvm install iojs\n\nnvm alias default v6.11.1\n```\n\n## References\n\n- [How To Install Node.js on Ubuntu 16.04 | DigitalOcean](https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-16-04)\n- [nvm node is recognised by npm install script. | GitHub](https://github.com/sass/node-sass/issues/2470)\n- [npm 的 --unsafe-perm 参数是有何作用呢?| segmentfault](https://segmentfault.com/q/1010000019365121)\n\n-- EOF --\n","tags":["node-js"]},{"title":"NGINX 启用 HTTP/2","url":"/2017/06/06/nginx-enable-http2/","content":"\n2015 年 5 月 14 日 HTTP/2 协议正式版的发布,越来越多的网站开始部署 HTTP/2 了。\n\nHTTP/2 协议是从 SPDY 演变而来,SPDY 已经完成了使命并很快就会退出历史舞台(例如 Chrome 在 2016 年初结束对 SPDY 的支持;Nginx 在 15 年年底正式支持 HTTP/2 后,也不再支持 SPDY)。\n\n[HTTP/2: the Future of the Internet | Akamai](https://http2.akamai.com/demo) 提供了 HTTP/1 和 HTTP/2 的加载速度对比。\n\n## HTTP/2 中的特性\n\n- 多路复用:通过多个请求 stream 共享一个 TCP 连接的方式,解决了 HTTP1.x holb (head of line blocking) 的问题,降低了延迟同时提高了带宽的利用率。\n- 压缩头部:HTTP/2 规定了在客户端和服务器端会使用并且维护“首部表”,来跟踪和存储之前发送的键值对,对于相同的头部,不必再通过请求发送,只需发送一次。\n- 二进制分帧:在应用层与传输层之间增加一个二进制分帧层,以此达到:在不改动 HTTP 的语义,HTTP 方法、状态码、URI 及首部字段的情况下,突破 HTTP1.1 的性能限制,改进传输性能,实现低延迟和高吞吐量。\n\n以下配置是在 Ubuntu 14.04 LTS 下。Ubuntu 14.04 LTS 中 Nginx、OpenSSL 的默认版本都是比较低的所以需要升级。\n\n## install OpenSSL\n\n```bash\nsudo wget openssl.org/source/openssl-1.0.2l.tar.gz\nsudo tar -xvzfopenssl-1.0.2l.tar.gz\ncd openssl-1.0.2l\nsudo ./config --prefix=/usr/\nsudo make depend\nsudo make install\nopenssl version\n```\n\n## install Nginx\n\n### apt-get\n\n```bash\n# 添加源\nsudo vim /etc/apt/sources.list.d/nginx.list\n```\n\nadd:\n\n```txt\ndeb http://nginx.org/packages/ubuntu/ trusty nginx\ndeb-src http://nginx.org/packages/ubuntu/ trusty nginx\n```\n\n```bash\n# 添加签名\nwget -q \"http://nginx.org/packages/keys/nginx_signing.key\" -O-| sudo apt-key add -\nsudo apt-get update\n```\n\n这样可以安装上比较新的 Nginx 版本应该就够用了。\n\n### make\n\n因为我使用了 `ngx_pagespeed` 模块,所以我采用的是源码编译安装的方式\n\n[nginx: download](http://nginx.org/en/download.html) 下载源码,编译 [Module ngx_http_v2_module](http://nginx.org/en/docs/http/ngx_http_v2_module.html)\n\n```bash\n# 需要添加 http_v2_module 和 --with-openssl\nsudo ./configure --with-http_v2_module --with-openssl=../openssl-1.0.2l\n```\n\n这里只写了 HTTP/2 涉及的模块,其他参数按需添加\n\n```bash\nsudo make\nsudo make install\n```\n\n## Nginx configuration\n\n```conf\nserver {\n listen 443 default_server ssl http2;\n listen [::]:443 default_server ssl http2;\n ...\n}\n```\n\n## test\n\n访问你的网站,在 Chrome Network 中勾选 `Protocol`,可以看到 `h2`\n\n## other\n\n根据 [『 Nginx 启用 HTTP/2』 有槽必吐 - 不吐槽,毋宁死](https://tsukkomi.org/post/enable-http-2-on-nginx) 的经验,在 Ubuntu 16.04 LTS 下只要配置 Nginx server 块就可以了。\n\nChrome 插件 [HTTP/2 and SPDY indicator](https://chrome.google.com/webstore/detail/http2-and-spdy-indicator/mpbpobfflnpcgagjijhmgnchggcjblin?hl=en-US) 如果网站是 HTTP/2 就会显示蓝色,如果是 SPDY(HTTP/2 的前身)就会显示绿色,如果没有则显示灰色。\n\n## References\n\n- [HTTP/2.0 相比 1.0 有哪些重大改进? - 知乎](https://www.zhihu.com/question/34074946)\n- [『 Nginx 启用 HTTP/2』 有槽必吐 - 不吐槽,毋宁死](https://tsukkomi.org/post/enable-http-2-on-nginx)\n- [http2 讲解 · GitBook](https://www.gitbook.com/book/ye11ow/http2-explained/details)\n\n-- EOF --\n","tags":["nginx"]},{"title":"PHP 中获取 Nginx 使用反向代理或 CDN 后的客户端真实 IP","url":"/2017/06/02/php-get-real-ip-after-nginx-using-reverse-proxy-or-cdn/","content":"\n获取 Nginx 反向代理后的客户端 IP,基本是按一定顺序检测以下参数中的信息:\n\n- HTTP_CLIENT_IP\n- HTTP_X_REAL_FORWARDED_FOR\n- HTTP_X_FORWARDED_FOR\n- REMOTE_ADDR\n\n## 在未使用 CDN 和反向代理情况下\n\n当业务服务器直接暴露在公网上,并且未使用 CDN 和反向代理服务器时,可以直接使用 `remote_addr`:\n\n```php\n$_SERVER['REMOTE_ADDR']\n```\n\n这时候 `HTTP_X_FORWARDED_FOR` 和 `HTTP_X_REAL_IP` 都是可以被伪造的,但 `REMOTE_ADDR` 是客户端和服务器的握手 IP,即 client 的出口 IP,伪造不了。\n\n<!-- more -->\n\n## 在使用 CDN 和反向代理情况下\n\n### 铁律\n\n当多层代理或使用 CDN 时,如果代理服务器不把用户的真实 IP 传递下去,那么业务服务器将永远不可能获取到用户的真实 IP。\n\n如果 WEB 服务器上层也是使用 Nginx 做代理或负载均衡,则需要在代理层的 Nginx 配置中明确 XFF 参数,累加传递上一个请求方的 IP 到 header 请求中。以下是代理层的 Nginx 配置参数。\n\n```\nproxy_set_header X-Real-IP $remote_addr;\nproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\nproxy_set_header Host $http_host;\nproxy_set_header X-NginX-Proxy true;\n```\n\n### 只有一层代理的情况\n\n我们按上面的配置发起一个伪造请求,10.100.11.25 是我电脑的 IP,链路为:\n\n```\n10.100.11.25(client)->10.200.21.33(Proxy)->10.200.21.32(Web Server)\n```\n\ncurl 请求:\n\n```bash\ncurl http://10.200.21.33:88/test.php -H 'X-Forwarded-For: unkonw, <8.8.8.8> 1.1.1.1' -H 'X-Real-IP: 2.2.2.2'\n```\n\n结果为:\n\n```bash\n[HTTP_X_FORWARDED_FOR] => unkonw, <8.8.8.8> 1.1.1.1, 10.100.11.25\n[REMOTE_ADDR] => 10.200.21.33\n[HTTP_X_REAL_IP] => 10.100.11.25\n```\n\n我们可以看到,XFF 被附加上了我的 IP,但前面的一系列伪造内容,可以轻易骗过很多规则,而 `HTTP_X_REAL_IP` 则传递了我电脑的 IP。因为在上面的配置中,`X-Real-IP` 已经被设置为握手 IP。\n\n但多层代理之后,以上面的规则,显然 `HTTP_X_REAL_IP` 也不会是真实的用户 IP 了。而 `HTTP_X_FORWARDED_FOR` 则在原有信息(我们伪造的信息)之后附上了握手 IP 一起传递过来了。\n\n### 两层或更多代理的情况\n\n我们这里只测试两层,实际链路为:\n\n```\n10.100.11.25(client)->10.200.21.34(Proxy)->10.200.21.33(Proxy)->10.200.21.32(Web Server)\n```\n\ncurl 请求:\n\n```bash\ncurl http://10.200.21.34:88/test.php -H 'X-Forwarded-For: unkonw, <8.8.8.8> 1.1.1.1' -H 'X-Real-IP: 2.2.2.2'\n```\n\n两层代理的情况下结果为:\n\n```\n[HTTP_X_FORWARDED_FOR] => unkonw, <8.8.8.8> 1.1.1.1, 10.100.11.25, 10.200.21.34\n[REMOTE_ADDR] => 10.200.21.33\n[HTTP_X_REAL_IP] => 10.200.21.34\n```\n\n根据上面的情况,怎么挑出真正的用户 IP 呢?设想三种方案:\n\n1、第一层代理将用户的真实 IP 放在 `X-Real-IP` 中传递下去,后面的每一层都使用 `X-Real-IP` 继续往下传递。配置为:\n\n```\nproxy_set_header X-Real-IP $remote_addr; # 针对首层代理,拿到真实IP\nproxy_set_header X-Real-IP $http_x_real_ip; # 针对非首层代理,一直传下去\n```\n\n2、从首层开始,将用户的真实 IP 放在 X-Forwarded-For 中,而不是累加各层服务器的 IP,但这样也不够合理,因为丢掉了整个链路信息。配置为:\n\n```\nproxy_set_header X-Forwarded-For $remote_addr; # 针对首层代理\nproxy_set_header X-Forwarded-For $http_x_forwarded_for; # 针对非首层代理\n```\n\n3、从 `X-Forwarded-For` 中获取的用户真实 IP,排除掉所有代理 IP,取最后一个符合 IP 规则的,注意不是第一个,因为第一个可能是被伪造的(除非首层代理使用了握手会话 IP 做为值向下传递)。\n\n一般 CDN 都会将用户的真实 IP 在 XFF 中传递下去。我们可以做几个简单的测试就能知道我们该怎么做。\n\n注意:Nginx 配置的这两个变量:\n\n- `$proxy_add_x_forwarded_for` 会累加代理层的 IP 向后传递\n- `$http_x_forwarded_for` 仅仅是上层传过来的值\n\n## Nginx realip 模块获取真实 IP\n\n秉承一个原则:_能通过配置让事情变的更简单和通用的事儿,就不要用程序去解决。即环境对程序透明。_ 这当然少不了系统运维人员的辛苦。\n\n如果能在配置中理清,就不必用复杂的程序去解决,因为 Server 上可能有各种应用都要来获取用户 IP,如果规则不统一,结果会不一致。\n\n程序不知道链路到底经过了几层才转到 WEB Server 上,所以让程序去做兼容并不是个好主意。索性就让程序把所有的代理都当成透明的好了。\n\n上面介绍的三种方法中,如果不能保证前面的代理层使用我们指定的规则,这时候怎么办呢?只能使用第三种方法。然后我们将各层代理的 IP 排除在外,就取到了真实的用户 IP。这个可以使用 Nginx 的一个模块儿 [Module ngx_http_realip_module](http://nginx.org/en/docs/http/ngx_http_realip_module.html) 来实现。\n\n原理是从 XFF 中抛弃指定的代理层 IP,那么最后一个符合规则的就是用户 IP。也可以配合第一起方法一起使用。但无论如何,首层代理的规则最重要,直接影响后面的代理层和 WEB Server 的接收结果。\n\n然后在 Nginx 配置中增加以下配置(可以在 http、server 或 location 段中增加):\n\n```\n# set user real ip to remote addr\nset_real_ip_from 10.200.21.0/24;\nset_real_ip_from 10.100.23.0/24;\nreal_ip_header X-Forwarded-For;\nreal_ip_recursive on;\n```\n\n`set_real_ip_from` 后面是可信 IP 规则,可以有多条。如果启用 CDN,知道 CDN 的溯源 IP,也要加进来,除排掉可信的,就是用户的真实 IP,会写入 `remote_addr` 这个变量中。\n\n在 PHP 中可以使用 `$_SERVER['REMOTE_ADDR']` 来获取。而 WEB Server 不使用任何反向代理时,也是取这个值,这就达到了我们之前所说的原则。\n\n`real_ip_recursive` 是递归的去除所配置中的可信 IP。如果只有一层代理,也可以不写这个参数。\n\n## ThinkPHP 中的获取 IP 方法\n\nThinkPHP 的 function 中提供了一个工具方法,在对获取 IP 地址不严格的情况下,可以启用高级模式\n\n```php\n/**\n * 获取客户端IP地址\n * @param integer $type 返回类型 0 返回IP地址 1 返回IPV4地址数字\n * @param boolean $adv 是否进行高级模式获取(有可能被伪装)\n * @return mixed\n */\nfunction get_client_ip($type = 0, $adv = false) {\n $type = $type ? 1 : 0;\n static $ip = NULL;\n if ($ip !== NULL) return $ip[$type];\n if($adv){\n if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {\n $arr = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);\n $pos = array_search('unknown',$arr);\n if(false !== $pos) unset($arr[$pos]);\n $ip = trim($arr[0]);\n }elseif (isset($_SERVER['HTTP_CLIENT_IP'])) {\n $ip = $_SERVER['HTTP_CLIENT_IP'];\n }elseif (isset($_SERVER['REMOTE_ADDR'])) {\n $ip = $_SERVER['REMOTE_ADDR'];\n }\n }elseif (isset($_SERVER['REMOTE_ADDR'])) {\n $ip = $_SERVER['REMOTE_ADDR'];\n }\n // IP地址合法验证\n $long = sprintf(\"%u\",ip2long($ip));\n $ip = $long ? array($ip, $long) : array('0.0.0.0', 0);\n return $ip[$type];\n}\n```\n\n## Nginx LOG 记录真实 IP\n\n```\nlog_format porxy '$http_x_forwarded_for - $remote_user [$time_local] '\n ' \"$request\" $status $body_bytes_sent '\n ' \"$http_referer\" \"$http_user_agent\" ';\n\naccess_log /usr/local/nginx/logs/access.log porxy;\n```\n\n文章称 `nginx reload` 配置并不生效,需要 `restart`。\n\n## References\n\n- [使用 PHP 获取客户端真实 IP 地址?——不可能! - 也就这样](http://blog.zhengshuiguang.com/php/php-ip.html)\n- [NGINX 多层转发或使用 CDN 之后如何获取用户真实 IP | Snow Blog](http://www.wkii.org/nginx-cdn-get-user-real-ip.html)\n- [Nginx 日志配置详情解析](https://juejin.im/post/59f94f626fb9a045023af34c)\n\n-- EOF --\n","tags":["php","nginx"]},{"title":"PHP 中 this self parent 用法","url":"/2017/05/25/how-to-use-this-self-parent-in-php/","content":"\n<!-- more -->\n\n- `self::` 调用本类属性、方法;可以抑制方法多态性。\n- `parent::` 调用父类属性、方法。\n- `static::` 调用静态属性、方法;可以体现多态性。\n- `$this->` 调用本实例的属性、方法;`$this::` 可以调用静态属性、方法;但是无法在静态方法里使用;可以体现多态性。\n- `->` object-operator, you always know you're dealing with an instance.\n- `::` scope-resolution-operator, you need more information about the context.\n\n```php\n<?php\nclass A\n{\n public static function newStaticClass()\n {\n return new static();\n }\n\n public static function newSelfClass()\n {\n return new self();\n }\n\n public function newThisClass()\n {\n return new $this();\n }\n}\n\nclass B extends A\n{\n public function newParentClass()\n {\n return new parent();\n }\n}\n\nclass C extends B\n{\n public static function newSelfClass()\n {\n return new self();\n }\n}\n\n$c = new C();\n\nvar_dump($c::newStaticClass()); // C and is same C::newStaticClass()\nvar_dump($c::newSelfClass()); // C because self now points to \"C\" class\nvar_dump($c->newThisClass()); // C\nvar_dump($c->newParentClass()); // A because parent was defined *way back* in class \"B\"\n```\n\n## References\n\n- [php - When to use self over \\$this? - Stack Overflow](https://stackoverflow.com/questions/151969/when-to-use-self-over-this)\n","tags":["php"]},{"title":"【摔跤吧,爸爸】随笔","url":"/2017/05/14/i-am-proud-of-you/","content":"\n周末看了《摔跤吧,爸爸》,也是第一次独自电影院看电影,试写一篇影评纪念下。\n\n注意:**严重剧透预警。**\n\n主角爸爸是印度全国摔跤冠军,一心想着为国家赢取一枚金牌。可自己没能实现梦想,把梦想转移给了自己还未出世的孩子。可事与愿违,想要男孩的主角爸爸的前三个孩子都是女孩,第四个还是女孩。主角爸爸近乎要放弃为国争取金牌的梦想时,却意外看的了大女儿、二女儿身上的摔跤天赋。\n\n<!-- more -->\n\n主角妈妈在得知主角爸爸准备将女儿们训练为拳击手时,道出了 “你不能将你的梦想施加在女儿们身上”,主角爸爸沉思片刻 “给我一年时间,其间你不要插手,如果没成功,我将有永远放弃我的梦想”。在主角爸爸说出这句话之前,我是反对父母将自己的梦想让孩子去实现的,也更没有去争取过孩子们的想法,现实中这样做的父母我是常为 Loser. 但是主角爸爸的回答让我看到的不是一个:被自己个人梦想冲昏头,用孩子的全部去成就自己的父亲。主角爸爸是讲道理的,一年后如果失败也会甘愿放弃。坚持与固执的区别也许就在此。\n\n主角爸爸变身为女儿们的魔鬼摔跤教练,每天除了上学就是训练,跑步时衣服不合适换男生衣服,找侄子当陪练,没有力量加餐牛奶、鸡肉,长头发难清理剪成了寸头。受苦中的女儿们找妈妈求情,主角妈妈遵守承诺,不干预主角爸爸的训练。这里真的要给主角妈妈点赞,后面女儿们的成功,女儿们、主角爸爸的努力在明面放着,而主角妈妈的守诺不干预,对主角爸爸的信赖,是同样的伟大。不然那柔弱的耳边风不知乱了多少坚定的意志。\n\n训练太辛苦的女儿们采用消极怠工对抗主角爸爸。破坏闹钟、破坏场地、假摔示弱。而旷工一天参加好友的婚礼现场被主角爸爸收拾。在与好友诉苦时,才发现了:对比其他女孩一望到底的家庭妇女人生,主角爸爸的狠心是那样让人羡慕。\n\n观念改变的女儿们开始主动训练。大女儿首次参赛就初露锋芒,差一点将男孩对手打败。逐渐成长的大女儿愈战愈勇的直到赢得全国冠军。全国冠军都会进入国家体育学校学习,大女儿也要离开主角爸爸接受新教练的训练,备战世界大赛。从小镇走出来的大女儿,没了主角爸爸的严格管束,开始着迷于这外面多彩的世界。吃油炸食品、留起头发、逛街、电影院。假期回家的大女儿用自己新学的技巧击败了主角爸爸,更是产生自我膨胀,对主角爸爸传统技术技巧的不屑与不信任。\n\n国际大赛上大女儿频频失利,无能教练将此归为命运。与此同时坚信主角爸爸训练方式的二儿女也获得全国冠军,进入国家队。在二儿女和主角妈妈的劝解下,一通电话化解了大女儿和主角爸爸的隔阂。主角爸爸亲自来到体育学校,为大女儿备战下次的国际大赛。\n\n主角爸爸在仔细研究大女儿对手后,制定了针对性的战术帮助大女儿杀入决赛。无能教练为了不让主角爸爸抢了自己的功劳,在决赛开始前将主角爸爸骗入小黑屋,无法让主角爸爸指导大女儿比赛。决赛第二局大女儿没能把握赛点,让对手追平。主角爸爸继续被困,只能默默祈祷。决赛最后一局,大女儿大比分落后,最后 10 秒大女儿脑海闪现着主角爸爸的叮咛,一击 5 分绝杀翻盘,赢得世界冠军。冠军的国歌声奏起,小黑屋中的主角爸爸也被路人解救,冲入赛场的主角爸爸与大女儿四目相对,此时这已经不是大女儿或主角爸爸哪一个人的冠军,而是所有正在与命运抗战者的胜利。\n\n主角爸爸:“你是我的骄傲”\n\n---\n\n电影的专业手法和技术自己不懂,但是自己有个感觉:影片后期有大量的摔跤比赛,时不时有种看奥运赛的感觉,但是自己并没有感到乏味。电影故事情节很寻常,但是娓娓道来,很多细节小事让节奏不拖沓。故事人物有主角爸爸的理性和坚持、主角妈妈的守诺不干预、女儿们自我的成长、侄子的酱油加醋、无能教练损人为己\n\n电影给我也留下了几个现实问题:\n\n- 父母的认识经验一定比子女高的多,主角爸爸在看到女儿们的摔跤天赋时,狠心训练与女儿本身的意愿如何选择?\n- 父母逐渐变老,他们真的跟不上我们了吗?\n- 如何辨别无能教练的瞎指挥?\n\n-- EOF --\n","tags":["reading-notes"]},{"title":"区分 NGINX 中 fastcgi_params fastcgi fastcgi-php","url":"/2017/04/22/nginx-fastcgi-params-fastcgi-fastcgi-php/","content":"\nNGNIX 有两份 fastcgi 配置文件,分别是 `fastcgi_params` 和 `fastcgi.conf`,其区别只有一点点。到目前为止,由于 package managers,他们仍然引起新用户的混淆。\n\n在自己系统中还有份 `snippets/fastcgi-php.conf`,这个又是啥?\n\n## fastcgi_params vs fastcgi.conf\n\n它们都是用于配置 NGINX 与 FastCGI 应用程序通信的参数文件。\n\n- fastcgi_params: 包含了FastCGI应用程序所需的最基本参数,如SCRIPT_FILENAME、QUERY_STRING等。这些参数通常不需要修改。\n- fastcgi.conf: 包含了更高级的FastCGI参数,可以用于优化FastCGI应用程序的性能,如设置连接超时时间、缓冲区大小等。\n\n`fastcgi.conf` 比 `fastcgi_params` 多了一行 `SCRIPT_FILENAME` 的定义\n\n```conf\nfastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n```\n\n注意:`$document_root` 和 `$fastcgi_script_name` 之间没有 `/`。\n\n原本 NGNIX 只有 `fastcgi_params`,后来发现很多人在定义 `SCRIPT_FILENAME` 时使用了硬编码的方式,于是为了规范用法便引入了 `fastcgi.conf`。\n\n不过这样的话就产生一个疑问:为什么一定要引入一个新的配置文件,而不是修改旧的配置文件?\n\n这是因为`fastcgi_param` 指令是数组型的,和普通指令相同的是:内层替换外层;和普通指令不同的是:当在同级多次使用的时候,是新增而不是替换。\n\n换句话说,如果在同级定义两次 `SCRIPT_FILENAME`,那么它们都会被发送到后端,这可能会导致一些潜在的问题,为了避免此类情况,便引入了一个新的配置文件。\n\n```conf\nserver {\n listen 80;\n server_name foo.com;\n\n root /path;\n index index.html index.htm index.php;\n\n location / {\n try_files $uri $uri/ /index.php$is_args$args;\n }\n\n location ~ \\.php$ {\n try_files $uri =404;\n\n include fastcgi.conf;\n fastcgi_pass 127.0.0.1:9000;\n }\n}\n```\n\n## fastcgi-php.conf\n\n`fastcgi-php.conf` 是一个 Nginx 配置文件片段,用于配置 Nginx 服务器与 PHP FastCGI 进程之间的通信。它定义了 FastCGI 连接的参数和选项,以及如何处理 PHP 脚本。通常,这个文件是在 Nginx 的主配置文件中包含的,以确保 Nginx 能够正确地将请求发送到 PHP FastCGI 进程。\n\n```conf\n# regex to split $uri to $fastcgi_script_name and $fastcgi_path\nfastcgi_split_path_info ^(.+\\.php)(/.+)$;\n\n# Check that the PHP script exists before passing it\ntry_files $fastcgi_script_name =404;\n\n# Bypass the fact that try_files resets $fastcgi_path_info\n# see: http://trac.nginx.org/nginx/ticket/321\nset $path_info $fastcgi_path_info;\nfastcgi_param PATH_INFO $path_info;\n\nfastcgi_index index.php;\ninclude fastcgi.conf;\n```\n\n从 `fastcgi-php.conf` 的内容可以看出,它帮我们封装了一些公共代码。\n\n```conf\nserver {\n ...\n location ~ \\.php$ {\n include snippets/fastcgi-php.conf;\n fastcgi_pass 127.0.0.1:9000;\n }\n}\n```\n\n## PHP NGINX Unix Sock 切换 TCP/IP\n\n```conf\nsudo vim /etc/php5/fpm/pool.d/www.conf\n```\n\n```conf\n# 取消注释\nlisten.backlog = 65536\n# 查找\nlisten = /var/run/php5-fpm.sock\n# 修改为\nlisten = 127.0.0.1:9000\n```\n\nand then, edit NGINX configuration file\n\n```conf\nfastcgi_pass unix:/var/run/php5-fpm.sock;\n# 修改为\nfastcgi_pass 127.0.0.1:9000;\n```\n\n```conf\nsudo service php5-fpm restart\nsudo service nginx restart\n```\n\n## References\n\n- [如何正确配置 Nginx+PHP | 火丁笔记](https://huoding.com/2013/10/23/290)\n- [fastcgi_params Versus fastcgi.conf - Nginx Config History](http://blog.martinfjordvald.com/2013/04/nginx-config-history-fastcgi_params-versus-fastcgi-conf/)\n- [How To Install Nginx on Ubuntu 16.04 | DigitalOcean](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-16-04)\n- [nginx-build/fastcgi-php.conf at master · EasyEngine/nginx-build](https://github.com/EasyEngine/nginx-build/blob/master/nginx/debian/conf/snippets/fastcgi-php.conf)\n","tags":["php","nginx"]},{"title":"区分 CGI FastCGI PHP-CGI PHP-FPM","url":"/2017/04/21/cgi-fastcgi-phpcgi-php-fpm/","content":"\n更新 2023-03-20 提问 ChatGPT 3.5\n\n- CGI(Common Gateway Interface)是一种通用的 Web 服务器接口标准,它定义了 Web 服务器和应用程序之间进行通信的方式。CGI 程序在每次请求时都会启动一个新的进程,处理完请求后就会结束进程,因此效率较低。\n- FastCGI 是 CGI 的一种改进技术,它使用长连接的方式,使得多次请求可以在同一个进程中处理,从而提高了效率。\n- PHP-CGI 是 PHP 的 CGI 版本,它可以让 PHP 脚本在 Web 服务器上运行,但由于 CGI 的低效率,PHP-CGI 的性能也比较低。\n- PHP-FPM(PHP FastCGI Process Manager)是一个用于管理 PHP FastCGI 进程的工具,它可以提高 PHP 的性能和稳定性。\n\n关联和区别:\n\n- CGI 和 FastCGI 都是 Web 服务器和应用程序之间通信的方式,但 FastCGI 在性能上比 CGI 更好。\n- PHP-CGI 和 PHP-FPM 都是 PHP 在 Web 服务器上运行的方式,但 PHP-FPM 可以有效提高 PHP 的性能和稳定性。\n\n作用举例:\n\n- 如果使用 CGI 或 PHP-CGI 来运行 PHP 脚本,每次请求都需要启动一个新的进程,效率较低。而如果使用 FastCGI 或 PHP-FPM,则可以使用长连接的方式,提高效率。\n- 使用 PHP-FPM 时,可以配置多个进程,通过负载均衡的方式来分担请求,从而提高稳定性。\n\n---\n\n## CGI\n\nCommon Gateway Interface,HTTP 服务器与你的或其它机器上的程序进行“交谈”的一种协议,其程序须运行在网络服务器上。\n\nweb server(比如说 Nginx)只是内容的分发者。\n\n- 如果请求 /index.html,那么 web server 会去文件系统中找到这个文件,发送给浏览器,这里分发的是静态数据。\n- 如果现在请求的是 /index.php,根据配置文件,nginx 知道这个不是静态文件,需要去找 PHP 解析器来处理,那么他会把这个请求简单处理后交给 PHP 解析器。Nginx 会传哪些数据给 PHP 解析器呢?url、查询字符串、POST 数据、HTTP header 等等,CGI 就是规定要传哪些数据、以什么样的格式传递给后方处理这个请求的协议。\n\n## FastCGI\n\nFastCGI 是语言无关的、可伸缩架构的 CGI 开放扩展,其主要行为是将 CGI 解释器进程保持在内存中并因此获得较高的性能。众所周知,CGI 解释器的反复加载是 CGI 性能低下的主要原因(fork-and-execute),如果 CGI 解释器保持在内存中并接受 FastCGI 进程管理器调度,则可以提供良好的性能、伸缩性、Fail-Over 特性等。\n\nFastCGI 的特点是会在一个进程中依次完成多个请求,以达到提高效率的目的,多数 FastCGI 实现都会维护一个进程池。\n\n那么 FastCGI 是怎么做的呢?首先,FastCGI 会先启一个 master,解析配置文件,初始化执行环境,然后再启动多个 worker。当请求过来时,master 会传递给一个 worker,然后立即可以接受下一个请求。这样就避免了重复的劳动,效率自然是高。而且当 worker 不够用时,master 可以根据配置预先启动几个 worker 等着;当然空闲 worker 太多时,也会停掉一些,这样就提高了性能,也节约了资源。这就是 FastCGI 对进程的管理。\n\n## PHP-CGI\n\nPHP-CGI 只是个 CGI 程序,他自己本身只能解析请求,返回结果,不会进程管理。\n\nPHP-CGI 的不足:PHP-CGI 变更 php.ini 配置后需重启 PHP-CGI 才能让新的 php-ini 生效,不可以平滑重启。直接杀死 PHP-CGI 进程,PHP 就不能运行了。(PHP-FPM 和 Spawn-FCGI 就没有这个问题,守护进程会平滑从新生成新的子进程。)\n\n## PHP-FPM\n\nPHP-FPM 是一个 PHP FastCGI 管理器,是只用于 PHP。\n\nPHP-FPM 是 PHP 针对 FastCGI 协议的具体实现,也是 PHP 在多种服务器端应用编程端口(SAPI:cgi、fast-cgi、cli、isapi、apache)里使用最普遍、性能最佳的一款进程管理器。\n\n## 小故事\n\n你(PHP)去和爱斯基摩人(web 服务器,如 Apache、Nginx)谈生意。你说中文(PHP 代码),他说爱斯基摩语(C 代码),互相听不懂,怎么办?那就都把各自说的话转换成英语(FastCGI 协议)吧。\n\n怎么转换呢?你就要使用一个翻译机(PHP-FPM)(当然对方也有一个翻译机,那个是他自带的)\n\n我们这个翻译机是最新型的,老式的那个(PHP-CGI)被淘汰了。\n\n## 让我把话说完\n\nFastCGI 是 Nginx 和 PHP 之间的一个通信接口,该接口实际处理过程通过启动 PHP-FPM 进程来解析 PHP 脚本,即 PHP-FPM 相当于一个动态应用服务器,从而实现 Nginx 动态解析 PHP。\n\n因此,如果 Nginx 服务器需要支持 PHP 解析,需要在 nginx.conf 中增加 PHP 的配置:将 PHP 脚本转发到 FastCGI 进程监听的 IP 地址和端口(php-fpm.conf 中指定)。\n\n同时,PHP 安装的时候,需要开启支持 FastCGI 选项,并且编译安装 PHP-FPM 补丁/扩展,同时,需要启动 PHP-FPM 进程,才可以解析 Nginx 通过 FastCGI 转发过来的 PHP 脚本\n\n## References\n\n- [搞不清 FastCgi 与 PHP-fpm 之间是个什么样的关系](https://segmentfault.com/q/1010000000256516)\n- [什么是 CGI、FastCGI、PHP-CGI、PHP-FPM、Spawn-FCGI?](http://www.mike.org.cn/articles/what-is-cgi-fastcgi-php-fpm-spawn-fcgi/)\n- [nginx、fastCGI、php-fpm 关系梳理 842864681 新浪博客](http://blog.sina.com.cn/s/blog_6df9fbe30102v57y.html)\n- [概念了解:CGI,FastCGI,PHP-CGI 与 PHP-FPM](http://www.nowamagic.net/librarys/veda/detail/1319)\n","tags":["php"]},{"title":"MySQL 5.6 5.7 组内排序的区别","url":"/2017/04/20/mysql-group-by-and-order-by-difference-between-56-57/","content":"\nMySQL 5.7 对比 5.6 有很多的变化。一个常见的需求:按条件分组后,取出每组中某字段最大值的那条记录。其实就是组内排序的问题,我的做法是:子查询先进行倒序排序,外层查询分组。\n\n## 示例\n\n```md\n+----+----+-------+\n| id | no | name |\n+----+----+-------+\n| 5 | 5 | Mike |\n| 4 | 4 | Herry |\n| 3 | 3 | wyett |\n| 2 | 2 | John |\n| 7 | 2 | John |\n| 1 | 1 | Mike |\n| 6 | 1 | John |\n| 8 | 1 | Mike |\n| 9 | 1 | Mike |\n+----+----+-------+\n```\n\n要求:取出每人(按 name),最大 no 的记录。\n\n```sql\nselect * from (\n select id,no,name from testorder order by no desc\n)a group by a.name;\n```\n\n```md\n+----+----+-------+\n| id | no | name |\n+----+----+-------+\n| 4 | 4 | Herry |\n| 2 | 2 | John |\n| 5 | 5 | Mike |\n| 3 | 3 | wyett |\n+----+----+-------+\n```\n\n但是在 5.7 中,首先需要关闭 `ql_mode = ONLY_FULL_GROUP_BY`;相同的 `name` 值,返回则是取了 **最早写入的数据行** ,**忽略了 `order by no desc`,按照数据的逻辑存储顺序来返回**\n\n```md\n+----+----+-------+\n| id | no | name |\n+----+----+-------+\n| 4 | 4 | Herry |\n| 2 | 2 | John |\n| 1 | 1 | Mike |\n| 3 | 3 | wyett |\n+----+----+-------+\n```\n\n等价于\n\n```sql\nselect id,no,name from testorder group by name\n```\n\n<!--more -->\n\nA query such as\n\n```sql\nSELECT field1, field2 FROM ( SELECT field1, field2 FROM table1 ORDER BY field2 ) alias\n```\n\nreturns a result set that is not necessarily ordered by field2. This is not a bug.\nA \"table\" (and subquery in the FROM clause too) is - according to the SQL standard - an unordered set of rows.\nRows in a table (or in a subquery in the FROM clause) do not come in any specific order.\n\n可以总结为:\n\n- 在 FROM 后的 subquery 中的 ORDER BY 会被忽略\n- GROUP BY cloumn 返回的行是无序的\n\n## 解决方案\n\n```sql\nselect a.id,a.no,a.name\n from testorder a inner join (\n select max(no) no,name from testorder group by name\n ) b on a.no = b.no and a.name = b.name\ngroup by name,no\n```\n\n其他方案:\n\n1. 对于不符合 ONLY_FULL_GROUP_BY 限制的字段,添加 unique 索引。\n2. 使用 ANY_VALUE(),让 MySQL 跳过 ONLY_FULL_GROUP_BY 检测。\n\n## 小结\n\n> [MySQL 5.6 Handling of GROUP BY](https://dev.mysql.com/doc/refman/5.6/en/group-by-handling.html)\n\nIn standard SQL, a query that includes a GROUP BY clause cannot refer to nonaggregated columns in the select list that are not named in the GROUP BY clause.\n\n在标准 SQL 中,包含 GROUP BY 子句的查询 **不能引用** select 列表中未在 GROUP BY 子句中命名的列。\n\nMySQL extends the standard SQL use of GROUP BY so that the select list can refer to nonaggregated columns not named in the GROUP BY clause. This means that the preceding query is legal in MySQL.\n\nMySQL 扩展了 GROUP BY 的标准 SQL 使用,以便选择列表可以引用 GROUP BY 子句中未命名的非集合列。这意味着前面的查询在 MySQL 中是合法的。\n\nHowever, this is useful primarily when all values in each nonaggregated column not named in the GROUP BY are the same for each group. The server is free to choose any value from each group, so unless they are the same, the values chosen are indeterminate.\n\n但是,主要是在 GROUP BY 中 **未命名的每个非分组列中的所有值对于每个组是相同的**,这是有用的。服务器可以自由选择每个组中的任何值,因此除非它们相同,所选择的值是 **不确定的**。\n\nFurthermore, the selection of values from each group cannot be influenced by adding an ORDER BY clause. Result set sorting occurs after values have been chosen, and ORDER BY does not affect which values within each group the server chooses.\n\n此外,通过添加 ORDER BY 子句不会影响来自每个组的值的选择。结果集排序发生在选择值后,ORDER BY **不影响** 服务选择的每个组中的哪些值。\n\n> [MySQL 5.7 Handling of GROUP BY](https://dev.mysql.com/doc/refman/5.7/en/group-by-handling.html)\n\nMySQL 5.7.5 and up implements detection of functional dependence. If the ONLY_FULL_GROUP_BY SQL mode is enabled (which it is by default), MySQL rejects queries for which the select list, HAVING condition, or ORDER BY list refer to nonaggregated columns that are neither named in the GROUP BY clause nor are functionally dependent on them. (Before 5.7.5, MySQL does not detect functional dependency and ONLY_FULL_GROUP_BY is not enabled by default.)\n\nMySQL 5.7.5 及以上功能依赖检测功能。如果启用了 ONLY_FULL_GROUP_BY SQL 模式(默认情况下),MySQL 将拒绝对列表,HAVING 条件或 ORDER BY 列表的查询引用在 GROUP BY 子句中既未命名的非集合列,也不在功能上依赖于它们。(5.7.5 之前,MySQL 没有检测到功能依赖关系,默认情况下不启用 ONLY_FULL_GROUP_BY)\n\nYou can achieve the same effect without disabling ONLY_FULL_GROUP_BY by using ANY_VALUE() to refer to the nonaggregated column.\n\n你可以通过使用 `ANY_VALUE()` 使禁用了 ONLY_FULL_GROUP_BY 的 SQL,来实现相同的效果来引用非聚合列。\n\n## 5.6 与 5.7 的区别\n\n5.6 升级到 5.7 版本要注意:\n\n1. sql_mode 默认值的改变。\n2. optimizer_switch 值的改变。\n3. 备库升级影响主备复制。\n\n```sql\nSELECT @@SQL_MODE, @@GLOBAL.SQL_MODE;\n\n-- 5.6\n-- NO_ENGINE_SUBSTITUTION\n\n-- 5.7\n-- ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION\n```\n\n- `ONLY_FULL_GROUP_BY` SELECT 查询的字段必须是 GROUP BY 中出现的或者使用聚合函数的或者是具有唯一索引的。\n- `STRICT_TRANS_TABLES` 在该模式下,如果一个值不能插入到一个事务表中,则中断当前的操作,对非事务表不做任何限制。\n- `NO_ZERO_IN_DATE` 在严格模式,不接受月或日部分为 0 的日期。如果使用 IGNORE 选项,我们为类似的日期插入'0000-00-00'。在非严格模式,可以接受该日期,但会生成警告。\n- `NO_ZERO_DATE` 在严格模式,不要将 '0000-00-00'做为合法日期。你仍然可以用 IGNORE 选项插入零日期。在非严格模式,可以接受该日期,但会生成警告。\n- `ERROR_FOR_DIVISION_BY_ZERO` 在严格模式,在 INSERT 或 UPDATE 过程中,如果被零除(或 MOD(X,0)),则产生错误(否则为警告)。如果未给出该模式,被零除时 MySQL 返回 NULL。如果用到 INSERT IGNORE 或 UPDATE IGNORE 中,MySQL 生成被零除警告,但操作结果为 NULL。\n- `NO_AUTO_CREATE_USER` 防止 GRANT 自动创建新用户,除非还指定了密码。\n- `NO_ENGINE_SUBSTITUTION` 如果需要的存储引擎被禁用或未编译,那么抛出错误。不设置此值时,用默认的存储引擎替代,并抛出一个异常。\n\n### STRICT_TRANS_TABLES 和 STRICT_ALL_TABLES 的区别\n\n唯一的区别是:对于不支持事务的表,若开启 STRICT_TRANS_TABLES,MySQL 会尝试将一个不合法的字段值转换成一个值最近的合法值插入表中;而开启 STRICT_ALL_TABLES 后,则表现为不写入数据,且抛出错误。\n\n因为现在绝大部分用的 InnoDB 引擎,是支持事务的,所以基本不用关心这种区别。\n\n### 严格模式和非严格模式的区别\n\n```sql\nSET sql_mode = 'STRICT_TRANS_TABLES';\n```\n\n严格模式下不能在无符号整数字段插入负值。\n\n- 非严格模式下,会存储为 0。\n- 严格模式下,报错。\n\n严格模式下,无默认值的 NOT NULL 字段在插入数据时必须指定值。\n\n- 非严格模式下,若不插入数据会存储字段类型的默认值。\n- 严格模式下,报错。\n\n严格模式下,插入字符串不能超出定义长度\n\n- 非严格模式下,会成功插入数据,但是内容被截断。\n- 严格模式下,报错。\n\n## References\n\n- [MySQL 组内排序取最大值 | mysqlwyett](http://mysqlwyett.com/blog/2017/01/17/max_values_in_group_by/)\n- [sql - MySQL Group By and Order By; - Stack Overflow](http://stackoverflow.com/questions/1066453/mysql-group-by-and-order-by)\n- [MySQL5.7 中的 sql_mode 默认值 | zhihu](https://zhuanlan.zhihu.com/p/50278304)\n- [MySQL: 严格模式 | letianbiji](https://www.letianbiji.com/mysql/mysql-strict-mode.html)\n\n-- EOF --\n","tags":["mysql"]},{"title":"MySQL 管理用户与访问授权","url":"/2017/04/11/mysql-manage-user-and-grant/","content":"\nMySQL 创建用户、修改密码、删除用户;查看、授予、撤销用户权限;对 MySQL 远程访问的新理解。\n\n```sql\n-- 创建用户 + 授权\nGRANT ALL PRIVILEGES ON _._ TO 'tom'@'%' IDENTIFIED BY 'pwd123' WITH GRANT OPTION;\n-- 查询权限\nSHOW GRANTS FOR 'tom'@'%';\n-- 授权\nGRANT SELECT ON `my_db`.* TO 'tom'@'%';\n-- 撤权\nREVOKE ALL PRIVILEGES ON `my_db`.* FROM 'tom'@'%';\nREVOKE GRANT OPTION ON `my_db`.* from 'tom'@'%';\n\n-- not necessary\nFLUSH PRIVILEGES;\n```\n\n## 管理用户\n\n### 创建用户\n\n```bash\nCREATE USER 'username'@'host' IDENTIFIED BY 'password';\n```\n\n- `username`:创建的用户名\n- `host`:该用户在哪个主机上可以登陆。如果是本地用户可用 `localhost`;如果想让该用户可以从任意远程主机登陆,可以使用通配符 `%`\n- `password`:该用户的登陆密码。密码可以为空,如果为空则该用户可以不需要密码登陆服务器\n\n例子:\n\n```bash\nCREATE USER 'dog'@'localhost' IDENTIFIED BY '123456';\nCREATE USER 'pig'@'192.168.1.%' IDENDIFIED BY '123456';\nCREATE USER 'pig'@'%' IDENTIFIED BY '123456';\nCREATE USER 'pig'@'%' IDENTIFIED BY '';\nCREATE USER 'pig'@'%';\n```\n\n### 修改用户密码\n\n```bash\nSET PASSWORD FOR 'username'@'host' = PASSWORD('newpassword');\n\nSET PASSWORD FOR 'pig'@'%' = PASSWORD(\"123456\");\n```\n\n如果修改当前登陆用户密码:\n\n```bash\nSET PASSWORD = PASSWORD(\"newpassword\");\n```\n\n### 删除用户\n\n```bash\nDROP USER 'username'@'host';\n```\n\n## 访问授权\n\n### 查看用户权限\n\n```bash\nSHOW GRANTS FOR 'pig'@'%';\n```\n\n### 授予用户权限\n\n```bash\nGRANT PRIVILEGES ON databasename.tablename TO 'username'@'host'\n\nGRANT SELECT, INSERT ON test.user TO 'pig'@'%';\nGRANT ALL ON *.* TO 'pig'@'%';\n```\n\n- `privileges`:用户的操作权限,如 `SELECT` `INSERT` `UPDATE` 等,如果要授予所的权限则使用 `ALL`\n- `databasename`:数据库名\n- `tablename`:表名,如果要授予该用户对所有数据库和表的相应操作权限则可用 `*` 表示\n\n**注意:** 用以上命令授权的用户不能给其它用户授权,如果想让该用户可以授权,用 `WITH GRANT OPTION` 命令:\n\n```bash\nGRANT PRIVILEGES ON databasename.tablename TO 'username'@'host' WITH GRANT OPTION;\n```\n\n### 撤销用户权限\n\n```bash\nREVOKE PRIVILEGE ON databasename.tablename FROM 'username'@'host';\n\nREVOKE SELECT ON *.* FROM 'pig'@'%';\n```\n\n**注意:** 假如你在给用户'pig'@'%'授权的时候是这样的(或类似的):\n\n```bash\nGRANT SELECT ON test.user TO 'pig'@'%';\n```\n\n则在使用:\n\n```bash\nREVOKE SELECT ON _._ FROM 'pig'@'%';\n```\n\n命令并 **不能** 撤销该用户对 test 数据库中 user 表的 SELECT 操作。\n\n相反,如果授权使用的是:\n\n```bash\nGRANT SELECT ON _._ TO 'pig'@'%';\n```\n\n则:\n\n```bash\nREVOKE SELECT ON test.user FROM 'pig'@'%';\n```\n\n命令 **也不能** 撤销该用户对 test 数据库中 user 表的 SELECT 权限。\n\n具体信息可以用查看命令:\n\n```bash\nSHOW GRANTS FOR 'pig'@'%';\n```\n\n### 刷新权限\n\n```bash\nFLUSH PRIVILEGES;\n```\n\n### 开启远程访问\n\n通过上面的配置,可以发现:开启 MySQL 远程访问,其实就是修改用户权限:\n\n```bash\n-- 只允许 192.168.1.100 连接\nGRANT ALL PRIVILEGES ON _._ TO 'root'@'192.168.1.100' IDENTIFIED BY 'pwd123' WITH GRANT OPTION;\n\n-- 允许所有 ip 访问\nGRANT ALL PRIVILEGES ON _._ TO 'root'@'%' IDENTIFIED BY 'pwd123' WITH GRANT OPTION;\n```\n\n**注意:** Ubuntu 上还需要修改 MySQL 配置文件\n\n```bash\nsudo vim /etc/mysql/mysql.conf.d/mysqld.cnf\n```\n\n将 `bind-address = 127.0.0.1` 这一行注释掉, 即:\n\n```bash\n#bind-address = 127.0.0.1\n```\n\n重启 MySQL\n\n## 补充\n\n查询当前用户:\n\n```bash\nmysql> SELECT CURRENT_USER();\n```\n\n| HOST | User | 被条目匹配的连接 |\n| ----------------- | ------ | ------------------------------------------------------ |\n| 'thomas.loc.gov' | 'fred' | fred, 从 thomas.loc.gov 连接 |\n| 'thomas.loc.gov' | '' | 任何用户, 从 thomas.loc.gov 连接 |\n| '%' | 'fred' | fred, 从任何主机连接 |\n| '%' | '' | 任何用户, 从任何主机连接 |\n| '%.loc.gov' | 'fred' | fred, 从在 loc.gov 域的任何主机连接 |\n| 'x.y.%' | 'fred' | fred, 从 x.y.net、x.y.com,x.y.edu 等联接(这或许无用) |\n| '144.155.166.177' | 'fred' | fred, 从有 144.155.166.177 的主机连接 |\n| '144.155.166.%' | 'fred' | fred, 从 144.155.166 C 类子网的任何主机连接 |\n\n可以在 Host 字段使用 IP 通配符值(例如,`'144.155.166.%'` 匹配在一个子网上的每台主机),有可能某人可能企图探究这种能力,通过命名一台主机为 144.155.166.somewhere.com。为了阻止这样的企图,MySQL 不允许匹配以数字和一个点起始的主机名,这样,如果你用一个命名为类似 1.2.foo.com 的主机,它的名字决不会匹配授权表中的 Host 列。只有一个 IP 数字能匹配 IP 通配符值。\n\n## References\n\n- [MySQL 创建用户与授权 - 简书](http://www.jianshu.com/p/d7b9c468f20d)\n- [5.7.5. 访问控制, 阶段 1:连接核实](http://www.iteedu.com/database/mysql/mysqlmanualcn/database-administration/connection-access.php)\n\n-- EOF --\n","tags":["mysql"]},{"title":"Ubuntu 命令行下免密码执行 sudo 命令","url":"/2017/04/10/execute-sudo-without-password-in-ubuntu-terminal/","content":"\n解决你的问题的方法是将你的用户加入 sudoers 文件。\n\n```bash\nsudo visudo\n```\n\n在文件底部输入:\n\n```txt\nusername ALL=(ALL) NOPASSWD: ALL\n```\n\n这只适用于终端窗口中的 sudo 命令。例如,当你尝试在 software center 中安装软件包时,将提示你输入密码。\n\n<!--more -->\n\n## References\n\n- [command line - Execute sudo without Password? - Ask Ubuntu](http://askubuntu.com/questions/147241/execute-sudo-without-password)\n","tags":["ubuntu"]},{"title":"Ubuntu 下使用 sendmail mail 发送邮件","url":"/2017/04/06/using-sendmail-mail-in-ubuntu/","content":"\n使用邮件发送程序的执行情况、运行日志都非常方便,Ubuntu 下搭建邮件服务也不复杂。\n\n## sendmail\n\n### install\n\n```bash\nsudo apt-get install sendmail\n```\n\n<!-- more -->\n\n### configure\n\nrun sendmail's config and answer `Y` to everything\n\n```bash\nsudo sendmailconfig\n```\n\n## mail\n\n### install\n\n```bash\nsudo apt-get install mailutils\n```\n\n## test\n\n```bash\necho 'test-email-content' | mail -s 'email title' [email protected]\n```\n\n## sendmail mail 区别\n\n先需要搞清三个概念:\n\n- 邮件用户代理(MUA,Mail User Agent)\n- 邮件传送代理(MTA,Mail Transport Agent)\n- 邮件分发代理(MDA,Mail Deliver Agent)\n\nsedmail 是负责邮件传输的 MTA,类似 apache、nginx 的作用。mail 是用户使用客户端 MUA,类似 foxmail。\n\n## References\n\n- [Install and configure Sendmail on Ubuntu](https://gist.github.com/adamstac/7462202)\n- [Linux 下 mail、mailx 和 sendmail 的区别? - 知乎](https://www.zhihu.com/question/19728556)\n","tags":["ubuntu"]},{"title":"Git 在工作目录之间使用 push 进行同步","url":"/2017/03/30/git-synchronizing-between-working-directories-by-push/","content":"\n_Pushing to a non-bare repo is now possible (Git 2.3.0 February 2015)._\n\nAnd it is possible when you are pushing the branch currently checked out at the remote repo!\n\n现在已经是可以在俩个 non-bare 的仓库之间推送代码。\n\n只需要再远程仓库配置:\n\n```bash\ngit config receive.denyCurrentBranch updateInstead\n```\n\n就可以直接 `push` 分支到远程,并更新工作区。此方法可以用于项目部署。\n\n<!--more -->\n\n**receive-pack: add another option for `receive.denyCurrentBranch`**\n\nWhen synchronizing between working directories, it can be handy to update the current branch via 'push' rather than 'pull', e.g. when pushing a fix from inside a VM, or when pushing a fix made on a user's machine (where the developer is not at liberty to install an ssh daemon let alone know the user's password).\n\nThe common workaround – pushing into a temporary branch and then merging on the other machine – is no longer necessary with this patch.\n\nThe new option is:\n\n`updateInstead`: Update the working tree accordingly, but refuse to do so if there are any uncommitted changes.\n\nin remote repo:\n\n```bash\ngit config receive.denyCurrentBranch updateInstead\n```\n\nand then you can use `git push` to synchronize between working directories in local repo.\n\n## References\n\n- [cannot push into git repository - Stack Overflow](http://stackoverflow.com/questions/3221859/cannot-push-into-git-repository)\n- [receive-pack: add another option for receive.denyCurrentBranch](https://github.com/git/git/commit/1404bcbb6b3bdb248d32024430644e55faec91ce)\n","tags":["git"]},{"title":"解决 SSH 连接提示 Permission denied publickey","url":"/2017/03/30/resolving-ssh-permission-denied-publickey/","content":"\n服务器是使用 publickey 进行连接,当在 git push 时发生 `Permission denied (publickey)`。同时解决 ssh-add 重启后失效。\n\n## 解决\n\n```bash\nssh-add your_publickey\n```\n\n如果遇到报错\n\n```bash\nCould not open a connection to your authentication agent.\n```\n\nTry to\n\n```bash\neval `ssh-agent`\n```\n\n<!--more -->\n\n**注意:** 在重启电脑后失效,一直没有找的其他合适的解决方案,所以选择在 `~/.bashrc` 或 `~/.zshrc` 中添加:\n\n```bash\nssh-add your_publickey 2> /dev/null\n```\n\n`2> /dev/null` 是为了保持静默运行\n\n## References\n\n- [ssh 连接提示 Permission denied (publickey) 怎么破? | 吴川斌的博客](http://www.mr-wu.cn/ssh-permission-denied-publickey/)\n","tags":["linux","ssh"]},{"title":"解决 Ubuntu Sogou 无法选词输入中文","url":"/2017/03/17/resolving-ubuntu-sogou-can-not-select-word/","content":"\nsogou 输入法突然无法选词输入中文,候选词位置出现白框,多次重重装 fcitx 和 sogou 也没有解决。尝试使用 google pinyin 代替,但是感觉很不顺手。\n\n<!-- more -->\n\n## issue in GitHub\n\nsogou 输入法 GitHub 上的一些 issue:\n\n- [#43](https://github.com/FZUG/repo/issues/43)\n- [#177](https://github.com/FZUG/repo/issues/177)\n- [#179](https://github.com/FZUG/repo/issues/179)\n\n## 解决\n\n### 方案一\n\ntry to lastest version\n\n### 方案二\n\nclean `fcitx`, `SogouPY*`, `sogou-qimpanel` in `~/.config`, then relogin and try again\n\n```bash\ncd ~/.config\nsudo rm -rf fcitx SogouPY sogou-qimpanel\n```\n\n## References\n\n- [ubuntu-sogou-不能显示候选词 | Color Win's Notes](https://colorwin.github.io/2017/02/17/ubuntu-sogou/)\n\n-- EOF --\n","tags":["ubuntu"]},{"title":"Git pull rebase 和 merge no-ff 保持提交线图整洁","url":"/2017/03/17/git-pull-rebase-and-merge-no-ff-to-keep-clear-commit-graph/","content":"\ngit log 中的一个清晰的提交线图是很方便进行 code review 和代码回退\n`git pull --rebase` 主要是为是将提交约线图平坦化,而 `git merge --no-ff` 则是刻意制造分叉\n\n## pull rebase\n\n> perform a rebase after fetching\n\n### 状况\n\nGit 作为分布式版本控制系统,所有修改操作都是基于本地的,在团队协作过程中,假设你和你的同伴在本地中分别有各自的新提交,而你的同伴先于你 push 了代码到远程分支上,所以你必须先执行 `git pull` 来获取同伴的提交,然后才能 push 自己的提交到远程分支。\n\n![170317-git-pull-rebase-and-merge-no-ff-to-keep-clear-commit-graph-01](https://user-images.githubusercontent.com/9289792/80202129-c1cdfb00-8657-11ea-814e-49f8618f301c.jpg)\n\n按照 Git 的默认策略,如果远程分支和本地分支之间的提交线图有分叉的话(即不是 fast-forwarded),Git 会执行一次 merge 操作,因此产生**一次没意义的提交记录**,从而造成了像上图那样的混乱。\n\n### 解决\n\n其实在 pull 操作的时候,使用 `git pull --rebase` 选项即可很好地解决上述问题。 加上 `--rebase` 参数的作用是,提交线图有分叉的话,Git 会 `rebase` 策略来代替默认的 `merge` 策略。\n假设提交线图在执行 pull 前是这样的:\n\n```bash\n A---B---C remotes/origin/master\n /\n D---E---F---G master\n```\n\n如果是执行 `git pull` 后,结果多出了 H 这个没必要的提交记录。提交线图会变成这样:\n\n```bash\n A---B---C remotes/origin/master\n / \\\n D---E---F---G---H master\n```\n\n如果是执行 `git pull --rebase` 的话,提交线图就会变成这样:\n\n```bash\n remotes/origin/master\n |\n D---E---A---B---C---F'---G' master\n```\n\nF G 两个提交通过 `rebase` 方式重新拼接在 C 之后,多余的分叉去掉了,目的达到。\n\n### 注意\n\n使用 `git log --graph` 可查看提交线图\n使用 `git pull --rebase` 是为了使提交线图更好看,从而方便 code review 和代码回退\n使用 `git pull --rebase` 比直接 pull 容易导致冲突的产生,如果预期冲突比较多的话,建议还是直接 pull\n\n<!--more -->\n\n## merge no-ff\n\n> generate a merge commit even if the merge resolve\n\n### 例子 1\n\n`git pull --rebase` 策略目的是修整提交线图,使其形成一条直线,而即将要用到的 `git merge --no-ff <branch-name>` 策略偏偏是反行其道,刻意地弄出提交线图分叉出来。\n\n假设你在本地准备合并两个分支,而刚好这两个分支是 fast-forwarded 的,那么直接合并后你得到一个直线的提交线图,当然这样没什么坏处,但如果你想更清晰地告诉你同伴:这一系列的提交都是为了实现同一个目的,那么你可以刻意地将这次提交内容弄成一次提交线图分叉。\n\n执行 `git merge --no-ff <branch-name>` 的结果大概会是这样的:\n\n![170317-git-pull-rebase-and-merge-no-ff-to-keep-clear-commit-graph-02](https://user-images.githubusercontent.com/9289792/80202132-c397be80-8657-11ea-8135-781a36fc64e5.jpg)\n\n中间的分叉线路图很清晰的显示这些提交都是为了实现:**complete adjusting user domains and tags**\n\n### 例子 2\n\n往往在合并分支之前(假设要在本地将 feature 分支合并到 dev 分支),会先检查 feature 分支是否部分落后于远程 dev 分支:\n\n```bash\ngit checkout dev\ngit pull # 更新 dev 分支\ngit log feature..dev # 对比\n```\n\n如果没有输出任何提交信息的话,即表示 feature 对于 dev 分支是 up-to-date 的。如果有输出的话而马上执行了 `git merge --no-ff` 的话,提交线图会变成这样:\n\n![170317-git-pull-rebase-and-merge-no-ff-to-keep-clear-commit-graph-03](https://user-images.githubusercontent.com/9289792/80202134-c4305500-8657-11ea-8c4b-52f858f669ec.jpg)\n\n所以这时在合并前,通常先执行:\n\n```bash\ngit checkout feature\ngit rebase dev\n```\n\n这样就可以将 feature 重新拼接到更新了的 dev 之后,然后就可以合并了。这时分叉点将上移,最终得到一个干净舒服的提交线图\n\n## 总结\n\n- 使用 `git pull --rebase` 和 `git merge --no-ff` 其实和直接使用 `git pull` `git merge` 得到的代码应该是一样。\n- 使用 `git pull --rebase` 主要是为是将提交约线图平坦化,而 `git merge --no-ff` 则是刻意制造分叉。\n\n## References\n\n- [洁癖者用 Git:pull --rebase 和 merge --no-ff](http://hungyuhei.github.io/2012/08/07/better-git-commit-graph-using-pull---rebase-and-merge---no-ff.html)\n\n-- EOF --\n","tags":["git"]},{"title":"禁止 Google 根据区域重定向跳转","url":"/2017/03/14/google-no-country-redirection/","content":"\n使用代理上 Google 时,Google 常会根据网络代理的区域进行重定向跳转。例如:使用韩国代理时,`google.com` 会跳转到 `https://www.google.co.kr/`,搜索结果也多为韩语,很是不方便。\n\n解决的办法其实也很简单:访问 `https://www.google.com/ncr` 就可以了。\n\n<!-- more -->\n\n`ncr` 表示:No Country Redirection,禁止区域重定向。\n\n-- EOF --\n"},{"title":"Git 修改提交历史","url":"/2017/03/13/git-modify-commits-history/","content":"\n在使用 Git 时,我们经常会遇到修改本地提交记录的情况。比如:修改最近一次提交记,还比如:将多次小的 `commit` 合并成一个大的 `commit`。\n\n这种做发有利也有弊,利在:review 代码时,可以按功能看,可以省去 review 一些前期写的无效的代码;弊是:一次提交修改过多,如果有问题,不利于调试。\n\n具体情况具体分析,是解决问题的金句。\n\n## 修改最近一次提交记录\n\n### 修改提交说明\n\n如果只想更改最近一次的提交说明,只需输入:\n\n```bash\ngit commit --amend\n```\n\n然后你就会进入文本编辑器,输入你想要的内容,保存并退出即可\n\n### 改被提交的快照\n\n如果你完成 `commit` 后又想修改被提交的快照,增加或者修改其中的文件。\n\n先执行 `git add` 命令,将修改的文件添加到缓存区,然后运行 `git commit -amend` 命令,该命令会获取你当前的暂存区的内容一并提交到最后一次 `commit`\n\n例如:新加了一个文件 `new_file.cpp` ,想要合并到最后一次提交,过程如下:\n\n```bash\ngit add new_file.cpp\ngit commit -amend\n```\n\n也可以直接运行下面的命令,不过要小心,不要提交了多余的文件\n\n```bash\ngit commit -a -amend\n```\n\n### 将文件从本次提交中移除\n\n如果想把已经 `commit` 的文件从这次 `commit` 移除的话,运行命令:\n\n```bash\ngit reset [-soft] HEAD~1 # -soft可加可不加,默认就是soft选项\ngit checkout -filename # 要从本次提交移除的文件名\ngit commit -m \"new commit\"\n```\n\n<!-- more -->\n\n## 修改多个提交记录\n\n要修改历史中更早的提交,你必须采用更复杂的工具。Git 没有一个修改历史的工具,但是你可以使用 rebase 工具来衍合一系列的提交到它们原来所在的 HEAD 上。\n\n依靠这个交互式的 rebase 工具,你就可以停留在每一次提交后,如果你想修改或改变说明、增加文件或任何其他事情。你可以通过给 `git rebase -i` 命令以交互方式进行 rebase。\n\n例如,你想修改最近三次的提交说明,或者其中任意一次,你必须给 `git rebase -i` 提供一个参数,指明你想要修改的提交的父提交。\n\n例如 `HEAD~3` 是指从 HEAD 指针到 HEAD+3 的位置,也就是最近第 4 次提交。所以想修改最近 3 次提交,你需要指明第 3 次提交的父提交(第 4 次提交)即 `HEAD~3`。运行命令:\n\n```bash\ngit rebase -i HEAD~3\n```\n\n再次提醒这是一个衍合命令,也就是 HEAD~3 到 HEAD 范围内的每一次提交都会被重写,不管你是否修改提交说明 SHA-1 的值都会发生变化。\n\n所以千万不要涵盖你已经推送到中心服务器的提交。这么做会使其他开发者产生混乱,因为你提供了同样变更的不同版本。运行该命令后进入交互界面,类似:\n\n```bash\npick fecb551 Init the view model\npick bb199a0 Update the version\npick bc5cd9d Add new method\n\n# Rebase f77f585..fecb551 onto f77f585\n#\n# Commands:\n# p, pick = use commit\n# r, reword = use commit, but edit the commit message\n# e, edit = use commit, but stop for amending\n# s, squash = use commit, but meld into previous commit\n# f, fixup = like \"squash\", but discard this commit's log message\n# x, exec = run command (the rest of the line) using shell\n#\n# These lines can be re-ordered; they are executed from top to bottom.\n# If you remove a line here THAT COMMIT WILL BE LOST.\n# However, if you remove everything, the rebase will be aborted.\n# Note that empty commits are commented out\n```\n\n根据命令提示,就可以进行历史更改了。很重要的一点是你得注意这些提交的顺序与你通常通过 log 命令看到的是相反的。\n\n如果你运行 log,你会看到下面这样的结果:\n\n```bash\npick bc5cd9d Add new method\npick bb199a0 Update the version\npick fecb551 Init the view model\n```\n\n### 修改指定提交\n\n例如:只修改最近第 3 次提交说明可以进行如下更改:\n\n```bash\nreword fecb551 Init the view model\npick bb199a0 Update the version\npick bc5cd9d Add new method\n...\n```\n\n保存并退出编辑器,rebase 命令在衍合到第 3 次提交时会进入提交说明编辑页面,在此进行编辑新的提交说明,保存并退出即可,rebase 命令继续进行直至完成全部衍合操作。\n\n如果你不仅想要修改提交说明,还要更改提交,可以进行如下更改:\n\n```bash\nedit fecb551 Init the view model\npick bb199a0 Update the version\npick bc5cd9d Add new method\n...\n```\n\n保存并退出编辑器,rebase 命令在衍合到第三次提交时会等待你提交新的更改,并提示你修改完成后运行 `git commit –amend` 命令,然后运行 `git rebase –continue` 继续进行 rebase 直至完成全部衍合。\n\n### 重排提交\n\n你也可以使用 `git rebase -i` 命令对提交历史彻底重排或删除提交。例如你想删除”Update the version”这个提交,并且修改其他两次提交的顺序,可以将:\n\n```bash\npick fecb551 Init the view model\npick bb199a0 Update the version\npick bc5cd9d Add new method\n...\n```\n\n改为:\n\n```bash\npick bc5cd9d Add new method\npick fecb551 Init the view model\n```\n\n然后保存并退出编辑器,此时 rebase 命令会先应用 bc5cd9d (Add new method),然后应用 fecb551 (Init the view model) bb199a0 Update the version 这次提交。然后保存并退出编辑器,此时 rebase 命令会先应用 bc5cd9d (Add new method),然后应用 fecb551 (Init the view model),接着停止。\n\n执行完上诉操作,你已经修改了这些提交的顺序,并且删除了 bb199a0 (Update the version) 这次提交。\n\n### 合并提交\n\n`git rebase -i` 命令还可以将一系列提交合并成一个提交。从上面的脚本提示中可以看到 s, squash = use commit, but meld into previous commit 提示。\n\n如果用 squash 修饰提交就可以进行提交之间的合并,例如可以将脚本修改成这样:\n\n```bash\npick fecb551 Init the view model\nsquash bb199a0 Update the version\nsquash bc5cd9d Add new method\n```\n\n保存并退出编辑器,rebase 命令会应用全部三次变更然后进入编辑器来归并三次提交说明。当你保存之后,你就拥有了一个包含前三次提交的全部变更的单一提交。\n\n### 拆分提交\n\n拆分提交实际上就是撤销一次提交,然后分多次进行重新提交。例如你想将三次提交中的中间一次拆分。将 ”Update the version” 拆分成两次提交:”Update the version1” 和 ”Update the version2”,可以进行如下修改:\n\n```bash\npick fecb551 Init the view model\nedit bb199a0 Update the version\npick bc5cd9d Add new method\n```\n\n当 rebase 到 bb199a0 时,会进入等待你提交新 commit 的状态,这时看可以运行 git reset HEAD^ 对当前提交进行重置,然后分别运行 git add 命令添加想要提交的文件,分别进行 git commit,最后运行 git rebase –continue 完成所有衍合。整体过程如下:\n\n```bash\ngit reset HEAD^\ngit add file1\ngit commit -m 'Update the version1'\ngit add file2\ngit commit -m 'Update the version2'\ngit rebase --continue\n```\n\n执行完上诉操作,提交历史看起来就像这样了:\n\n```bash\n1c002dd Add new method\n9b29157 Update the version2\n35cfb2b Update the version1\nf3cc40e Init the view model\n```\n\n再次提醒,这会修改你列表中的提交的 SHA 值,所以请确保这个列表里不包含你已经推送到共享仓库的提交。\n\n## References\n\n- [Git 修改提交历史 - GLGJing’s Blog](http://glgjing.github.io/blog/2015/01/06/git-xiu-gai-ti-jiao-li-shi/)\n","tags":["git"]},{"title":"PHP empty 方法判断 0.0","url":"/2017/03/09/php-empty-method-judge-0/","content":"\n在使用 `empty(mixed $var)` 时要考虑 `$var` 的 **类型**,尤其是在判断数据库查询后的字段。\n\n<!-- more -->\n\n```txt\nbool empty(mixed $var)\n```\n\n以下的东西被认为是空 `true` 的:\n\n- `\"\"`(空字符串)\n- `0` (作为整数的 0)\n- `0.0` (作为浮点数的 0)\n- `\"0\"` (作为字符串的 0)\n- `NULL`\n- `FALSE`\n- `array()` 一个空数组\n- `$var` 未初始化的变量\n- `new stdClass()` 不包含任何属性的对象\n\n**注意:** string 的判断要非常注意,数据库查询后的字段常常为 string,应该进行正确的类型转换。\n\n以下的东西被认为是非空 `false` 的:\n\n- $var = true;\n- $var = 1;\n- $var = -1;\n- $var = \"0.0\";\n- $var = \"foo\";\n- $var = array(0);\n- $var = new stdClass(); $var->property = null;\n\n```php\n$str = '0.0';\necho empty($str); // false 很可能和预期是相反的\necho empty((float)$str); // true\n```\n\n-- EOF --\n","tags":["php"]},{"title":"Linux crontab 内容定时备份","url":"/2017/03/07/linux-crontab-content-backup/","content":"\n`crontab -r` 是一个很危险的命令,它将直接重置 crontab 中的内容;输入 `crontab` 后,使用 `ctrl + d` 退出也将清空 crontab 中的内容。所以 crontab 内容的定时备份也变得有必要了。\n\n## 备份脚本\n\n`crontab_bak.sh`\n\n```bash\n#!/bin/bash\ncrontab -l > /home/tom/crontab_bak/bak`date '+%Y%m%d_%H%M%S'`.txt\n```\n\nconfig in crontab\n\n```bash\n12 12 * * * /bin/bash /home/tom/crontab_bak/crontab_bak.sh\n```\n\n<!-- more -->\n\n## crontab 常用命令\n\n```bash\ncrontab -l # 列举 crontab 的任务\ncrontab -e # 编辑 crontab 的任务\ncrontab -r # 删除 crontab 的任务;风险\ncrontab -h # crontab 的帮助\ncrontab -i # 删除 crontab 前进行提示\n```\n\n-- EOF --\n","tags":["linux"]},{"title":"解决 Ubuntu warning Setting locale failed","url":"/2017/03/03/resolving-ubuntu-warning-setting-locale-failed/","content":"\n在配置新服务器时遇到 `Setting locale failed` 的警告,要求 `Please check that your locale settings`\n\n```bash\nperl: warning: Setting locale failed.\nperl: warning: Please check that your locale settings:\n LANGUAGE = (unset),\n LC_ALL = (unset),\n LC_MESSAGES = \"zh_CN.UTF-8\",\n LANG = \"zh_CN.UTF-8\"\n are supported and installed on your system.\nperl: warning: Falling back to the standard locale (\"C\").\n```\n\n<!-- more -->\n\n## 解决方法\n\n### 安装 localepurge 管理语言文件\n\n```bash\nsudo apt-get install localepurge\n```\n\n选择我们想要的语言,例如 `en_US.UTF-8` 和 `zh_CN.UTF-8`\n当然也可以使用以下命令再次进行配置:\n\n```bash\nsudo dpkg-reconfigure localepurge\n```\n\n### 生成自己想要的语言\n\n```bash\nsudo locale-gen zh_CN.UTF-8 en_US.UTF-8\n```\n\n打印出当前的配置信息\n\n```bash\nlocale\n```\n\n默认情况下终端 ssh 的时候会将本地的 locale 传到服务器中,可以通过命令指定 ssh 服务器的语言:\n\n```bash\nLC_ALL=en_US.UTF-8 ssh <host>\n```\n\n## References\n\n- [ubuntu 解决语言设置错误的问题 —— 文翼的博客](http://wenzhixin.net.cn/2014/01/11/ubuntu_setting_locale_failed)\n\n-- EOF --\n","tags":["ubuntu"]},{"title":"回顾 2016","url":"/2016/12/31/review-2016/","content":"\n2016 肯定是很特别的一年。年初离开了老师的队伍,来北京找实习。年中和 318 告别,大学毕业了。年末换房学做饭,开始另种生活。对于 2017 更多了对自己的期待。\n\n<!-- more -->\n\n<iframe frameborder=\"no\" border=\"0\" marginwidth=\"0\" marginheight=\"0\" width=260 height=52 src=\"//music.163.com/outchain/player?type=2&id=31445772&auto=0&height=32\"></iframe>\n\n2016 是在保定的雾霾中开始。结束了大为,决定离开老师的队伍,来北京实习,这对我来说是一个不大不小的决定。老师带着,跟学长一起走到这里,这里有了很多宝贵的东西,有很多回忆,很多故事可以讲。选择离开,主要还是想看看在这之外的是什么样子。\n\n朋友、老师、爸妈都为我的实习操过心,不过推荐的地方在各种奇怪的理由下都没有成。自己开始在拉勾上投简历,一次次来返在京石铁路上。面试一直是不顺的。完全没有想到对实习生有如此高的要求,很多技术问题正中下怀。心里是有落差的,在我当时的想法里,那些在工程中是用不易用到的,但也感到自己技术知识的浅薄。面试了俩三家后也发现了套路,回答不好的、答不上的问题,回去查查准备准备,下回就能用上。\n\n没什么难度的大学毕业。大学里更多的收获是跟着老师、学长在课堂外搞事,是和宿舍的、好朋友在一起的时间。真的好幸运啊,遇到 318 的舍友们。没想到和你们每一个人都超级聊的来。大四的时候人就渐渐不齐,但只要宿舍在,不到最后毕业真的不知道要分别了。我们相互影响着成长,至少除去了我很多很 low 的思想。我们相互练着酒量、每天演着戏、日常说着骚话。\n\n德育答辩上一个同学说:我常翘课、考试挂科,但我大学四年过的很坦荡,我会因为和舍友游戏上的一个好玩的套路高兴很久。我收获朋友。我没什么后悔的。\n这很戳中我的泪点。没什么后悔的,也是对我大学的注脚。愿你们成家的幸福,工作的有成。\n\n从上学到工作,过渡的很平稳。工作中有一些挑战,但总之是还应对的来。可没有什么出众的地方,我不想过的太平凡,愿有一技之长,独挡一面。我常说自己:菜啊、傻啊、什么的,我确实不想用这些来词来逃避问题,我想尽快的甩掉这些。我也愿自己可以更有底气和勇气说出,现在不能说出的话,愿更决绝的对大事小情做决定。\n\n年末随着公司搬家,自己也搬了。炒菜做饭成了每天另一部分。也许才是真正生活二字的开始。\n\n突然想到学长提的一个问题:你们有谁把技术当做以后安身立命的本事?\n现在对于我来说,这问题的答案比任何时候都更加清晰。\n\n最后,还是怀揣着 2014 末的敬请期待,让一切皆有可能,开始 2017。\n\n—— 送给 木林木丶\n\n-- EOF --\n","categories":["review"]},{"title":"Ubuntu 下使用 UFW 管理防火墙服务","url":"/2016/10/10/manage-iptables-using-ufw-in-ubuntu/","content":"\nUFW (Uncomplicated Firewall) 作为 `iptables` 的前端应用,给用户提供了简单的接口界面。使用着不需要去记非常复杂的 `iptables` 语法。UFW 也使用了 简单英语 作为它的参数。像 allow、deny、reset 就是他们当中的一部分。UFW 绝对是那些想要快速、简单的就建立自己的防火墙,而且还很安全的用户的最佳替代品之一。\n\n## 检查系统上是否已经安装 UFW\n\n```bash\nsudo dpkg --get-selections | grep ufw\n```\n\n## 安装 UFW\n\n```bash\nsudo apt-get install ufw\n```\n\n<!-- more -->\n\n## UFW 常用命令\n\n### 查看 UFW 状态\n\n```bash\nsudo ufw status\n```\n\n### see configured rules even when inactive\n\n```\nufw show added\n```\n\n### 启用/禁用 UFW\n\n```bash\n# 启用\nsudo ufw enable\n# 禁用\nsudo ufw disable\n```\n\n先添加 `ufw allow ssh`,防止墙掉自己\n\n### 查看 UFW 规则\n\n需要先启用 UFW\n\n```bash\nsudo ufw status verbose\n```\n\n在每条规则上加个序号数字\n\n```bash\nsudo ufw status numbered\n```\n\n### 添加 UFW 允许规则\n\n允许特定服务程序\n\n```bash\nsudo ufw allow ssh\n```\n\n允许特定服务程序特定协议\n\n```bash\nsudo ufw allow ssh/tcp\n```\n\n允许特定端口\n\n```bash\nsudo ufw allow 8080\n```\n\n允许特定端口特定协议\n\n```bash\nsudo ufw allow 8080/tcp\n```\n\n允许范围端口特定协议,必须指明协议 udp 或 tcp\n\n```bash\nsudo ufw allow 2290:2300/tcp\n```\n\n允许特定 IP\n\n```bash\nsudo ufw allow from 192.168.0.104\n```\n\n允许范围 IP\n\n```bash\nsudo ufw allow from 192.168.0.0/24\n```\n\n- [networking - UFW - allow range of IP addresees? - Ask Ubuntu](https://askubuntu.com/questions/646424/ufw-allow-range-of-ip-addresees)\n- [IPv4 subnetting reference - Wikipedia](https://en.wikipedia.org/wiki/IPv4_subnetting_reference)\n\n组合参数\n\n可以把 IP 地址、协议和端口这些组合在一起用。创建一条规则,限制仅仅来自于 192.168.0.104 的 IP ,而且只能使用 tcp 协议和通过 22 端口来访问本地资源:\n\n```bash\nsudo ufw allow from 192.168.0.104 proto tcp to any port 22\n```\n\n### 添加 UFW 拒绝规则\n\n创建拒绝规则的命令和允许的规则类似,仅需要把 `allow` 参数换成 `deny` 参数就可以。\n\n```bash\nsudo ufw deny ftp\n```\n\n### 删除 UFW 规则\n\n方法一:\n\n```bash\nsudo ufw delete allow ftp\n```\n\n方法二:按行号删除\n\n```bash\n# 显示行号\nsudo ufw status numbered\n\n# 按行号删除\nsudo ufw delete 1\n```\n\n### 重置 UFW 所有规则\n\n```bash\nsudo ufw reset\n```\n\n## References\n\n- [Debian/Ubuntu 系统中安装和配置 UFW-简单的防火墙-技术 ◆ 学习|Linux.中国-开源社区](https://linux.cn/article-2489-1.html)\n- [See configured rules even when inactive](https://askubuntu.com/questions/30781/see-configured-rules-even-when-inactive)\n","tags":["ubuntu"]},{"title":"【Core Java】读书笔记 Part 2","url":"/2016/09/23/core-java-reading-notes-part2/","content":"\n本文总结的是书中的:第 5 章 继承\n\n前几章的总结在:[ZYF.IM-【Core Java】读书笔记 Part1](/2016/05/06/core-java-reading-notes/)\n\n<!-- more -->\n\n## 5 继承\n\n### 5.1 类、超类和子类\n\n1、有些人认为 `super` 与 `this` 引用是类似的概念,实际上,这样比较并不太恰当。这是因为 `super` 不是一个对象的引用,不能将 `super` 赋予另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。\n2、使用 `super` 或 `this` 调用构造器的语句必须时子类构造器的第一条语句。也就是说 `super` 和 `this` 不能同时出现在一个构造器中。\n3、`this` 关键字。\n\n- 引用隐式参数\n- 调用该类其他的构造器\n\n4、`super` 关键字\n\n- 调用超类的方法\n- 调用超类的构造器\n\n5、调用构造器的语句只能作为另一个构造器的第一条语句出现。\n6、一个对象变量可以指示多种实际类型的现象被称为多态。在运行时能够自动的选择则调用哪个方法的现象称为动态绑定。\n7、`Manager[] managers = new Manager[10];` 将它转换成 `Employee[]` 数组是完全合法的:\n`Employee[] staff = managers;// OK`\n毕竟如果`manager[i]`是一个 Manager,也一定是一个 Employee,然而,实际上,将会发生一些令人惊讶的事情。要切记 managers 和 staff 引用的时同一个数组。现在请看:\n`staff[0] = new Employee(\"Harry Hacker\", ..);`\n编译器竟然接纳了这个赋值操作。`staff[0]`与`manager[0]`引用的是同一个对象,似乎我们把一个普通雇员擅自归入了经理行列中了。这时一个种很忌讳发生的情形,当调用`managers[0].setBonus(1000)`的时候,将会导致调用一个不存在的实例域,今儿搅乱相邻存储空间的内容。\n为了确保不发生这类错误,所有数组都要牢记创建它们的元素类型,并负责监督仅将类型兼容的引用存储到数组中。\n\n8、调用对象方法的执行过程\n\n- 编译器查看对象的声明类型和方法名。假设调用 x.f(param),编译器将会一一列举所有 C 类中名为 f 的方法和其他超类中访问属性为 public 且名为 f 的方法(超类的私有方法不可访问)。\n- 编译器将查看调用方法时提供的参数类型。如果在所有名为 f 的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。这个工程称为重载解析。\n- Tips:返回类型不是签名方法的一部分,因此,在覆盖方法时,一定要保证返回类型的兼容性。允许子类将覆盖方法的返回类型定义为原返回类型的子类型。\n- 如果是 private 方法、static 方法、final 方法或者构造器,那么编译器将可以准确的知道应该调用哪个方法,我们将这种调用方式称为静态绑定。与此对应,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。\n\n9、动态绑定与静态绑定区别对比\n9.1)静态绑定发生在编译时期,动态绑定发生在运行时\n9.2)使用 private 或 static 或 final 修饰的变量或者方法,使用静态绑定。而虚方法(可以被子类重写的方法)则会根据运行时的对象进行动态绑定。\n9.3)静态绑定使用类信息来完成,而动态绑定则需要使用对象信息来完成。\n9.4)重载(Overload)的方法使用静态绑定完成,而重写(Override)的方法则使用动态绑定完成。\n\n10、当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与 x 所引用对象的实际类型最合适的那个类的方法。假设 x 的实际类型是 D,它是 C 类的子类。如果 D 类定义了方法 f(String),就直接调用它;否则,将在 D 类的超类中寻找 f(String),以此类推。\n\n11、每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表,其中列出了所有方法的签名和实际调用的方法。这样一来,在真正调用方法的时候,虚拟机仅查找这个表就行了。\n\n### 5.1 类、超类和子类\n\n1、4 个访问修饰符\n\n- 仅对本类可 - private\n- 对所有类可见 - publice\n- 对本包和所有子类可见 - protected\n- 对本包可见 - 默认,不需要修饰符\n\n2、编写一个完美的 equals 方法的建议\n2.1)参数命名为 otherObject,稍后需要将它转换成另一个叫做 other 的变量\n2.2)检测 this 与 otherObject 是否引用同一个对象:\n`if(this == otherObject) return true;`\n这条语句只是一个优化。实际上,这是一种经常采用的形式。因为计算这个等试要比一个一个的比较类中的域付出的代价小得多。\n2.3)检测 otherObject 是否为 null,如果为 null,返回 false。这项检测时很有必要的。\n`if(otherObject == null) return false;`\n2.4)比较 this 与 otherObject 是否属于同一个类。如果 equals 的语义在每一个子类中有所改变(子类能够拥有自己的相等概念),就使用 getClass 检测:\n`if(getClass() != otherObject.getClass()) return false;`\n如果所有的子类都拥有统一的语义(由超类决定相等的概念)就使用 instanceof 检测:\n`if(!(otherObject instanceof ClassName)) return false;`\n2.5)将 otherObject 转换为相应的类类型变量\n`ClassName other = (ClassName)otherObject;`\n2.6)现在开始对所有需要比较的域进行比较了。使用 == 比较基本类型域,使用 equals 比较对象域。如果所有的域都匹配,就返回 true;否则返回 false\n2.7)完整的例子:\n\n```java\nclass Employee {\n\t...\n\tpublic boolean equals(Object otherObject) {\n\t\t// a quick test to see if the objects are identical\n\t\tif(this == otherObject) return true;\n\n\t\t// must return false if the explicit parameter is null\n\t\tif(otherObject == null) return false;\n\n\t\t// if the classes don`t match, they can`t be equal\n\t\tif(getClass() != otherObject.getClass()) return false;\n\n\t\t// now we know otherObject is a non-null values\n\t\tEmloyee other = (Emloyee)otherObject;\n\n\t\t// test whether the fields have identical values\n\t\treturn name.equals(other.name) && salary == other.salary\n\t\t&& hrieDay.equals(other.hirDay);\n\t}\n}\n```\n\n## References\n\n- [技术小黑屋-Java 中的静态绑定和动态绑定](http://droidyue.com/blog/2014/12/28/static-biding-and-dynamic-binding-in-java/)\n","tags":["java"]},{"title":"LeetCode Shell 解题集合","url":"/2016/09/19/leetcode-shell-solutions/","content":"\nLeetCode Shell 的试题多为文本操作,[195. Tenth Line](https://leetcode.com/problems/tenth-line/)、[193. Valid Phone Numbers](https://leetcode.com/problems/valid-phone-numbers/)、[192. Word Frequency](https://leetcode.com/problems/word-frequency/)、[194. Transpose File](https://leetcode.com/problems/transpose-file/) 暂时只有 4 道题,就整合在这一起了\n\nShell 中文本处理的事情基本 `awk` `sed` `grep` `sort` `uniq` `tail` `head` 几个命令组合组合就搞定了\n\n<!-- more -->\n\n## [195. Tenth Line](https://leetcode.com/problems/tenth-line/)\n\n### 大体意思\n\nHow would you print just the 10th line of a file?\nFor example, assume that file.txt has the following content:\n\n```\nLine 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9\nLine 10\n```\n\nYour script should output the tenth line, which is:\n\n```\nLine 10\n```\n\n打印出文本文件中第 10 行的内容。\n\n### 自己的解法\n\n```bash\ncat file.txt | tail -n +10 | head -n 1\n```\n\n这是学习了:[linux 如何显示一个文件的某几行(中间几行) - 香格里拉\\(^o^)/](http://www.cnblogs.com/xianghang123/archive/2011/08/03/2125977.html)\n\n### 别人的解法\n\n方法一:\n\n```bash\nawk 'NR==10' file.txt\n# awk的默认动作就是打印$0,所以NR==10后面可以不用加{print $0}\n```\n\n方法二:\n\n```bash\nsed -n '10p' file.txt\n# 如果不够10行,则什么也不打印\n```\n\n方法三:\n\n```bash\nline=$(cat file.txt | wc -l) # 千万注意,等号前后一定不要有空格\nif [ \"$line\" -ge 10 ] ; then # $line的双引号也可以不用加\n cat file.txt | head -n 10 | tail -n 1\nfi\n```\n\nReference: [leetcode-195 Tenth Line - 2> /dev/null](http://blog.csdn.net/sole_cc/article/details/44977821)\n\n## [193. Valid Phone Numbers](https://leetcode.com/problems/valid-phone-numbers/)\n\n### 大体意思\n\nGiven a text file `file.txt` that contains list of phone numbers (one per line), write a one liner bash script to print all valid phone numbers.\nYou may assume that a valid phone number must appear in one of the following two formats: (xxx) xxx-xxxx or xxx-xxx-xxxx. (x means a digit)\nYou may also assume each line in the text file must not contain leading or trailing white spaces.\n\nFor example, assume that `file.txt` has the following content:\n\n```\n987-123-4567\n123 456 7890\n(123) 456-7890\n```\n\nYour script should output the following valid phone numbers:\n\n```\n987-123-4567\n(123) 456-7890\n```\n\n匹配有效的电话号码\n\n### 自己的解法\n\n```bash\ncat file.txt | grep '^[0-9]\\{3\\}-[0-9]\\{3\\}-[0-9]\\{4\\}$\\|^([0-9]\\{3\\}) [0-9]\\{3\\}-[0-9]\\{4\\}$'\n```\n\n### 别人的解法\n\n```bash\ngrep -P '^(\\(\\d{3}\\)|\\d{3}-)\\d{3}-\\d{4}$' file.txt\n```\n\nReference: [LeetCode 193: Valid Phone Numbers – Revo](https://ruixublog.wordpress.com/2015/05/13/leetcode-193-valid-phone-numbers/)\n\n## [192. Word Frequency](https://leetcode.com/problems/word-frequency/)\n\n### 大体意思\n\nWrite a bash script to calculate the frequency of each word in a text file `words.txt`.\n\nFor simplicity sake, you may assume:\n\n`words.txt` contains only lowercase characters and space `' '` characters.\nEach word must consist of lowercase characters only.\nWords are separated by one or more whitespace characters.\nFor example, assume that `words.txt` has the following content:\n\n```\nthe day is sunny the the\nthe sunny is is\n```\n\nYour script should output the following, sorted by descending frequency:\n\n```\nthe 4\nis 3\nsunny 2\nday 1\n```\n\nNote:\nDon't worry about handling ties, it is guaranteed that each word's frequency count is unique.\n\n统计单词出现的频次,然后倒序排列\n\n### 别人的解法\n\n```bash\nawk '{i=1;while(i<=NF){print $i;i++}}' words.txt | sort | uniq -c | sort -k1nr | awk '{print $2 \" \" $1}'\n```\n\n1、利用 awk 默认一行一条记录,默认以空格划分每条记录,NF 为划分的总块数先打印出所有单词\n2、排序 + 统计 + 消除重复\n3、输出\n\nReference: [LeetCode 192 Word Frequency - wangxiaobupt 的专栏](http://blog.csdn.net/wangxiaobupt/article/details/45201817)\n\n```bash\nawk '\n{ for (i=1; i<=NF; i++) { ++S[$i]; } }\nEND { for (i in S) { print i, S[i] } }\n' words.txt | sort -nr -k 2\n```\n\nReference: [leetcode/solutions/192.Word_Frequency at master · illuz/leetcode](https://github.com/illuz/leetcode/tree/master/solutions/192.Word_Frequency)\n\n## [194. Transpose File](https://leetcode.com/problems/transpose-file/)\n\n### 大体意思\n\nGiven a text file `file.txt`, transpose its content.\n\nYou may assume that each row has the same number of columns and each field is separated by the `' '` character.\n\nFor example, if `file.txt` has the following content:\n\n```\nname age\nalice 21\nryan 30\n```\n\nOutput the following:\n\n```\nname alice ryan\nage 21 30\n```\n\n### 别人的解法\n\n```\nawk '\n{\n for (i = 1; i <= NF; i++) {\n if(NR == 1) {\n s[i] = $i;\n } else {\n s[i] = s[i] \" \" $i;\n }\n }\n}\nEND {\n for (i = 1; s[i] != \"\"; i++) {\n print s[i];\n }\n}' file.txt\n```\n\nReference: [leetcode/solutions/194.Transpose_File at master · illuz/leetcode](https://github.com/illuz/leetcode/tree/master/solutions/194.Transpose_File)\n\n## 总结\n\nShell 中文本处理的事情基本 `awk` `sed` `grep` `sort` `uniq` `tail` `head` 几个命令组合组合就搞定了,各命令的常用方法之后总结\n","tags":["leetcode"]},{"title":"Ubuntu 下使用 sysv-rc-conf 管理开机启动服务","url":"/2016/09/19/manage-startup-program-using-sysv-rc-conf-in-ubuntu/","content":"\n`sysv-rc-conf` gives an easy to use interface for managing \"/etc/rc{runlevel}.d/\" symlinks. The interface comes in two different flavors, one that simply allows turning services on or off and another that allows for more fine tuned management of the symlinks.\n\n## 安装 sysv-rc-conf\n\n```bash\nsudo apt-get install sysv-rc-conf\n```\n\n## 使用 sysv-rc-conf\n\n```bash\nsudo sysv-rc-conf\n```\n\n操作界面十分简洁,你可以用鼠标点击,也可以用键盘方向键定位,用空格键选择,用 `Ctrl+n` 翻下一页,用 `Ctrl+p` 翻上一页,用 `q` 退出。\n\n<!-- more -->\n\n## 相关知识\n\n### Ubuntu 运行级别\n\nLinux 系统任何时候都运行在一个指定的运行级上,并且不同的运行级的程序和服务都不同,所要完成的工作和要达到的目的都不同,系统可以在这些运行级之间进行切换,以完成不同的工作。\n\nUbuntu 的系统运行级别:\n\n```bash\n0 停机\n1 单用户,Does not configure network interfaces, start daemons, or allow non-root logins\n2 多用户,无网络连接 Does not configure network interfaces or start daemons\n3 多用户,启动网络连接 Starts the system normally.\n4 用户自定义\n5 多用户带图形界面\n6 重启\n```\n\n查看当前运行级别,执行命令:\n\n```bash\nrunlevel\n```\n\nrunlevel 显示上次的运行级别和当前的运行级别,“N”表示没有上次的运行级别。\n\n切换运行级别,执行命令:\n\n```bash\nint [0123456Ss]\n```\n\n即在 init 命令后跟一个参数,此参数是要切换到的运行级的运行级代号,如:用 init 0 命令关机;用 init 6 命令重新启动。\n\n```bash\nls /etc/rc*\n```\n\n对于以 K 开头的文件,系统将终止对应的服务;\n对于以 S 开头的文件,系统将启动对应的服务;\n\n### Linux 系统主要启动步骤\n\n1、读取 MBR 的信息,启动 Boot Manager\n\nWindows 使用 NTLDR 作为 Boot Manager,如果您的系统中安装多个版本的 Windows,您就需要在 NTLDR 中选择您要进入的系统。Linux 通常使用功能强大,配置灵活的 GRUB 作为 Boot Manager。\n\n2、加载系统内核,启动 init 进程\n\ninit 进程是 Linux 的根进程,所有的系统进程都是它的子进程。\n\n3、init 进程读取 `/etc/inittab` 文件中的信息,并进入预设的运行级别,按顺序运行该运行级别对应文件夹下的脚本。脚本通常以 start 参数启动,并指向一个系统中的程序。通常情况下,`/etc/rcS.d/` 目录下的启动脚本首先被执行,然后是 `/etc/rcN.d/` 目录。例如您设定的运行级别为 `3`,那么它对应的启动目录为 `/etc/rc3.d/`。\n\n4、根据 `/etc/rcS.d/` 文件夹中对应的脚本启动 Xwindow 服务器 xorg Xwindow 为 Linux 下的图形用户界面系统。\n\n5、启动登录管理器,等待用户登录 Ubuntu 系统默认使用 GDM 作为登录管理器,您在登录管理器界面中输入用户名和密码后,便可以登录系统。\n\n### 常见的系统服务\n\n```bash\nacpi-support 高级电源管理支持\nacpid acpi 守护程序。这两个用于电源管理,非常重要\nalsa 声音子系统\nalsa-utils\nanacron cron 的子系统,将系统关闭期间的计划任务,在下一次系统运行时执行\napmd acpi 的扩展\natd 类似于 cron 的任务调度系统。建议关闭\nbinfmt-support 核心支持其他二进制的文件格式。建议开启\nbluez-utiles 蓝牙设备支持\nbootlogd 启动日志。开启它\ncron 任务调度系统,建议开启\ncupsys 打印机子系统\ndbus 消息总线系统(message bus system)。非常重要\ndns-clean 使用拨号连接时,清除 dns 信息\nevms 企业卷管理系统(Enterprise Volumn Management system)\nfetchmail 邮件用户代理守护进程,用于收取邮件\ngdm gnome 登录和桌面管理器。\ngdomap\ngpm 终端中的鼠标支持\nhalt 别动它\nhdparm 调整硬盘的脚本,配置文件为 /etc/hdparm.conf\nhibernate 系统休眠\nhotkey-setup 笔记本功能键支持。支持类型包括: HP, Acer, ASUS, Sony,Dell, 和 IBM\nhotplug and hotplug-net 即插即用支持,比较复杂,建议不要动它\nhplip HP 打印机和图形子系统\nifrename 网络接口重命名脚本。如果您有十块网卡,您应该开启它\ninetd 在文件 /etc/inetd.conf 中,注释掉所有你不需要的服务。如果该文件不包含任何服务,那关闭它是很安全的\nklogd 重要\nlinux-restricted-modules-common 受限模块支持\n/lib/linux-restricted-modules/ 文件夹中的模块为受限模块。例如某些驱动程序,如果您没有使用受限模块,就不需要开启它\nlvm 逻辑卷管理系统支持\nmakedev 创建设备文件,非常重要\nmdamd 磁盘阵列\nmodule-init-tools 从/etc/modules 加载扩展模块,建议开启\nnetworking 网络支持。按 /etc/network/interfaces 文件预设激活网络,非常重要\nntpdate 时间同步服务,建议关闭\npcmcia pcmcia 设备支持\npowernowd 移动 CPU 节能支持\nppp and ppp-dns 拨号连接\nreadahead 预加载库文件\nreboot 别动它\nresolvconf 自动配置 DNS\nrmnologin 清除 nologin\nrsync rsync 守护程序\nsendsigs 在重启和关机期间发送信号\nsingle 激活单用户模式\nssh ssh 守护程序。建议开启\nstop-bootlogd 在 2,3,4,5 运行级别中停止 bootlogd 服务\nsudo 检查 sudo 状态。重要\nsysklogd 系统日志\nudev & udev-mab 用户空间 dev 文件系统(userspace dev filesystem)。重要\numountfs 卸载文件系统\nurandom 随机数生成器\nusplash 开机画面支持\nvbesave 显卡 BIOS 配置工具。保存显卡的状态\nxorg-common 设置 X 服务 ICE socket。\nadjtimex 调整核心时钟的工具\ndirmngr 证书列表管理工具,和 gnupg 一起工作。\nhwtools irqs 优化工具\nlibpam-devperm 系统崩溃之后,用于修理设备文件许可的守护程序。\nlm-sensors 板载传感器支持\nmdadm-raid 磁盘陈列管理器\nscreen-cleanup 清除开机屏幕的脚本\nxinetd 管理其他守护进程的一个 inetd 超级守护程序\n```\n\n## References\n\n- [Ubuntu 下使用 sysv-rc-conf 管理服务 » 冰河的博客](https://www.binghe.org/2009/12/manage-services-using-sysv-rc-conf-in-ubuntu/)\n- [Ubuntu 下使用 sysv-rc-conf 管理服务 - AderStep](http://blog.csdn.net/gatieme/article/details/45251389)\n- [启动 - Ubuntu 中文](http://wiki.ubuntu.org.cn/%E5%90%AF%E5%8A%A8)\n","tags":["ubuntu"]},{"title":"Eclipse Java 注释模板","url":"/2016/09/18/eclipse-java-comments-templates/","content":"\n自己总结的比较规范的 Eclipse Java 注释模板\n\n## Eclipse Java 注释模板设置\n\nWindow -> Preference -> Java -> CodeStyle -> Code Template 然后展开 Comments 节点就是所有需设置注释的元素\n\n<!-- more -->\n\n## 各项注释模板\n\n### Files\n\n```\n/**\n * Copyright © ${year}. All rights reserved.\n *\n * @Title: ${file_name}\n * @Prject: ${project_name}\n * @Package: ${package_name}\n * @Description: ${todo}\n * @author: ${user}\n * @date: ${date} ${time}\n */\n```\n\n### Types\n\n```\n/**\n *\n *\n * @ClassName: ${type_name}\n * @Description: ${todo}\n * @author: ${user}\n * @date: ${date} ${time}\n * ${tags}\n */\n```\n\n### Fields\n\n```\n/**\n * @fieldName: ${field}\n * @fieldType: ${field_type}\n * @Description: ${todo}\n */\n```\n\n### Constructors\n\n```\n/**\n * @Title:${enclosing_type}\n * @Description:${todo}\n * ${tags}\n */\n```\n\n### Methods\n\n```\n/**\n *\n *\n * @Title: ${enclosing_method}\n * @Description: ${todo}\n * @author: ${user}\n * ${tags}\n * @return: ${return_type}\n */\n```\n\n### Overriding methods\n\n```\n/* (no Javadoc)\n * <p>Title: ${enclosing_method}</p>\n * <p>Description: </p>\n * ${tags}\n * ${see_to_overridden}\n */\n```\n\n## 定制 \\${user} 显示内容\n\n找到你的 Eclipse 安装路径,打开 `eclipse.ini` 文件,在 -vmargs 下面添加\n\n```\n-Duser.name=姓名 + 空格 + 邮箱地址\n```\n","tags":["java"]},{"title":"MySQL LIMIT 查询优化","url":"/2016/09/18/mysql-limit-query-optimization/","content":"\n最近常在 SQL 中使用到 `LIMIT ? ?`,在执行 `LIMIT 0, 1000` 与 `LIMIT 100000, 1000` 时,查询速度明显有很大的区别,而且随着 LIMIT 的偏移量的增加,查询速度越来越慢。是否有办法对 SQL 中 LIMIT 查询进行优化呢?\n\n<!-- more -->\n\n## LIMIT 速度慢的原因\n\nLIMIT 100000, 1000 的意思扫描满足条件的 101000 行,扔掉前面的 100000 行,返回最后的 1000 行,问题就在这里。\n\n## LIMIT 优化思路\n\n1、尽可能从索引中直接获取数据,避免或减少直接扫描行数据的频率\n2、尽可能减少扫描的记录数,也就是先确定起始的范围,再往后取 N 条记录即可\n\n## LIMIT 优化示例\n\n### 原始 SQL\n\n```sql\n-- 含条件\nSELECT * FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 100, 10;\n-- 不含 WHERE\nSELECT * FROM `t1` ORDER BY id DESC LIMIT 100, 10;\n```\n\n### 子查询优化 SQL\n\n```sql\n-- 采用子查询的方式优化,在子查询里先从索引获取到最大 id,然后倒序排,再取 10 行结果集\n-- 注意这里采用了 2 次倒序排,因此在取 LIMIT 的 start 值时,比原来的值加了 10,即 935510,否则结果将和原来的不一致\nSELECT * FROM\n (SELECT * FROM `t1` WHERE id >\n ( SELECT id FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 935510, 1)\n LIMIT 10) T\nORDER BY id DESC;\n```\n\n### INNER 优化 SQL\n\n```sql\n-- 采用 INNER JOIN 优化,JOIN 子句里也优先从索引获取 ID 列表,然后直接关联查询获得最终结果,这里不需要加 10\nSELECT * FROM `t1`\n INNER JOIN ( SELECT id FROM `t1`\n WHERE ftype=1 ORDER BY id DESC LIMIT 935500,10) t2\n USING (id);\n```\n\n1、要学着使用 `EXPLAIN` 对 SQL 进行优化调整\n2、推荐使用 INNER 优化 SQL\n3、发现了一个 ubuntu 中监控 MySQL 的 `mytop` 命令,刚刚安装还没细研究\n\n```bash\nsudo apt-get install mytop\n```\n\n4、`USING (id)` 如果两个表的字段名都一样,那么可以用 using(字段名) 来协商条件,效果跟 `on a.id = b.id` 一样\n\n## References\n\n- [MySQL 优化案例系列 — 分页优化 | iMySQL](http://imysql.com/2014/07/26/mysql-optimization-case-paging-optimize.shtml)\n- [MYSQL 分页 limit 速度太慢优化方法 | Fienda blog](http://www.fienda.com/archives/110)\n\n-- EOF --\n","tags":["mysql"]},{"title":"Eclipse Maven 配置使用国内镜像库","url":"/2016/09/14/eclipse-maven-settings-mirror-in-china/","content":"\nWindows 下因为 Eclipse 自带了 Maven 插件,还算够用就懒得安装 Maven 了。在不使用代理的情况下,用 Maven 的下载库不是一般的慢。Eclipse Maven 的插件怎么配置国内的镜像库呢?其实很简单。\n\n<!-- more -->\n\n## 创建 settings.xml 文件\n\n注意两个地方:\n1、`<localRepository>C:\\Users\\Lenovo\\.m2\\repository</localRepository>` 本地仓库位置\n2、镜像配置,选取的是 aliyun 的\n\n```xml\n<mirrors>\n <mirror>\n <id>alimaven</id>\n <name>aliyun maven</name>\n <url>http://maven.aliyun.com/nexus/content/groups/public/</url>\n <mirrorOf>central</mirrorOf>\n </mirror>\n</mirrors>\n```\n\n完整的 settings.xml 文件\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<!--\n | This is the configuration file for Maven. It can be specified at two levels:\n |\n | 1. User Level. This settings.xml file provides configuration for a single user,\n | and is normally provided in ${user.home}/.m2/settings.xml.\n |\n | NOTE: This location can be overridden with the CLI option:\n |\n | -s /path/to/user/settings.xml\n |\n | 2. Global Level. This settings.xml file provides configuration for all Maven\n | users on a machine (assuming they're all using the same Maven\n | installation). It's normally provided in\n | ${maven.home}/conf/settings.xml.\n |\n | NOTE: This location can be overridden with the CLI option:\n |\n | -gs /path/to/global/settings.xml\n | |\n |-->\n<settings xmlns=\"http://maven.apache.org/SETTINGS/1.0.0\"\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n xsi:schemaLocation=\"http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd\">\n\n <!-- Change in below line -->\n <localRepository>C:\\Users\\Lenovo\\.m2\\repository</localRepository>\n\n <interactiveMode>true</interactiveMode>\n\n <offline>false</offline>\n\n <pluginGroups>\n <!-- pluginGroup\n | Specifies a further group identifier to use for plugin lookup.\n <pluginGroup>com.your.plugins</pluginGroup>\n -->\n </pluginGroups>\n\n <proxies>\n <!--\n <proxy>\n <id>optional</id>\n <active>true</active>\n <protocol>http</protocol>\n <username>proxyuser</username>\n <password>proxypass</password>\n <host>proxy.host.net</host>\n <port>80</port>\n <nonProxyHosts>local.net|some.host.com</nonProxyHosts>\n </proxy>\n -->\n </proxies>\n\n <servers>\n <!--\n <server>\n <id>deploymentRepo</id>\n <username>crunchify</username>\n <password>crunchify</password>\n </server>\n \t-->\n </servers>\n\n <mirrors>\n <mirror>\n <id>alimaven</id>\n <name>aliyun maven</name>\n \t<url>http://maven.aliyun.com/nexus/content/groups/public/</url>\n\t <mirrorOf>central</mirrorOf>\n </mirror>\n </mirrors>\n\n <profiles>\n </profiles>\n\n</settings>\n```\n\n## 设置 Eclipse Maven Plugin\n\nWindow -> Preferences -> Maven -> User Settings -> User settings Browse 选择上面创建的 `setting.xml` -> ok\n","tags":["java"]},{"title":"LeetCode Database Trips and Users 262","url":"/2016/09/14/leetcode-database-trips-and-users-262/","content":"\n[262. Trips and Users](https://leetcode.com/problems/trips-and-users/)\n  The `Trips` table holds all taxi trips. Each trip has a unique Id, while Client_Id and Driver_Id are both foreign keys to the Users_Id at the `Users` table. Status is an ENUM type of (‘completed’, ‘cancelled_by_driver’, ‘cancelled_by_client’).\n\n```sql\n+----+-----------+-----------+---------+--------------------+----------+\n| Id | Client_Id | Driver_Id | City_Id | Status |Request_at|\n+----+-----------+-----------+---------+--------------------+----------+\n| 1 | 1 | 10 | 1 | completed |2013-10-01|\n| 2 | 2 | 11 | 1 | cancelled_by_driver|2013-10-01|\n| 3 | 3 | 12 | 6 | completed |2013-10-01|\n| 4 | 4 | 13 | 6 | cancelled_by_client|2013-10-01|\n| 5 | 1 | 10 | 1 | completed |2013-10-02|\n| 6 | 2 | 11 | 6 | completed |2013-10-02|\n| 7 | 3 | 12 | 6 | completed |2013-10-02|\n| 8 | 2 | 12 | 12 | completed |2013-10-03|\n| 9 | 3 | 10 | 12 | completed |2013-10-03|\n| 10 | 4 | 13 | 12 | cancelled_by_driver|2013-10-03|\n+----+-----------+-----------+---------+--------------------+----------+\n```\n\n  The `Users` table holds all users. Each user has an unique Users_Id, and Role is an ENUM type of (‘client’, ‘driver’, ‘partner’).\n\n```sql\n+----------+--------+--------+\n| Users_Id | Banned | Role |\n+----------+--------+--------+\n| 1 | No | client |\n| 2 | Yes | client |\n| 3 | No | client |\n| 4 | No | client |\n| 10 | No | driver |\n| 11 | No | driver |\n| 12 | No | driver |\n| 13 | No | driver |\n+----------+--------+--------+\n```\n\n  Write a SQL query to find the cancellation rate of requests made by unbanned clients between Oct 1, 2013 and Oct 3, 2013. For the above tables, your SQL query should return the following rows with the cancellation rate being rounded to two decimal places.\n\n```sql\n+------------+-------------------+\n| Day | Cancellation Rate |\n+------------+-------------------+\n| 2013-10-01 | 0.33 |\n| 2013-10-02 | 0.00 |\n| 2013-10-03 | 0.50 |\n+------------+-------------------+\n```\n\n<!-- more -->\n\n### 大体意思\n\n写一个 SQL 语句,查询非禁止客户(Users 表中 Banned 列为 No 的客户)在 2013-10-1 至 2013-10-3 间的单据取消率,结果为四舍五入后的两位有效数字。\n\n### 别人的解法\n\n```sql\nSELECT Request_at DAY,\n ROUND(SUM(IF(Status = 'completed', 0, 1)) / COUNT(*), 2) 'Cancellation Rate'\nFROM Trips t\n\tLEFT JOIN Users t1 ON t.Client_Id = t1.Users_Id\nWHERE t1.Banned = 'No' AND Request_at BETWEEN '2013-10-01' AND '2013-10-03'\nGROUP BY t.Re\tquest_at;\n```\n\n这道题主要是统计计算\n\n笔记:\n1、函数:ROUND(column_name,decimals)\n`column_name` 必需。要舍入的字段\n`decimals` 必需。规定要返回的小数位数\n\n2、`SUM(IF(Status = 'completed', 0, 1))`\n统计 SUM 这里也好巧妙\n\n3、`having` 与 `where` 的区别\nhaving 字句可以让我们筛选成组后的各种数据,where 字句在聚合前先筛选记录,也就是说作用在 group by 和 having 字句前。而 having 子句在聚合后对组记录进行筛选。\n\n```sql\nSELECT region, SUM(population), SUM(area)\nFROM bbc\nGROUP BY region\nHAVING SUM(area) > 1000000\n```\n\n在这里,我们不能用 where 来筛选超过 1000000 的地区,因为表中不存在这样一条记录。相反,having 子句可以让我们筛选成组后的各组数据\n\n> Reference:\n> [LeetCode:Trips and Users - 出租车接单取消率 - Tsybius2014](https://my.oschina.net/Tsybius2014/blog/496047) > [mysql 中的 where 和 having 子句的区别 - Hukin](http://www.blogjava.net/Johnny-Ajun/archive/2011/08/28/357445.html)\n","tags":["leetcode"]},{"title":"LeetCode Database Department Highest Salary 184","url":"/2016/09/12/leetcode-database-department-highest-salary-184/","content":"\n[184. Department Highest Salary](https://leetcode.com/problems/department-highest-salary/)\n  The `Employee` table holds all employees. Every employee has an Id, a salary, and there is also a column for the department Id.\n\n```sql\n+----+-------+--------+--------------+\n| Id | Name | Salary | DepartmentId |\n+----+-------+--------+--------------+\n| 1 | Joe | 70000 | 1 |\n| 2 | Henry | 80000 | 2 |\n| 3 | Sam | 60000 | 2 |\n| 4 | Max | 90000 | 1 |\n+----+-------+--------+--------------+\n```\n\n  The `Department` table holds all departments of the company.\n\n```sql\n+----+----------+\n| Id | Name |\n+----+----------+\n| 1 | IT |\n| 2 | Sales |\n+----+----------+\n```\n\n  Write a SQL query to find employees who have the highest salary in each of the departments. For the above tables, Max has the highest salary in the IT department and Henry has the highest salary in the Sales department.\n\n```sql\n+------------+----------+--------+\n| Department | Employee | Salary |\n+------------+----------+--------+\n| IT | Max | 90000 |\n| Sales | Henry | 80000 |\n+------------+----------+--------+\n```\n\n<!-- more -->\n\n### 大体意思\n\n查询出每一个部门中收入最后的员工的信息\n\n### 自己的解法\n\n一开始时自己没有考虑收入最高可能有并列的情况,就直接 MAX(),加 join in 了;同时也要考虑到:所属部门不存在的情况;修改后 SQL 为\n\n```sql\nSELECT\n d.NAME Department,\n e.NAME Employee,\n e.Salary\nFROM\n Employee e\n JOIN ( SELECT max( salary ) salary, departmentId FROM Employee GROUP BY departmentId ) m USING ( salary, departmentId )\n JOIN Department d ON e.departmentId = d.id\n```\n\n### 别人的解法\n\n基本都是连表查询的套路,但是有一段总结还是有些意思的。\n\n1、聚合函数 max() 的效率不如嵌套子查询\n2、in 与 exists 效率差不多,当时在网上查的是:\n\n> in 和 not in 也要慎用,否则会导致全表扫描\n> 很多时候用 exists 代替 in 是一个好的选择\n\n3、将 join on 代替了 where 判断,效率提升很多,后来有个看过 MYSQL 源码的大神说:\n\n> 在 MySQL 的 SELECT 查询当中,其核心算法就是 JOIN 查询算法。其他的查询语句都相应向 JOIN 靠拢:单表查询被当作 JOIN 的特例;子查询被尽量转换为 JOIN 查询\n\n4、将 join 替换为了 straight_join,还是源码大神说的:\n\n> 对于多表查询,如果可以确定表按照某一固定次序处理可以获得较好的效率,则建议加上 STRAIGHT_JOIN 子句,以减少优化器对表进行重排序优化的过程。\n> 该子句一方面可以用于优化器无法给出最优排列的 SQL 语句;另一方面同样适用于优化器可以给出最优排列的 SQL 语句,因为 MySQL 算出最优排列也需要耗费较长的流程。\n> 对于后一状况,可以根据 EXPLAIN 的提示选定表的顺序,并加上 STRAIGHT_JOIN 子句固定该顺序。该状况下的使用前提是几个表之间的数据量比例会一直保持在某一顺序,否则在各表数据此消彼长之后会适得其反。\n\n对于经常调用的 SQL 语句,这一方法效果较好;同时操作的表越多,效果越好。\n\n> Reference:\n> [leetcode-184-Department Highest Salary 优化记录 - M-zyh](http://www.cnblogs.com/zhangyunhao/p/4896055.html)\n","tags":["leetcode"]},{"title":"LeetCode Database Consecutive Numbers 180","url":"/2016/09/11/leetcode-database-consecutive-numbers-180/","content":"\n[180. Consecutive Numbers](https://leetcode.com/problems/consecutive-numbers/)\n  Write a SQL query to find all numbers that appear at least three times consecutively.\n\n```sql\n+----+-----+\n| Id | Num |\n+----+-----+\n| 1 | 1 |\n| 2 | 1 |\n| 3 | 1 |\n| 4 | 2 |\n| 5 | 1 |\n| 6 | 2 |\n| 7 | 2 |\n+----+-----+\n```\n\nFor example, given the above `Logs` table, `1` is the only number that appears consecutively for at least three times.\n\n<!-- more -->\n\n### 大体意思\n\n写 SQL 查询出连续出现至少 3 次的 Num\n\n### 自己的解法\n\n```sql\nSELECT DISTINCT L1.Num AS ConsecutiveNums\nFROM Logs L1\n\tJOIN Logs L2 ON L1.Id + 1 = L2.Id\n\tJOIN Logs L3 ON L1.Id + 2 = L3.Id\nWHERE L1.Num = L2.Num AND L1.Num = L3.Num\n```\n\n很传统连表查询,但是有坑的地方,就是依靠的时 Id,所以局限是 Id 要连贯\n\n### 别人的解法\n\n首先,增加一个 rank 字段,记录序号,初始值为 1,当后一个值与前一个值相等时,序号加 1。之后,把所有 rank 值大于等于 3 的都检索出来,再去重即可。\n\n```sql\nSELECT DISTINCT(Num) AS ConsecutiveNums FROM\n(SELECT Id,Num,\n@Rank:=IF(@prevNum != Num,1,@Rank+1) AS Rank,\n@prevNum:=Num\nFROM Logs) t,\n(SELECT @Rank:=0,@prevNum:=NULL) r\nWHERE t.Rank >= 3;\n```\n\n### 自己的理解\n\n```\n(SELECT @Rank:=0,@prevNum:=NULL) r\n```\n\n是个巧妙的地方,对变量进行了初始化\n\n> Reference:\n> [【leetcode Database】180. Consecutive Numbers - Kevin_zhai 的博客](http://blog.csdn.net/Kevin_zhai/article/details/52152289)\n","tags":["leetcode"]},{"title":"LeetCode Database Second Highest Salary 176","url":"/2016/09/11/leetcode-database-second-highest-salary-176/","content":"\n[176. Second Highest Salary](https://leetcode.com/problems/second-highest-salary/)\n  Write a SQL query to get the second highest salary from the `Employee` table.\n\n```sql\n+----+--------+\n| Id | Salary |\n+----+--------+\n| 1 | 100 |\n| 2 | 200 |\n| 3 | 300 |\n+----+--------+\n```\n\nFor example, given the above Employee table, the second highest salary is `200`. If there is no second highest salary, then the query should return `null`.\n\n<!-- more -->\n\n### 大体意思\n\n写 SQL 查询出第二高薪水的 Id。如何没有第二高,则返回 `null`\n\n### 自己的解法\n\n```\nSELECT\n Salary\nFROM\n Employee\nORDER BY Salary DESC\nLIMIT 1,1\n```\n\n但是在没有第二高的时候将没有返回值,不符合题意;看了别人的,发现自己也少考虑 `DISTINCT`\n\n### 别人的解法\n\n多加一层 SELECT 并添加一个 IF 条件判断。如果结果有 0 行则返回 NULL,有 1 行返回正常结果。由于可以预期上一步结果只有一个,所以这里可以用 COUNT 而不用 GROUP BY。\n\n构造测试数据:\n\n```sql\nCREATE TABLE IF NOT EXISTS Employee (\n Id INT,\n Salary INT\n);\nDELETE FROM Employee;\nINSERT INTO Employee VALUES\n(1, 100),\n(2, 200),\n(3, 100),\n(4, 300);\n```\n\n预期结果:\n\n```sql\n+---------------------+\n| SecondHighestSalary |\n+---------------------+\n| 200 |\n+---------------------+\n```\n\n解法一:\n\n```sql\nSELECT\n IF(COUNT(Salary) >= 1, Salary, NULL) AS SecondHighestSalary\nFROM\n (SELECT DISTINCT\n Salary\n FROM\n Employee\n ORDER BY Salary DESC\n LIMIT 1 , 1) tmp\n```\n\n解法二:\n正常 Ranking 类问题解法,使用自定义变量计算排名。接着和上面一种解法类似需要对结果进行处理,没有第 2 名的返回 NULL:\n\n```sql\nSELECT\n IF(COUNT(Salary) >= 1, Salary, NULL) AS SecondHighestSalary\nFROM\n (SELECT DISTINCT\n Salary\n FROM\n (SELECT\n\t Id,\n Salary,\n @rank:=IF(@prevVal > Salary, @rank:=@rank + 1, @rank) AS Rank,\n @prevVal:=Salary\n\t FROM\n\t Employee, (SELECT @prevVal:=NULL) x, (SELECT @rank:=1) y\n\t ORDER BY Salary DESC) tmp\n WHERE\n tmp.Rank = 2) tmp2\n```\n\n解法三:\n上面两种解法都是可以扩展到任意排名的,如果想偏一点可以得到其他解法。排名第 2 可以看做是除了 MAX 之外的 MAX,可以得到这两种类似的解法。由于 MAX 函数可以返回 NULL 结果,就不用在进一步加工结果。\n\n```sql\nSELECT\n MAX(Salary)\nFROM\n Employee\nWHERE\n Salary < (SELECT\n MAX(Salary)\n FROM\n Employee)\n```\n\n```sql\nSELECT\n MAX(Salary)\nFROM\n Employee\nWHERE\n Salary NOT IN (SELECT\n MAX(Salary)\n FROM\n Employee)\n```\n\n> Reference:\n> [Leetcode Database: #176 Second Highest Salary | tsuinteru](http://tsuinte.ru/2015/04/05/leetcode-database-176-second-highest-salary/)\n","tags":["leetcode"]},{"title":"LeetCode Database Rising Temperature 197","url":"/2016/09/11/leetcode-database-rising-temperature-197/","content":"\n[197. Rising Temperature](https://leetcode.com/problems/rising-temperature/)\n  Given a `Weather` table, write a SQL query to find all dates' Ids with higher temperature compared to its previous (yesterday's) dates.\n\n```sql\n+---------+------------+------------------+\n| Id(INT) | Date(DATE) | Temperature(INT) |\n+---------+------------+------------------+\n| 1 | 2015-01-01 | 10 |\n| 2 | 2015-01-02 | 25 |\n| 3 | 2015-01-03 | 20 |\n| 4 | 2015-01-04 | 30 |\n+---------+------------+------------------+\n```\n\n<!-- more -->\n\nFor example, return the following Ids for the above Weather table:\n\n```sql\n+----+\n| Id |\n+----+\n| 2 |\n| 4 |\n+----+\n```\n\n---\n\n### 大体意思\n\n写 SQL 查询出温度比昨天大的日期 Id\n\n### 别人的解法\n\nThe `DATEDIFF()` function returns the time between two dates\n\n```sql\nSELECT w1.Id FROM Weather w1, Weather w2\nWHERE w1.Temperature > w2.Temperature AND (DATEDIFF(w1.Date, w2.Date) = 1 )\n```\n\n> Reference:\n> [LeetCode Q197 Rising Temperature | Lei Jiang Coding](https://leijiangcoding.wordpress.com/2015/05/01/leetcode-q197-rising-temperature/)\n","tags":["leetcode"]},{"title":"Mybatis Generator 使用配置","url":"/2016/09/10/mybatis-generator-tutorial/","content":"\nMyBatis Generator (MBG) 是一个 Mybatis 的代码生成器。MBG 可以内省数据库的表(或多个表)然后生成可以用来访问(多个)表的基础对象。 这样和数据库表进行交互时不需要创建对象和配置文件。MBG 的解决了对数据库操作有最大影响的一些简单的 CRUD(插入、查询、更新、删除)操作。\n\n## Mybatis Generator 文档\n\n- [Mybatis Generator 官方原版](http://www.mybatis.org/generator/index.html)\n- [Mybatis Generator 中文版](http://mbg.cndocs.tk/index.html)\n\n## 通过 Maven 运行 MBG\n\nMyBatis Generator (MBG) 包含了一个可以集成到 Maven 构建的 Maven 插件,按照 Maven 的配置惯例,将 MBG 集成到 Maven 很容易。\n\n<!-- more -->\n\n### `pom.xml` 配置\n\n```xml\n...\n<properties>\n ...\n <!-- 设置一些变量 -->\n <!-- plugin versions -->\n <plugin.mybatis.generator>1.3.5</plugin.mybatis.generator>\n <!-- plugin setting -->\n <mybatis.generator.generatorConfig.xml>${basedir}/src/test/resources/generatorConfig.xml</mybatis.generator.generatorConfig.xml>\n <mybatis.generator.generatorConfig.properties>file:///${basedir}/src/test/resources/generatorConfig.properties</mybatis.generator.generatorConfig.properties>\n</properties>\n...\n<build>\n ...\n <plugins>\n ...\n <!-- Mybatis generator代码生成插件 配置 -->\n <plugin>\n <groupId>org.mybatis.generator</groupId>\n <artifactId>mybatis-generator-maven-plugin</artifactId>\n <version>${plugin.mybatis.generator}</version>\n <configuration>\n <configurationFile>${mybatis.generator.generatorConfig.xml}</configurationFile>\n <overwrite>true</overwrite>\n <verbose>true</verbose>\n </configuration>\n </plugin>\n </plugins>\n</build>\n...\n```\n\n### `generatorConfig.xml` 核心配置\n\n非常完整的 MBG 核心配置文件,配合文档效果更佳。因为有大量的注释篇幅长,建议复制到 xml 文件中查看\ngeneratorConfig.xml 的文件位置要对应在 pom.xml 中的:\n\n```xml\n<mybatis.generator.generatorConfig.xml>${basedir}/src/test/resources/generatorConfig.xml</mybatis.generator.generatorConfig.xml>\n```\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE generatorConfiguration\n PUBLIC \"-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN\"\n\"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd\">\n<!-- 配置生成器 -->\n<generatorConfiguration>\n <!-- 可以用于加载配置项或者配置文件,在整个配置文件中就可以使用${propertyKey}的方式来引用配置项 -->\n <!-- resource:配置资源加载地址,使用resource,MBG从classpath开始找,比如com/myproject/generatorConfig.properties -->\n <!-- url:配置资源加载地质,使用URL的方式,比如file:///C:/myfolder/generatorConfig.properties.\n 注意,两个属性只能选址一个; -->\n <!-- 另外,如果使用了mybatis-generator-maven-plugin,那么在pom.xml中定义的properties都可以直接在generatorConfig.xml中使用 -->\n <!-- <properties resource=\"\" url=\"\" /> -->\n\n <!-- 配置文件路径 -->\n <properties url=\"${mybatis.generator.generatorConfig.properties}\" />\n\n <!-- 在MBG工作的时候,需要额外加载的依赖包 location属性指明加载jar/zip包的全路径 -->\n <!-- 数据库访问的驱动包 -->\n <classPathEntry location=\"${mbg.drive.class.path}\" />\n\n <!-- context:生成一组对象的环境 id:必选,上下文id,用于在生成错误时提示 -->\n <!-- defaultModelType:指定生成对象的样式 1,conditional:类似hierarchical; 2,flat:所有内容(主键,blob)等全部生成在一个对象中;\n 3,hierarchical:主键生成一个XXKey对象(key class),Blob等单独生成一个对象,其他简单属性在一个对象中(record\n class) -->\n <!--targetRuntime: 1,MyBatis3:默认的值,生成基于MyBatis3.x以上版本的内容,包括XXXBySample;\n 2,MyBatis3Simple:类似MyBatis3,只是不生成XXXBySample; introspectedColumnImpl:类全限定名,用于扩展MBG -->\n <context id=\"mysql_mbg\" defaultModelType=\"flat\" targetRuntime=\"MyBatis3Simple\">\n\n <!-- 自动识别数据库关键字,默认false,如果设置为true,根据SqlReservedWords中定义的关键字列表; 一般保留默认值,遇到数据库关键字(Java关键字),使用columnOverride覆盖 -->\n <property name=\"autoDelimitKeywords\" value=\"false\" />\n <!-- 生成的Java文件的编码 -->\n <property name=\"javaFileEncoding\" value=\"UTF-8\" />\n <!-- 格式化java代码 -->\n <property name=\"javaFormatter\"\n value=\"org.mybatis.generator.api.dom.DefaultJavaFormatter\" />\n <!-- 格式化XML代码 -->\n <property name=\"xmlFormatter\"\n value=\"org.mybatis.generator.api.dom.DefaultXmlFormatter\" />\n\n <!-- beginningDelimiter和endingDelimiter:指明数据库的用于标记数据库对象名的符号,比如ORACLE就是双引号,MYSQL默认是`反引号; -->\n <property name=\"beginningDelimiter\" value=\"`\" />\n <property name=\"endingDelimiter\" value=\"`\" />\n\n <!--关闭注释 -->\n <commentGenerator>\n <property name=\"suppressAllComments\" value=\"true\" />\n </commentGenerator>\n\n <!-- 必须要有的,使用这个配置链接数据库 @TODO:是否可以扩展 -->\n <jdbcConnection driverClass=\"com.mysql.jdbc.Driver\"\n connectionURL=\"${mbg.jdbc.url}\" userId=\"${mbg.jdbc.username}\"\n password=\"${mbg.jdbc.password}\">\n <!-- 这里面可以设置property属性,每一个property属性都设置到配置的Driver上 -->\n </jdbcConnection>\n\n <!-- java类型处理器 用于处理DB中的类型到Java中的类型,默认使用JavaTypeResolverDefaultImpl; 注意一点,默认会先尝试使用Integer,Long,Short等来对应DECIMAL和\n NUMERIC数据类型; -->\n <javaTypeResolver\n type=\"org.mybatis.generator.internal.types.JavaTypeResolverDefaultImpl\">\n <!-- true:使用BigDecimal对应DECIMAL和 NUMERIC数据类型 false:默认, scale>0;length>18:使用BigDecimal;\n scale=0;length[10,18]:使用Long; scale=0;length[5,9]:使用Integer; scale=0;length<5:使用Short; -->\n <property name=\"forceBigDecimals\" value=\"false\" />\n </javaTypeResolver>\n\n <!-- java模型创建器,是必须要的元素 负责:1,key类(见context的defaultModelType);2,java类;3,查询类 -->\n <!-- targetPackage:生成的类要放的包,真实的包受enableSubPackages属性控制; -->\n <!-- targetProject:目标项目,指定一个存在的目录下,生成的内容会放到指定目录中,如果目录不存在,MBG不会自动建目录 -->\n <javaModelGenerator targetPackage=\"${mbg.model.package}\"\n targetProject=\"src/test/java\">\n <!-- for MyBatis3/MyBatis3Simple 自动为每一个生成的类创建一个构造方法,构造方法包含了所有的field;而不是使用setter; -->\n <property name=\"constructorBased\" value=\"false\" />\n\n <!-- 在targetPackage的基础上,根据数据库的schema再生成一层package,最终生成的类放在这个package下,默认为false -->\n <property name=\"enableSubPackages\" value=\"false\" />\n\n <!-- for MyBatis3 / MyBatis3Simple 是否创建一个不可变的类,如果为true, 那么MBG会创建一个没有setter方法的类,取而代之的是类似constructorBased的类 -->\n <property name=\"immutable\" value=\"false\" />\n\n <!-- 设置一个根对象,如果设置了这个根对象,那么生成的keyClass或者recordClass会继承这个类;在Table的rootClass属性中可以覆盖该选项 -->\n <!-- 注意:如果在key class或者record class中有root class相同的属性,MBG就不会重新生成这些属性了, 包括:\n 1,属性名相同,类型相同,有相同的getter/setter方法; -->\n <!-- <property name=\"rootClass\" value=\"com.domain.BaseDomain\" /> -->\n\n <!-- 设置是否在getter方法中,对String类型字段调用trim()方法 -->\n <property name=\"trimStrings\" value=\"false\" />\n </javaModelGenerator>\n\n <!-- 生成SQL map的XML文件生成器,注意,在Mybatis3之后,我们可以使用mapper.xml文件+Mapper接口(或者不用mapper接口),\n 或者只使用Mapper接口+Annotation,所以,如果 javaClientGenerator配置中配置了需要生成XML的话,这个元素就必须配置\n targetPackage/targetProject:同javaModelGenerator -->\n <sqlMapGenerator targetPackage=\"${mbg.xml.mapper.package}\"\n targetProject=\"src/test/java\">\n <!-- 在targetPackage的基础上,根据数据库的schema再生成一层package,最终生成的类放在这个package下,默认为false -->\n <property name=\"enableSubPackages\" value=\"false\" />\n </sqlMapGenerator>\n\n <!-- 对于mybatis来说,即生成Mapper接口,注意,如果没有配置该元素,那么默认不会生成Mapper接口 targetPackage/targetProject:同javaModelGenerator -->\n <!-- type:选择怎么生成mapper接口(在MyBatis3/MyBatis3Simple下): -->\n <!-- 1,ANNOTATEDMAPPER:会生成使用Mapper接口+Annotation的方式创建(SQL生成在annotation中),不会生成对应的XML; -->\n <!-- 2,MIXEDMAPPER:使用混合配置,会生成Mapper接口,并适当添加合适的Annotation,但是XML会生成在XML中; -->\n <!-- 3,XMLMAPPER:会生成Mapper接口,接口完全依赖XML; -->\n <!-- 注意,如果context是MyBatis3Simple:只支持ANNOTATEDMAPPER和XMLMAPPER -->\n <javaClientGenerator targetPackage=\"${mbg.xml.mapper.package}\"\n type=\"XMLMAPPER\" targetProject=\"src/test/java\">\n <!-- 在targetPackage的基础上,根据数据库的schema再生成一层package,最终生成的类放在这个package下,默认为false -->\n <property name=\"enableSubPackages\" value=\"false\" />\n <!-- 可以为所有生成的接口添加一个父接口,但是MBG只负责生成,不负责检查 -->\n <!-- <property name=\"rootInterface\" value=\"\"/> -->\n </javaClientGenerator>\n\n <!-- 选择一个table来生成相关文件,可以有一个或多个table,必须要有table元素 选择的table会生成一下文件: -->\n <!-- 1,SQL map文件 2,生成一个主键类; 3,除了BLOB和主键的其他字段的类; 4,包含BLOB的类; 5,一个用户生成动态查询的条件类(selectByExample,\n deleteByExample),可选; 6,Mapper接口(可选) -->\n <!-- 必要: -->\n <!-- tableName :要生成对象的表名; 注意:大小写敏感问题。正常情况下,MBG会自动的去识别数据库标识符的大小写敏感度,在一般情况下,MBG会\n 根据设置的schema,catalog或tablename去查询数据表,按照下面的流程: 1,如果schema,catalog或tablename中有空格,那么设置的是什么格式,就精确的使用指定的大小写格式去查询;\n 2,否则,如果数据库的标识符使用大写的,那么MBG自动把表名变成大写再查找; 3,否则,如果数据库的标识符使用小写的,那么MBG自动把表名变成小写再查找;\n 4,否则,使用指定的大小写格式查询; 另外的,如果在创建表的时候,使用的\"\"把数据库对象规定大小写,就算数据库标识符是使用的大写,在这种情况下也会使用给定的大小写来创建表名;\n 这个时候,请设置delimitIdentifiers=\"true\"即可保留大小写格式; -->\n <!-- 可选: -->\n <!-- 1,schema:数据库的schema; -->\n <!-- 2,catalog:数据库的catalog; -->\n <!-- 3,alias:为数据表设置的别名,如果设置了alias,那么生成的所有的SELECT SQL语句中,列名会变成:alias_actualColumnName -->\n <!-- 4,domainObjectName:生成的domain类的名字,如果不设置,直接使用表名作为domain类的名字;可以设置为somepck.domainName,那么会自动把domainName类再放到somepck包里面; -->\n <!-- 5,enableInsert(默认true):指定是否生成insert语句; 6,enableSelectByPrimaryKey(默认true):指定是否生成按照主键查询对象的语句(就是getById或get); -->\n <!-- 7,enableSelectByExample(默认true):MyBatis3Simple为false,指定是否生成动态查询语句; -->\n <!-- 8,enableUpdateByPrimaryKey(默认true):指定是否生成按照主键修改对象的语句(即update); -->\n <!-- 9,enableDeleteByPrimaryKey(默认true):指定是否生成按照主键删除对象的语句(即delete); -->\n <!-- 10,enableDeleteByExample(默认true):MyBatis3Simple为false,指定是否生成动态删除语句; -->\n <!-- 11,enableCountByExample(默认true):MyBatis3Simple为false,指定是否生成动态查询总条数语句(用于分页的总条数查询); -->\n <!-- 12,enableUpdateByExample(默认true):MyBatis3Simple为false,指定是否生成动态修改语句(只修改对象中不为空的属性); -->\n <!-- 13,modelType:参考context元素的defaultModelType,相当于覆盖; -->\n <!-- 14,delimitIdentifiers:参考tableName的解释,注意,默认的delimitIdentifiers是双引号,如果类似MYSQL这样的数据库,使用的是`(反引号,那么还需要设置context的beginningDelimiter和endingDelimiter属性) -->\n <!-- 15,delimitAllColumns:设置是否所有生成的SQL中的列名都使用标识符引起来。默认为false,delimitIdentifiers参考context的属性\n 注意,table里面很多参数都是对javaModelGenerator,context等元素的默认属性的一个复写; -->\n <table tableName=\"test_handbook\">\n\n <!-- 参考 javaModelGenerator 的 constructorBased属性 -->\n <!-- <property name=\"constructorBased\" value=\"false\" /> -->\n <!-- 默认为false,如果设置为true,在生成的SQL中,table名字不会加上catalog或schema; -->\n <!-- <property name=\"ignoreQualifiersAtRuntime\" value=\"false\" /> -->\n <!-- 参考 javaModelGenerator 的 immutable 属性 -->\n <!-- <property name=\"immutable\" value=\"false\" /> -->\n <!-- 指定是否只生成domain类,如果设置为true,只生成domain类,如果还配置了sqlMapGenerator,那么在mapper\n XML文件中,只生成resultMap元素 -->\n <!-- <property name=\"modelOnly\" value=\"false\" /> -->\n <!-- 参考 javaModelGenerator 的 rootClass 属性 <property name=\"rootClass\" value=\"\"/> -->\n <!-- 参考javaClientGenerator 的 rootInterface 属性 <property name=\"rootInterface\"\n value=\"\"/> -->\n <!-- 如果设置了runtimeCatalog,那么在生成的SQL中,使用该指定的catalog,而不是table元素上的catalog\n <property name=\"runtimeCatalog\" value=\"\"/> -->\n <!-- 如果设置了runtimeSchema,那么在生成的SQL中,使用该指定的schema,而不是table元素上的schema <property\n name=\"runtimeSchema\" value=\"\"/> -->\n <!-- 如果设置了runtimeTableName,那么在生成的SQL中,使用该指定的tablename,而不是table元素上的tablename\n <property name=\"runtimeTableName\" value=\"\"/> -->\n <!-- 注意,该属性只针对MyBatis3Simple有用; 如果选择的runtime是MyBatis3Simple,那么会生成一个SelectAll方法,如果指定了selectAllOrderByClause,那么会在该SQL中添加指定的这个order条件; -->\n <!-- <property name=\"selectAllOrderByClause\" value=\"age desc,username\n asc\" /> -->\n <!-- 如果设置为true,生成的model类会直接使用column本身的名字,而不会再使用驼峰命名方法,比如BORN_DATE,生成的属性名字就是BORN_DATE,而不会是bornDate -->\n <property name=\"useActualColumnNames\" value=\"true\" />\n\n <!-- generatedKey用于生成生成主键的方法 -->\n <!-- 如果设置了该元素,MBG会在生成的<insert>元素中生成一条正确的<selectKey>元素, -->\n <!-- 该元素可选 column:主键的列名; sqlStatement:要生成的selectKey语句, -->\n <!-- 有以下可选项: -->\n <!-- Cloudscape:相当于selectKey的SQL为: VALUES IDENTITY_VAL_LOCAL() -->\n <!-- DB2 :相当于selectKey的SQL为: VALUES IDENTITY_VAL_LOCAL() -->\n <!-- DB2_MF :相当于selectKey的SQL为:SELECT IDENTITY_VAL_LOCAL() FROM SYSIBM.SYSDUMMY1 -->\n <!-- Derby :相当于selectKey的SQL为:VALUES IDENTITY_VAL_LOCAL() -->\n <!-- HSQLDB :相当于selectKey的SQL为:CALL IDENTITY() -->\n <!-- Informix :相当于selectKey的SQL为:select dbinfo('sqlca.sqlerrd1') from\n systables where tabid=1 -->\n <!-- MySql :相当于selectKey的SQL为:SELECT LAST_INSERT_ID() -->\n <!-- SqlServer :相当于selectKey的SQL为:SELECT SCOPE_IDENTITY() -->\n <!-- SYBASE :相当于selectKey的SQL为:SELECT @@IDENTITY -->\n <!-- JDBC :相当于在生成的insert元素上添加useGeneratedKeys=\"true\"和keyProperty属性 -->\n <!-- <generatedKey column=\"\" sqlStatement=\"\"/> -->\n\n <!-- 该元素会在根据表中列名计算对象属性名之前先重命名列名,非常适合用于表中的列都有公用的前缀字符串的时候, -->\n <!-- 比如列名为:CUST_ID,CUST_NAME,CUST_EMAIL,CUST_ADDRESS等; -->\n <!-- 那么就可以设置searchString为\"^CUST_\",并使用空白替换,那么生成的Customer对象中的属性名称就不是 custId,custName等,而是先被替换为ID,NAME,EMAIL,然后变成属性:id,name,email; -->\n <!-- 注意,MBG是使用java.util.regex.Matcher.replaceAll来替换searchString和replaceString的,\n 如果使用了columnOverride元素,该属性无效; -->\n <!-- <columnRenamingRule searchString=\"\" replaceString=\"\"/> -->\n\n <!-- 用来修改表中某个列的属性,MBG会使用修改后的列来生成domain的属性; column:要重新设置的列名; 注意,一个table元素中可以有多个columnOverride元素 -->\n <!-- <columnOverride column=\"username\"> -->\n <!-- 使用property属性来指定列要生成的属性名称 -->\n <!-- <property name=\"property\" value=\"userName\" /> -->\n <!-- javaType用于指定生成的domain的属性类型,使用类型的全限定名 <property name=\"javaType\" value=\"\"/> -->\n <!-- jdbcType用于指定该列的JDBC类型 <property name=\"jdbcType\" value=\"\"/> -->\n <!-- typeHandler 用于指定该列使用到的TypeHandler,如果要指定,配置类型处理器的全限定名 注意,mybatis中,不会生成到mybatis-config.xml中的typeHandler\n 只会生成类似:where id = #{id,jdbcType=BIGINT,typeHandler=com._520it.mybatis.MyTypeHandler}的参数描述\n <property name=\"jdbcType\" value=\"\"/> -->\n <!-- 参考table元素的delimitAllColumns配置,默认为false <property name=\"delimitedColumnName\"\n value=\"\"/> -->\n <!-- </columnOverride> -->\n <!-- ignoreColumn设置一个MGB忽略的列,如果设置了改列,那么在生成的domain中,生成的SQL中,都不会有该列出现 column:指定要忽略的列的名字;\n delimitedColumnName:参考table元素的delimitAllColumns配置,默认为false 注意,一个table元素中可以有多个ignoreColumn元素\n <ignoreColumn column=\"deptId\" delimitedColumnName=\"\"/> -->\n </table>\n </context>\n</generatorConfiguration>\n```\n\n### 相关的 properties 文件 `generatorConfig.properties`\n\ngeneratorConfig.properties 要对应在 pom.xml 中的:\n\n```xml\n<mybatis.generator.generatorConfig.properties>file:///${basedir}/src/test/resources/generatorConfig.properties</mybatis.generator.generatorConfig.properties>\n```\n\n```xml\n# 数据库驱动 jar 路径\nmbg.drive.class.path=/home/yifan/.m2/repository/mysql/mysql-connector-java/5.1.39/mysql-connector-java-5.1.39.jar\n# 数据库连接参数\nmbg.jdbc.driver=com.mysql.jdbc.Driver\nmbg.jdbc.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8\nmbg.jdbc.username=root\nmbg.jdbc.password=root\n# 包路径配置\nmbg.model.package=com.test.demo.mybatis\nmbg.dao.package=com.test.demo.mybatis\nmbg.xml.mapper.package=com.test.demo.mybatis\n```\n\n### 生成代码 方法一:Eclipse Maven 运行\n\n如果是在 Eclipse 中,选择 pom.xml 文件,击右键先择:\nRun AS > Maven Build… > 在 Goals 框中输入:`mybatis-generator:generate`\n在 `Console` 中可以看 log\n\n### 生成代码 方法二:Shell 运行\n\n如果在命令行输入 Maven 命令即可,注意:一定是当前项目目录下运行该命令:\n\n```bash\nmvn mybatis-generator:generate\n```\n\n## Eclipse Plugin 运行 MGB\n\n详细见:[MyBatis Generator - Running MyBatis Generator with Eclipse](http://www.mybatis.org/generator/running/runningWithEclipse.html)\n\n1、如果使用这种方法,将不在依靠 Maven,pox.xml 中的配置将可以省去\n2、加载 properties 文件需变化,自己一直没搞清楚 properties 的路径应该怎么写,后来变量就直接写在 generatorConfig.xml 中了。要是有明白的小伙伴,可以在下面留言\n3、需要下载 plugin:Help > Eclipse Marketplace... > Search for \"MyBatis Generator\";这个也是个局限,就是用的人还需要下载 Eclipse plugin,所以推荐上面使用 Maven 的方法\n\n## References\n\n- [Mybatis Generator 官方原版](http://www.mybatis.org/generator/index.html)\n- [Mybatis Generator 中文版](http://mbg.cndocs.tk/index.html)\n- [Mybatis Generator 最完整配置详解 - 简书](http://www.jianshu.com/p/e09d2370b796)\n- [用 Maven 插件生成 Mybatis 代码 - 边城刀客的博客](http://my.oschina.net/lilw/blog/168304)\n","tags":["java"]},{"title":"SpringMVC 入门使用","url":"/2016/09/07/springmvc-getting-started-tutorial/","content":"\n本文主要参考了 [imooc-SpringMVC 起步](http://www.imooc.com/video/7237) 视频教程和 [SpringMVC 从入门到精通 系列 - HansonQ](http://www.imooc.com/article/3804) ,还有自己的一些总结。\n\n主要内容:MVC 简介、前端控制器模式、SpringMVC 基本概念、SpringMVC 配置、SpringMVC 中的注解、SpringMVC 数据绑定。\n\n<!-- more -->\n\n## MVC 简介\n\n1、MVC 是一种架构模式\n\n程序分层,分工合作,既相互独立,又协同工作,分为三层:模型层、视图层和控制层\n\n2、MVC 是一种思考方式\n\n- View:视图层,为用户提供 UI,重点关注数据的呈现,为用户提供界面\n- Model:模型层,业务数据的信息表示,关注支撑业务的信息构成,通常是多个业务实体的组合\n- Controller:控制层,调用业务逻辑产生合适的数据(Model),传递数据给视图用于呈现\n\nMVC 设计模式在 B/S 下的应用:\n\n![160907-springmvc-getting-started-tutorial-mvc](https://user-images.githubusercontent.com/9289792/80199601-1cfdee80-8654-11ea-8328-9d74c174b2bc.gif)\n\n①:浏览器发送请求到控制器(这里要知道控制器的作用)\n②:控制器不能处理请求必须交给模型层来处理接着去访问数据库\n③:模型层将处理好的结果返回给控制层\n④:控制层将逻辑视图响应给浏览器(浏览器显示的是渲染过的视图)\n\nMVC 本质:**MVC 的核心思想是业务数据抽取同业务数据呈现相分离;分离有利于程序简化,方便编程**\n\n## 前端控制器模式\n\n前端控制器模式(Front Controller Pattern)是用来提供一个集中的请求处理机制,所有的请求都将由一个单一的处理程序处理。该处理程序可以做认证/授权/记录日志,或者跟踪请求,然后把请求传给相应的处理程序。\n\n- 前端控制器(Front Controller)- 处理应用程序所有类型请求的单个处理程序,应用程序可以是基于 web 的应用程序,也可以是基于桌面的应用程序。\n- 调度器(Dispatcher) - 前端控制器可能使用一个调度器对象来调度请求到相应的具体处理程序。\n- 视图(View) - 视图是为请求而创建的对象。\n\n前端控制器的主要作用:\n\n- 指前端控制器将我们的请求分发给我们的控制器去生成业务数据\n- 将生成的业务数据分发给恰当的视图模版来生成最终的视图界面\n\n![160907-springmvc-getting-started-tutorial-front-controller](https://user-images.githubusercontent.com/9289792/80201864-63a11800-8657-11ea-91cb-2b2a2f55f6a6.jpg)\n\n## SpringMVC 基本概念\n\n![160907-springmvc-getting-started-tutorial-springmvc01](https://user-images.githubusercontent.com/9289792/80199605-1d968500-8654-11ea-8f05-57d4685934bb.jpg)\n\n对组件说明:\n\n1. DispatherServlet:前端控制器 用户请求到达前端控制器,相当于 MVC 中的 C,而 DispatherServlet 是整个流程的核心,它来调用其他组件来处理用户的请求,前端控制器的存在降低了其他组件之间的耦合度。\n2. HandlerMapping:处理器映射器 它的作用就好比去看电影要拿着电影票根据电影票上面的座位号找到座位其中座位就是 Handler,电影票以及上面的座位号就是 URL HandlerMapping 负责根据用户请求找到 Handler 即处理器,SpringMVC 提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。\n3. Handler:处理器 Handler 是后端控制器,在前端控制器的控制下后端控制器对具体的用户请求进行处理,Handler 涉及到具体的用户业务请求,所以一般情况下需要程序员根据业务需求开发。\n4. HandlerAdapter:处理器适配器 通过 HandlerAdapter 对处理器进行执行,这是适配器模式的应用,通过适配器可以对更多类型的处理器进行执行。播放的电影是 3D 的你看不清楚,因此电影院跟你说你要想看清电影就必须戴 3D 眼镜。也就是说 Handler 满足一定的要求才可以被执行。\n5. ViewResolver:视图解析器 ViewResolver 负责将处理结果生成 View 视图,ViewResolver 首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成 View 视图对象,最后对 View 进行渲染将处理结果通过页面展示给用户。\n\n![160907-springmvc-getting-started-tutorial-springmvc02](https://user-images.githubusercontent.com/9289792/80199609-1d968500-8654-11ea-997b-67a5862bfd25.jpg)\n\n工作原理解释说明:\n1、用户发送请求到 SpringMVC 框架提供的 DispatcherServlet 这个前端控制器(了解 struts2 的朋友也都知道其实 struts2 也有一个前端控制器 web.xml 中的 filter 标签就是)。\n2、前端控制器会去找处理器映射器(HandlerMapping),处理器映射器根据请求 url 找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给 DispatcherServlet 。\n3、根据处理器映射器返回的处理器,DispatcherServlet 会找“合适”的处理器适配器(HandlerAdapter)\n4、处理器适配器 HandlerAdpater 会去执行处理器(Handler 开发的时候会被叫成 Controller 也叫后端控制器在 struts2 中 action 也是一个后端控制器)执行之前会有转换器、数据绑定、校验器等等完成上面这些才会去正在执行 Handler\n5、后端控制器 Handler 执行完成之后返回一个 ModelAndView 对象 。\n6、处理器适配器 HandlerAdpater 会将这个 ModelAndView 返回前端控制器 DispatcherServlet。前端控制器会将 ModelAndView 对象交给视图解析器 ViewResolver。\n7、视图解析器 ViewResolver 解析 ModelAndView 对象之后返回逻辑视图。\n8、前端控制器 DispatcherServlet 对逻辑视图进行渲染(数据填充)之后返回真正的物理 View 并响应给浏览器。\n\n![160907-springmvc-getting-started-tutorial-springmvc03](https://user-images.githubusercontent.com/9289792/80199612-1e2f1b80-8654-11ea-9245-65a4470bc5f5.jpg)\n\n## SpringMVC 配置\n\n1、前端控制器需要在 web.xml 中配置\n\n```xml\n<!-- 配置前端控制器 -->\n<servlet>\n <servlet-name>web-dispatcher</servlet-name>\n <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>\n <!--加载前端控制器配置文件 上下文配置位置-->\n <init-param>\n <!-- 备注:\n contextConfigLocation:指定 SpringMVC 配置的加载位置,如果不指定则默认加载\n WEB-INF/[DispatcherServlet 的 Servlet 名字]-servlet.xml\n -->\n <param-name>contextConfigLocation</param-name>\n <param-value>classpath:spring/spring-*.xml</param-value>\n </init-param>\n <!-- 表示随WEB服务器启动 -->\n <load-on-startup>1</load-on-startup>\n</servlet>\n<servlet-mapping>\n<servlet-name>web-dispatcher</servlet-name>\n <!-- 备注:可以拦截三种请求\n 第一种:拦截固定后缀的url,比如设置为 *.do、*.action, 例如:/user/add.action 此方法最简单,不会导致静态资源(jpg,js,css)被拦截\n 第二种:拦截所有,设置为/,例如:/user/add /user/add.action此方法可以实现REST风格的url,\n 很多互联网类型的应用使用这种方式.但是此方法会导致静态文件(jpg,js,css)被拦截后不能正常显示.需要特殊处理\n 第三种:拦截所有,设置为/*,此设置方法错误,因为请求到Action,当action转到jsp时再次被拦截,提示不能根据jsp路径mapping成功\n -->\n <!-- 默认匹配所有的请求 -->\n <url-pattern>/</url-pattern>\n</servlet-mapping>\n```\n\n2、在 `spring/spring-web.xml` 配置视图解析器\n\n```xml\n<!-- 配置视图解析器 -->\n<!-- InternalResourceViewResolver:支持JSP视图解析 -->\n<!-- viewClass:JstlView 表示JSP模板页面需要使用JSTL标签库,所以classpath中必须包含jstl的相关jar包; -->\n<!-- prefix 和 suffix:查找视图页面的前缀和后缀,最终视图的址为: -->\n<!-- 前缀+逻辑视图名+后缀,逻辑视图名需要在controller中返回ModelAndView指定,比如逻辑视图名为hello,-->\n<!-- 则最终返回的jsp视图地址 \"WEB-INF/jsp/hello.jsp\" -->\n<bean class=\"org.springframework.web.servlet.view.InternalResourceViewResolver\">\n <!-- 决定视图类型,如果添加了jstl支持(即有jstl.jar),那么默认就是解析为jstl视图 -->\n <property name=\"viewClass\" value=\"org.springframework.web.servlet.view.JstlView\" />\n <!-- 视图前缀 -->\n <property name=\"prefix\" value=\"/WEB-INF/jsp/\" />\n <!-- 视图后缀 -->\n <property name=\"suffix\" value=\".jsp\" />\n</bean>\n```\n\n3、在 `spring/spring-web.xml` 配置 注解模式\n\n```xml\n<!-- 自动加载RequestMappingHandlerMapping和RequestMappingHandlerAdapter, -->\n<!-- 可用在xml配置文件中使用<mvc:annotation-driven>替代注解处理器和适配器的配置。 -->\n<mvc:annotation-driven/>\n```\n\n4、在 `spring/spring-web.xml` 配置 扫描 web 相关的 bean\n\n```xml\n<!-- 组件扫描器:可以扫描 @Controller、@Service、@Repository 等等 -->\n<context:component-scan base-package=\"com.controller\" />\n```\n\n## SpringMVC 中的注解\n\n### `@Controller`\n\n@Controller 注解,用于标识这个类是一个后端控制器(类似 struts 中的 action),主要作用就是接受页面的参数,转发页面。\n@Controller 源码:\n\n```java\n@Target({ElementType.TYPE}) // 表明只能定义在类上面\n@Retention(RetentionPolicy.RUNTIME) //保留策略是RUNTIME,在JVM加载类时,会把注解加载到JVM内存中(它是唯一可以用反射来读取注解的策略)\n@Documented //@Documented用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。Documented是一个标记注解,没有成员。\n@Component //spring框架规定当一个类不好归类(service、dao、controller)的时候可以使用这个注解,由此可见即便好归类内部还是使用的@Component注解\npublic @interface Controller {\n /**\n * The value may indicate a suggestion for a logical component name,\n * to be turned into a Spring bean in case of an autodetected component.\n * @return the suggested component name, if any\n */\n String value() default \"\";\n}\n```\n\n### `@RequestMapping`\n\n这个注解的作用目标就跟 @Controller 不一样了,这个注解可以定义在类上面也可以定义在方法上面。\n\n```java\n/**\n* 1.@RequestMapping:除了修饰方法,还可以修饰类\n* 2.类定义处:提供初步的请求信息映射.相对于WEB应用的根目录(窄化请求)\n* 3.方法处:提供进一步的细分映射信息。相对于类定义处的URL。\n* 若类定义处为标注@RequestMapping,则方法出的URL相对于WEB应用的根目录\n*/\n@Target({ElementType.METHOD, ElementType.TYPE})\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\n@Mapping\npublic @interface RequestMapping {\n String[] value() default {};\n RequestMethod[] method() default {}; //限制请求方式\n String[] params() default {}; //要求请求的URL包含指定的参数\n}\n```\n\n代码实例\n\n```java\n@Controller\n@RequestMapping(\"/demo\")\npublic class IndexController {\n @RequestMapping(value = \"/test\", method = RequestMethod.GET)\n public String index(Model model, HttpServletRequest request) {\n // 在游览器访问 http://localhost:8080/demo/test 将进入这里\n model.addAttribute(\"originURL\", \"\");\n model.addAttribute(\"controllerName\", \"index\");\n return \"index\";\n }\n}\n```\n\n@RequestMapping 还支持 Ant 方格的请求\n\n```txt\n?:匹配文件中的一个字符\n*:匹配文件中任意字符\n**:**匹配多层路径\n\n/user/*/createUser : 匹配 -/user/aa/createUser 或者 -/user/aa/createUser\n/user/**/createUser : 匹配 -/user/aa/createUser 或者 -/user/createUser 或者 -/user/aa/cc/createUser\n/user/createUser?? : 匹配 -/user/aa/createUseraa\n```\n\n### `@PathVariable`\n\n@PathVariable 这个注解支持现在当下较为流行的 Restful 风格的 URL。 先说说这个注解的作用,支持将 url 中的占位符参数绑定到目标方法的参数上, 该功能也是 SpringMVC 实现 Restful 风格 url 的重要措施。\n\n代码实例\n\n```java\n// http://localhost:8080/demo/sss\n@RequestMapping(value = \"/{slug:.+}\", method = RequestMethod.GET)\npublic String index2(@PathVariable(\"slug\") String slug, Model model) {\n LOG.info(\"DemoController index2 slug \" + slug);\n // common\n model.addAttribute(\"originURL\", \"demo/\");\n model.addAttribute(\"controllerName\", \"demo\");\n model.addAttribute(\"controllerMethod\", \"index2\");\n model.addAttribute(\"slug\", slug);\n return \"demo\";\n}\n\n//slug = sss\n```\n\n我们熟悉的请求应该是 POST 和 GET 请求,这两个请求也是最常用的而实际上 HTTP1.1 请求还有 PUT、DELETE 等 8 种来表名请求的动作。\n\n在 SpringMVC 中要实现 PUT 和 DELETE 请求需要在 web.xml 额外配置一个过滤器,这个过滤器的作用就是把 POST 请求变为 PUT 和 DELETE 请求。\n_关于 Restful 的内容计划单独写。_\n\n### `@RequestParam`\n\n```java\n@Target(ElementType.PARAMETER)\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\npublic @interface RequestParam {\n String value() default \"\";//值即为请求参数的参数名\n boolean required() default true;//该参数是否是必须。默认值为true\n String defaultValue() default ValueConstants.DEFAULT_NONE;//请求参数的默认值\n}\n```\n\n```java\n// http://localhost:8080/demo/para?slug=google\n@RequestMapping(value = \"/para\", method = RequestMethod.GET)\npublic String index3(@RequestParam(value = \"slug\", defaultValue = \"\") String slug, Model model) {\n model.addAttribute(\"originURL\", \"demo/\");\n model.addAttribute(\"controllerName\", \"demo\");\n model.addAttribute(\"controllerMethod\", \"index3\");\n model.addAttribute(\"slug\", slug);\n return \"demo\";\n}\nslug = google\n```\n\n另外还有一点要提示一下,参数没有加这个注解也能映射成功,这是应为 SpringMVC 框架支持请求参数和目标方法参数一致的时候可以省略这个注解。\n\n### `@ResponseBody`\n\n```java\n/**\n * Annotation that indicates a method return value should be bound to the web\n * response body. Supported for annotated handler methods in Servlet environments.\n *\n * 这个注解指明一个方法的返回值应该绑定在 web response body 中,在 Servlet 环境中支持注解处理方法\n *\n * <p>As of version 4.0 this annotation can also be added on the type level in\n * which case it is inherited and does not need to be added on the method level.\n */\n@Target({ElementType.TYPE, ElementType.METHOD})\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\npublic @interface ResponseBody {\n}\n```\n\n代码\n\n```java\n// http://localhost:8080/demo/json\n@RequestMapping(value = \"/json\", method = RequestMethod.POST)\npublic @ResponseBody Domain index7(HttpServletRequest request, Model model) {\n LOG.info(\"DemoController demo index7\");\n model.addAttribute(\"originURL\", \"demo/\");\n model.addAttribute(\"controllerName\", \"demo\");\n model.addAttribute(\"controllerMethod\", \"index7\");\n\n Domain domain = new Domain();\n domain.setDomain(\"gggoogle.com\");\n domain.setId(100);\n return domain;\n}\n\n/* response body\n{\n \"id\": 100,\n \"domain\": \"gggoogle.com\"\n}\n*/\n```\n\n## SpringMVC 数据绑定\n\n简单说一下场景:\n对于一个注册页面有很多信息譬如:用户名、密码、确认密码、邮箱、手机、兴趣等等。这时候就会想能不能将这些个参数包装在一个对象中(POJO),用这个 POJO 来做目标方法的形参上面。\n\n可以说的是 SpringMVC 是支持将 POJO 作为目标参数的。当然也是要遵循一些规则的,就是表单的 name 属性值要和 POJO 的属性值要一致。当然了,这样又会有一个新的疑问支不支持级联属性答案是支持的。\n\n```java\npublic class Address {\n private String city;\n ...\n}\n```\n\n```java\npublic class Persion {\n private String name;\n private Address address;\n ...\n}\n```\n\n```html\n<form action=\"/demo/pojo\">\n NAME:<input type=\"text\" name=\"name\" /> CITY:<input\n type=\"text\"\n name=\"address.city\"\n />\n</form>\n```\n\n```java\n@RequestMapping(value = \"/pojo\", method = RequestMethod.POST)\npublic String index4(Persion persion, Model model) {\n `model.addAttribute(\"originURL\", \"demo/\");\n model.addAttribute(\"controllerName\", \"demo\");\n model.addAttribute(\"controllerMethod\", \"index4\");\n model.addAttribute(\"persion\", persion);\n return \"demo\";`\n}\n```\n\n### SpringMVC 使用 Servlet API\n\n可以使用 Servlet 原生的 API 作为目标方法的参数。具体支持以下类型:HttpServletRequest、HttpServletResponse、HttpSession、java.security.Principal、Locale、InputStream、OutputStream、Reader、Writer\n\n```java\n// http://localhost:8080/demo/req?slug=facebook\n@RequestMapping(value = \"/req\", method = RequestMethod.GET)\npublic String index5(HttpServletRequest request, Model model) {\n String slug = request.getParameter(\"slug\");\n model.addAttribute(\"originURL\", \"demo/\");\n model.addAttribute(\"controllerName\", \"demo\");\n model.addAttribute(\"controllerMethod\", \"index5\");\n model.addAttribute(\"slug\", slug);\n return \"demo\";\n}\n```\n\n## References\n\n- [IMOOC-SpringMVC 起步](http://www.imooc.com/video/7237)\n- [SpringMVC 从入门到精通 系列 - HansonQ](http://www.imooc.com/article/3804)\n","tags":["java"]},{"title":"刚刚毕业的两个月小结","url":"/2016/08/31/2016-08-report/","content":"\n走出校园已经两个月了,因为之前的暑期也没怎么在家待过,大一在中康、大二在腾骏、大三在大为,大四毕业也就是现在,所以也没有什么特别的感觉。可以说,这两个月也做了些事情,学了些东西的。\n\n<!-- more -->\n\n从学校毕业,最直接的影响就是自己更加专注于计算机知识,不用再为学业担心。确定了先走技术的道路,也让自己不那么迷茫做什么。工作规律,自己开始读读书,才觉的读书是件有意思的事。也逼着自己常常写点东西,主要是觉的:写东西的时候自己会主动的思考,文笔练着练着也就能进步吧。\n\n(一)\n\nDMV 是毕业后自己第一个项目,全栈开发。初版两周上线,后有花了一周多的时间编写了答题记录的功能。其中的收获是:项目框架的搭建和前端入门知识。原来的项目搭建都不是自己做的,也一直觉的是一件很难的事情。也许就是因为没有做过才觉的难,做过之后也觉的不过如此。项目的页面不多,但都是自己一点点写的,HTML JS 原来也就是自己改改,没有完整写过,这次算是一次不错的锻炼。写前端的时候才发现现在的前端真的是日新月异 AngularJS React 好多好多自己都没有听过的东西,也是从 Mengqi 那里了解到了很多。可惜的是自己没有实际的用上这些,如有机会一定尝试。\n\nDMV 项目流量平平,因为有 Domain 类项目的工作,DMV 的维护就暂时放置了,还是心有不舍的。期间和 John 聊过一次关于产品的事基本总结为:\n\n- 产品项目是会有失败的,有流量的项目才有维护的意义。\n- 大型项目只有成功与失败之分,没有中间项,失败将代价很大。\n- 不一定要留住用户。\n- 更小的成本更高的流量,高流量后就可以做很多的事。\n\n(二)\n\nDomain 信息类项目,使用的架构和 DMV 一致,逻辑更加简单。自己主要做了数据整理,收获是关于 SQL 命令。SQL 就可以直接处理很多的事情。使用了 RMI 做数据同步,用反射写了一段程序,是一次有突破的尝试。\n一次看到自己年初的简历,笑了。自己是真敢写,能写个 Hello World 就敢标成了解。从会用到原理,是接下要走的路。\n\n(三)\n\n最近在看设计模式和算法的书,一直在做 LeetCode 的题。设计模式没用过,逮到能用机会绝不错过。算法健脑,刚刚开始接触觉的挺有意思,对数据结构也是种了解。\n\n(四)\n\n再次参加舍友的婚礼,又个结婚了。大一奶了一口,现在成真。祝福 Lu&Feifei。\n\n(五)\n\n> 来说说程序员那无处安放的创造力\n>\n> 有了锤子想找钉子是很正常的原始冲动,但我们必须认识到,创造力对于程序员这个职业来讲,是锦上添花的东西。如果你没有强大的工程能力,那么创造力也不过是无本之木。所以扎扎实实的把工程基础打好,这是最根本的。\n>\n> 在此基础上,我比较推荐程序员采用内外两条线来培养自己。在公司内的项目上采取相对保守的策略,尽力把稳定性做到最好,培养出自己卓越的工程能力;然后在公司外的开源项目和自己的独立项目上,采用一些新的技术、实践一些新的想法、充分发挥自己的创造力,梦想还是要有的,对吧。\n>\n> 这样做最明显的好处是,你可以了解到新技术和激进方案的优缺点,从而在进行方案选型时,有更多的依据;还有一个职业发展上的好处:如果不是主负责人,公司的项目往往不能代表你的能力;但独立项目却可以作为一个非常好的能力证明出现在你的简历里边。\n>\n> 你可以是一个身怀绝技的手艺人,在自己家里你尝试各种手法各种风格的个人作品;但当你参与颐和园这种级别的工程时,好好的把自己负责的石头雕成总设计师要求的样子就好 —— 毕竟这个时代一个人已经很难负责整个项目了。这就是我所理解的程序员的工匠精神。\n>\n> 摘自:[程序员到底是一个什么职业?](http://mp.weixin.qq.com/s?__biz=MzI5OTI5Njg2Mg==&mid=2247483667&idx=1&sn=d6e5953c7a7835148e3822b919b82416#rd)\n\n(尾)\n\n现在自己能听见进去一些原来听不去的话了。我不认为自己是被同化了,也许是心中少了些恶意。\n\n状态不错,继续前进。\n\n-- EOF --\n","tags":["thinking"]},{"title":"LeetCode Number of 1 Bits 191","url":"/2016/08/31/leetcode-number-of-1-bits-191/","content":"\n[191. Number of 1 Bits](https://leetcode.com/problems/number-of-1-bits/)\n  Write a function that takes an unsigned integer and returns the number of ’1' bits it has (also known as the Hamming weight).\n  For example, the 32-bit integer ’11' has binary representation `00000000000000000000000000001011`, so the function should return 3.\n\n<!-- more -->\n\n---\n\n### 大体意思\n\n写一个函数,输入一个无符号整数,返回其中值为 1 的比特位的个数(这个值也被称为数字汉明重量)\n\n### 自己的思路\n\n循环判断最后一位是否是 `1`\n\n```java\npublic int hammingWeight(int n) {\n\tint result = 0;\n\twhile (n != 0) {\n\t\tif ((n & 1) == 1) {\n\t\t\tresult++;\n\t\t}\n\t\tn = n >> 1;\n\t}\n\treturn result;\n}\n```\n\n在 Submit Solution\n\n```\nSubmission Result: Time Limit Exceeded\nLast executed input: 2147483648 (10000000000000000000000000000000)\n```\n\n### 别人的算法\n\n2147483648 输入超过了 Java 语言中整型的上限\n\n原来问题就出在输入可能是无符号数字上!当输入为 2147483648 时,Java 会将这个数字判断位 -1,而右移符号 >> 在计算时会保持数字的符号位,即正数右移高位补 0,负数右移高位补 1。使用这种规则进行右移,会导致数字在右移过程中被不断补 1,这样循环永远无法停止!因此,如果输入为负数,也应该保持右移时高位补 0,位运算符 >>> 可以帮助我们解决这个问题。\n\n```java\npublic int hammingWeight(int n) {\n\n\tint counter = 0;\n\twhile (n != 0)\n\t{\n\t if (n % 2 != 0) {\n\t\tcounter++;\n\t }\n\t n = n >>> 1;\n\t}\n\treturn counter;\n}\n```\n\n1. `>>` 是带符号右移,负数高位补 1,正数补 0\n2. `<<` 左移不管负数还是正数,在低位永远补 0\n3. `>>>` 是不带符号右移,不论负数还是正数,高位补 0\n\n> 引用:[LeetCode:Number of 1 Bits-整数的汉明重量-Tsybius2014](http://my.oschina.net/Tsybius2014/blog/491381)\n\n### 别人的算法 2\n\n- 资料:[Int 型数值存储](http://www.cnblogs.com/xinsheng/p/3419202.html)\n- 资料:[原码, 反码, 补码](http://www.cnblogs.com/zhangziqiu/archive/2011/03/30/ComputerCode.html)\n\n补码:正数为其本身,负数为取反加一\n\n```java\n// you need to treat n as an unsigned value\npublic int hammingWeight(int n) {\n\tlong l = Integer.toUnsignedLong(n);\n\tint count = 0;\n\tfor (; l != 0; l = l >> 1) {\n\t if ((l & 1) == 1) count ++;\n\t}\n\treturn count;\n}\n```\n\n所以在 Java 中需要将 `int` 转换为 `unsigned int` 的时候,可以将 `int` 转换到容量更大的 `long` 中就行了,直接 `((long) x) & 0xffffffffL` 或者使用函数 `Integer.toUnsignedLong()`。\n\n> 引用:[nekocode/leetcode-solutions-191. Number of 1 Bits](https://github.com/nekocode/leetcode-solutions/blob/master/solutions/191.%20Number%20of%201%20Bits.md)\n","tags":["leetcode"]},{"title":"LeetCode Happy Number 202","url":"/2016/08/29/leetcode-happy-number-202/","content":"\n[202. Happy Number](https://leetcode.com/problems/happy-number/)\n  Write an algorithm to determine if a number is \"happy\".\n  A happy number is a number defined by the following process: Starting with any positive integer, replace the number by the sum of the squares of its digits, and repeat the process until the number equals 1 (where it will stay), or it loops endlessly in a cycle which does not include 1. Those numbers for which this process ends in 1 are happy numbers.\n\n<!-- more -->\n\n  **Example**: 19 is a happy number\n\n```\n1^2 + 9^2 = 82\n8^2 + 2^2 = 68\n6^2 + 8^2 = 100\n1^2 + 0^2 + 0^2 = 1\n```\n\n---\n\n### 大体意思\n\n题目说的是取任意一个正整数,不断各个数位上数字的平方和,若最终收敛为 1,则该数字为`happy number`,否则程序可能从某个数开始陷入循环。\n\n### 自己的思路\n\n关键:通过 `/` 和 `%` 取 int 各个位的数字;用 `set` 判重复\n\n```java\npublic static boolean isHappy(int n) {\n Set<Integer> result = new HashSet<>();\n int newN = 0;\n do {\n do {\n int end = n % 10;\n n = n / 10;\n newN += Math.pow(end, 2);\n } while (n != 0);\n\n n = newN;\n newN = 0;\n if (result.contains(n)) {\n return false;\n } else {\n if (n == 1) {\n return true;\n } else {\n result.add(n);\n }\n }\n } while (true);\n}\n```\n\n套路:取各个位数字\n\n```java\ndo {\n int end = n % 10;\n n = n / 10;\n} while (n != 0);\n```\n","tags":["leetcode"]},{"title":"LeetCode Roman to Integer 13","url":"/2016/08/25/leetcode-roman-to-integer-13/","content":"\n[13. Roman to Integer](https://leetcode.com/problems/roman-to-integer/)\n  Given a roman numeral, convert it to an integer.\n  Input is guaranteed to be within the range from 1 to 3999.\n\n<!-- more -->\n\n> [wikipedia-罗马数字](https://zh.wikipedia.org/wiki/%E7%BD%97%E9%A9%AC%E6%95%B0%E5%AD%97)\n\n罗马数字共有 7 个,即 I(1)、V(5)、X(10)、L(50)、C(100)、D(500)和 M(1000)。按照下述的规则可以表示任意正整数。需要注意的是罗马数字中没有“0”,与进位制无关。一般认为罗马数字只用来记数,而不作演算。\n\n重复数次:一个罗马数字重复几次,就表示这个数的几倍。\n\n右加左减:\n\n- 在较大的罗马数字的右边记上较小的罗马数字,表示大数字加小数字。\n- 在较大的罗马数字的左边记上较小的罗马数字,表示大数字减小数字。\n- 左减的数字有限制,仅限于 I、X、C。比如 45 不可以写成 VL,只能是 XLV\n- 但是,左减时不可跨越一个位值。比如,99 不可以用 IC(100-1)表示,而是用 XCIX([100-10]+[10-1])表示。(等同于阿拉伯数字每位数字分别表示。)\n- 左减数字必须为一位,比如 8 写成 VIII,而非 IIX。\n- 右加数字不可连续超过三位,比如 14 写成 XIV,而非 XIIII。(见下方“数码限制”一项。)\n\n加线乘千:\n\n- 在罗马数字的上方加上一条横线或者加上下标的 Ⅿ,表示将这个数乘以 1000,即是原数的 1000 倍。\n- 同理,如果上方有两条横线,即是原数的 1000000(1000^{{2}})倍。\n\n数码限制:\n\n- 同一数码最多只能连续出现三次,如 40 不可表示为 XXXX,而要表示为 XL。\n- 例外:由于 IV 是古罗马神话主神朱庇特(即 IVPITER,古罗马字母里没有 J 和 U)的首字,因此有时用 IIII 代替 IV。\n\n### 自己的解法\n\n做一个数组对照的字典,遵守此条规定:在较大的罗马数字的左边记上较小的罗马数字,表示大数字减小数字。\n\n```java\npublic int romanToInt(String s) {\n\t\tchar[] chr = s.toCharArray();\n\t\tMap<String, Integer> map = new HashMap<>();\n\t\tmap.put(\"I\", 1);\n\t\tmap.put(\"V\", 5);\n\t\tmap.put(\"X\", 10);\n\t\tmap.put(\"L\", 50);\n\t\tmap.put(\"C\", 100);\n\t\tmap.put(\"D\", 500);\n\t\tmap.put(\"M\", 1000);\n\t\tint result = 0;\n\t\tfor (int i = 0; i < chr.length - 1; i++) {\n\t\t\tint nowInt = map.get(String.valueOf(chr[i]));\n\t\t\tint nextInt = map.get(String.valueOf(chr[i + 1]));\n\t\t\tif (nowInt < nextInt) {\n\t\t\t\tresult -= nowInt;\n\t\t\t} else {\n\t\t\t\tresult += nowInt;\n\t\t\t}\n\t\t}\n\t\tresult += map.get(String.valueOf(chr[chr.length - 1]));\n\t\treturn result;\n}\n```\n","tags":["leetcode"]},{"title":"LeetCode Majority Element 169","url":"/2016/08/24/leetcode-majority-element-169/","content":"\n[169. Majority Element](https://leetcode.com/problems/majority-element/)\n\nGiven an array of size n, find the majority element. The majority element is the element that appears more than [ n/2 ] times.\n\nYou may assume that the array is non-empty and the majority element always exist in the array.\n\n<!-- more -->\n\n### 题目大意\n\n给定一个数组,找这个数组中的主元素,主元素是元素出现次数大于⌊ n/2 ⌋的元素。\n假设给定的数组非空,主元素都存在。\n\n### 自己的解法\n\n将数组进行排序,根据题意,那么中间的这个数就是 majority element\n\n```java\npublic int majorityElement(int[] nums) {\n Arrays.sort(nums);\n return nums[nums.length / 2];\n}\n```\n\n### 别人的解法\n\n[【LeetCode-面试算法经典-Java 实现】【169-Majority Element(主元素)】](http://blog.csdn.net/DERRANTCM/article/details/47902549)\n\n  用一个标记 cnt 记录某个元素出现的次数,如果后面的元素和它相同就加一,有一个元素和他不相同就减一,当 cnt 小于等于 0 时重新记录新的元素。\n\n```java\npublic int majorityElement(int[] nums) {\n int main = nums[0]; // 用于记录主元素,假设第一个是主元素\n int count = 1; // 用于抵消数的个数\n for (int i = 1; i < nums.length; i++) { // 从第二个元素开始到最后一个元素\n if (main == nums[i]) { // 如果两个数相同就不能抵消\n count++; // 用于抵消的数据加1\n } else {\n if (count > 0) { // 如果不相同,并且有可以抵消的数\n count--; // 进行数据抵消\n } else { // 如果不相同,并且没有可以抵消的数\n main = nums[i]; // 记录最后不可以抵消的数\n }\n }\n }\n // 对于数组中可能没有主元素的情况,题中说明存在,此步可以省略。\n // count = 0;\n // for (int a : nums) {\n // if (a == main) {\n // count++;\n // }\n // }\n // if (count >= nums.length / 2) {\n // return main;\n // } else {\n // throw new RuntimeException(\"No majority element\");\n // }\n return main;\n}\n```\n","tags":["leetcode"]},{"title":"LeetCode First Unique Character in a String 387","url":"/2016/08/23/leetcode-first-unique-character-in-a-string-387/","content":"\n[387. First Unique Character in a String](https://leetcode.com/problems/first-unique-character-in-a-string/)\n  Given a string, find the first non-repeating character in it and return it's index. If it doesn't exist, return -1.\n  **Examples:**\n\n```\ns = \"leetcode\"\nreturn 0.\n\ns = \"loveleetcode\",\nreturn 2.\n```\n\n<!-- more -->\n\n  **Note:**\n  You may assume the string contain only lowercase letters.\n\n### 自己的解法\n\n开始觉的遍历 char[] 然后判断在 String 的首位置和末位置,若一样就返回索引。呃,感觉有点偷鸡\n\n```java\npublic int firstUniqChar(String s) {\n for (int i = 0; i < s.length(); i++) {\n if (s.indexOf(s.charAt(i)) == s.lastIndexOf(s.charAt(i))) {\n return i;\n }\n }\n return -1;\n}\n```\n\n查了下别人的,又是 `new int[26]`\n\n```java\npublic int firstUniqChar(String s) {\n int[] count = new int[26];\n for (int i = 0; i < s.length(); i++) {\n count[s.charAt(i) - 'a']++;\n }\n for (int i = 0; i < s.length(); i++) {\n if (count[s.charAt(i) - 'a'] == 1) {\n return i;\n }\n }\n return -1;\n}\n```\n\n但是在运行时间上,下面的方法更快\n","tags":["leetcode"]},{"title":"LeetCode Maximum Depth of Binary Tree 104","url":"/2016/08/23/leetcode-maximum-depth-of-binary-tree-104/","content":"\n[104. Maximum Depth of Binary Tree](https://leetcode.com/problems/maximum-depth-of-binary-tree/)\n  Given a binary tree, find its maximum depth.\n  The maximum depth is the number of nodes along the longest path from the root node down to the farthest leaf node.\n\n<!-- more -->\n\n### 自己的解法\n\n自己想到应该用递归、为空的返回 0,不为空的返回 1,递归累加;但是有两个点这么判断呢?其实很容易取两数字的 MAX\n\n```java\npublic int maxDepth(TreeNode root) {\n if (root == null) {\n return 0;\n }\n return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));\n}\n```\n\n参考:\n[104 Maximum Depth of Binary Tree | Data Structure and algorithm analysis](https://jingjingshao.gitbooks.io/data-structure-and-algorithm-analysis/content/Tree/104_maximum_depth_of_binary_tree.html)\n","tags":["leetcode"]},{"title":"【此生为完成】随笔","url":"/2016/08/22/being-alive-is-a-gift-reading-notes/","content":"\n> 我们要用多大的代价,才能认清活着的意义?\n> 于娟,这个风华正茂的女子,拥有留洋经历和博士学位的复旦大学青年教师,在与晚期癌症抗争一年又四个月后,终于撒手人寰。她带走的家人的思念和不舍,给我们留下坚强的力量。\n\n《此生未完成》这本书是上周末聚会时 Zhen 提到的,上周京东买书就带上了。书里前半探讨作者在生死临界时对生活、工作、名利、家人、朋友的一些看法和思考。这对于刚刚毕业的我来说,确实有些是无法真切体会到的,毕竟经历少。但是在书的字里行间中,仍然可以看到一个(不知道用什么词)的女子。\n\n<!-- more -->\n\n## (一)\n\n疾病能让人有怎样的疼痛?\n\n> 我还是没有哭,不是因为坚强,而是因为痛的想不起来哭,那个时候,只要能用尽全力顶着。如果稍微分神,我就会痛的晕厥。我不想家人看到我的痛苦。\n\n说来惭愧的一个对比,自己在喝多时有过这样的经历:自己抱着垃圾桶使劲顶着一股劲,要是有人来给自己说话,就会分神似的使劲的呕吐。可这蚀骨之痛要比这疼痛多少倍呢?\n\n## (二)\n\n> 重要的是当他得知我生病消息后的第一反应,眼神表情乃至电话语气网络留言里端倪尽出,你会觉得世间很多人情世故是那么的让你淡然一笑。\n\n在遇到大事的时候方显出真正的朋友和所谓的朋友,扬尘散土,洗沙留金。能遇到“大事”的机会不多,遇到的“大事”还能全身而退的机会就更少了。所以觉的自己应该及时去明辨所有的朋友,将真心给最真的,对于其他的就随他去吧。我不可能讨所有人喜欢,也用不着。\n\n## (三)\n\n> 三十岁之前的努力更多是因为自己有着太多的欲望和执著,从没有“只要活着就好”的简单。名利权情,没有一样是不辛苦的,却没有一样可以带走。\n\n这是一个作者很重要的一个观点。在现在的我看来,“只要活着就好”的活着,是一种精神上富有的活着,是充满智慧的活着。\n就说现在的自己,什么都不曾拥有,或许不用拥有过的名、利,但是其他方面什么都没有,自己一切都是空的,看到好就会有强烈的“欲望”,如果再强烈一点就会有“执著”。在文字中我感受到了作者在精神方面的强大与富有,或许在这之后才能感受到在名、利之外更重要的东西。其实,自己真的很难想通,到底拥有什么自己就可以“苟”了,自己到底想要什么。了解自己真正想要什么真是一幸事。\n作者提到的有时间,多多陪陪家人父母、有机会就尽孝,自己是很赞同的。\n\n## (四)\n\n> 外公反映土豆每次经过,都趴在玻璃上看玻璃窗里的圣诞树、圣诞彩球和姜饼屋。土豆眼神里有点想,但却乖乖看着,并不讨。外公回想说:“那个眼神我想起了卖火柴的小女孩,有时候孩子听话,你却反而会心酸。”\n\n这文字碰触心灵最柔软的地方。\n\n## (五)\n\n> 你的妈妈不是懦夫,所以你的人生里,遇到珍贵的人与关键的事,都要积极争取,可以失败,但是不能放弃。我想做一个让儿子骄傲的妈妈,只此一点,无论任何地步,我都不会选择自己走,哪怕,万劫不复的痛。\n\n## (六)\n\n> 更多人不会明白,我们两个的谈笑深处埋藏着多少不能言表的无声叹息。上一次见面,我和梅两个事多么风华正茂,像展翅云霄的鹰隼,挥着翅膀相约下次的冲天。这次的相逢,是灰头土脸被命运按在尘土里依然微笑的土鸡之间的问候。\n\n## (七)\n\n“为啥是我得癌症” 的非学术报告,是书的精华文章。从饮食习惯、睡眠习惯、突击作业、环境问题,分析了对身体不利的因素。突击作业其实就是工作习惯。\n\n在饮食上常吃不寻常的动物、暴饮暴食、嗜荤如命。\n\n> 我们要相信聪明的祖先,几千年的智慧沉淀,才最终锁定了我们现在的食材,并在远古对他们进行豢养。如果孔雀比鸡好吃,那么现在的就是孔雀,孔雀就是鸡。\n\n在睡眠习惯上:\n\n> 二十三时至次日三时,是肝脏活动能力最强的时段,也是肝脏最佳的排毒时期,如果肝脏功能得不到休息,会引起肝脏血流相对不足,已受损的肝细胞难以修复并加剧恶化。所以“长期熬夜等于慢性自杀”的说法并不夸张。因此医生建议人们从二十三时左右开始上床睡觉,次日一至三时进入深睡眠状态,好好地养足肝血。\n\n突击作业:\n\n> 一辆平时就跌跌撞撞一直不维修的破车,一踩油门就彻夜地疯跑疯开半个月。一年搞个五六次,就是钢筋铁打的汽车,开个二十几年也到报废了。\n\n环境问题,空气污染、水污染和食品安全危机还有那可怕的甲醛家具。\n\n## (八)\n\n> 不去想控制大局小局,不想去多管闲事淡事,我不在有对手,不再有敌人,我也不在关心谁比谁强,课题也好,任务也罢,暂且放着。\n\n也许只有正在想明白的人,才懂的世间的一切,隔岸看花、风淡云轻。\n","tags":["reading-notes"]},{"title":"LeetCode Intersection of Two Arrays 349","url":"/2016/08/22/leetcode-intersection-of-two-arrays-349/","content":"\n[349. Intersection of Two Arrays](https://leetcode.com/problems/intersection-of-two-arrays/)\n  Given two arrays, write a function to compute their intersection.\n  **Example:**\n  Given nums1 = `[1, 2, 2, 1]`, nums2 = `[2, 2]`, return `[2]`.\n\n<!-- more -->\n\n  **Note:**\n  Each element in the result must be unique.\n  The result can be in any order.\n\n### 自己的解法\n\n使用 set 进行去重\n\n```java\npublic static int[] intersection2(int[] nums1, int[] nums2) {\n\tSet<Integer> nums1Set = new HashSet<>();\n\tSet<Integer> set = new HashSet<>();\n\tfor (int integer : nums1) {\n\t\tnums1Set.add(integer);\n\t}\n\tfor (int integer : nums2) {\n\t\tif (nums1Set.contains(integer)) {\n\t\t\tset.add(integer);\n\t\t}\n\t}\n\tint[] result = new int[set.size()];\n\tint i = 0;\n\tfor (int integer : set) {\n\t\tresult[i] = integer;\n\t\ti++;\n\t}\n\treturn result;\n}\n```\n\n### 别人的思路\n\n[LeetCode – Intersection of Two Arrays (Java)](www.programcreek.com/2015/05/leetcode-intersection-of-two-arrays-java/)\n\nBinary Search\n\n```java\npublic int[] intersection(int[] nums1, int[] nums2) {\n Arrays.sort(nums1);\n Arrays.sort(nums2);\n ArrayList<Integer> list = new ArrayList<Integer>();\n for(int i=0; i<nums1.length; i++){\n if(i==0 || (i>0 && nums1[i]!=nums1[i-1])){\n if(Arrays.binarySearch(nums2, nums1[i])>-1){\n list.add(nums1[i]);\n }\n }\n }\n int[] result = new int[list.size()];\n int k=0;\n for(int i: list){\n result[k++] = i;\n }\n return result;\n}\n```\n\nTime = O(nlog(n)).\nSpace = O(n).\n","tags":["leetcode"]},{"title":"LeetCode Same Tree 100","url":"/2016/08/22/leetcode-same-tree-100/","content":"\n[100. Same Tree](https://leetcode.com/problems/same-tree/)\n  Given two binary trees, write a function to check if they are equal or not.\n  Two binary trees are considered equal if they are structurally identical and the nodes have the same value.\n\n<!-- more -->\n\n关于树结构自己没怎么看过,查了查遍历通常是:递归、stack\n\n### 自己的解法\n\n```java\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n * int val;\n * TreeNode left;\n * TreeNode right;\n * TreeNode(int x) { val = x; }\n * }\n */\npublic class Solution {\n public boolean isSameTree(TreeNode p, TreeNode q) {\n if (p == null && q == null) {\n\t\t\treturn true;\n\t\t}\n\t\tif (p == null || q == null) {\n\t\t\treturn false;\n\t\t}\n\t\tif (p.val == q.val) {\n\t\t\treturn isSameTree(p.left, q.left) && isSameTree(p.right, q.right);\n\t\t}\n\t\treturn false;\n }\n}\n```\n","tags":["leetcode"]},{"title":"LeetCode Excel Sheet Column Title 168","url":"/2016/08/22/leetcode-excel-sheet-column-title-168/","content":"\n[168. Excel Sheet Column Title](https://leetcode.com/problems/excel-sheet-column-title/)\n  Given a positive integer, return its corresponding column title as appear in an Excel sheet.\n\n<!-- more -->\n\nFor example:\n\n```java\n 1 -> A\n 2 -> B\n 3 -> C\n ...\n 26 -> Z\n 27 -> AA\n 28 -> AB\n```\n\n### 自己的思路\n\n十进制转“二十六进制”\n\n```java\npublic String convertToTitle(int n) {\n\tString str = \"\";\n\tdo {\n\t\tint chrInt = n % 26;\n\t\tchar chr;\n\t\tif (chrInt == 0) {\n\t\t\tchr = 'Z';\n\t\t\tn -= 26;\n\t\t} else {\n\t\t\tchr = (char) (chrInt - 1 + 'A');\n\t\t}\n\t\tstr = chr + str;\n\t\tn = n / 26;\n\t} while (n != 0);\n\treturn str;\n}\n```\n\n### 别人的思路\n\n```java\npublic String convertToTitle(int n) {\n if(n <= 0){\n throw new IllegalArgumentException(\"Input is not valid!\");\n }\n\n StringBuilder sb = new StringBuilder();\n\n while(n > 0){\n n--;\n char ch = (char) (n % 26 + 'A');\n n /= 26;\n sb.append(ch);\n }\n\n sb.reverse();\n return sb.toString();\n}\n```\n\n思路是一样的,但是代码更简洁,使用了`StringBuilder` 和 `reverse()`,提前减 1 也避免了特殊处理'Z'\n\n在算法问题中常常用到:\n\n- `1` 的 ascii 为 33,十六进制为 21H\n- `A` 的 ascii 为 65,十六进制为 41H\n- `a` 的 ascii 为 97,十六进制为 61H\n","tags":["leetcode"]},{"title":"LeetCode Excel Sheet Column Number 171","url":"/2016/08/22/leetcode-excel-sheet-column-number-171/","content":"\n[171. Excel Sheet Column Number](https://leetcode.com/problems/excel-sheet-column-number/)\n  Given a column title as appear in an Excel sheet, return its corresponding column number.\n\n<!-- more -->\n\nFor example:\n\n```java\n A -> 1\n B -> 2\n C -> 3\n ...\n Z -> 26\n AA -> 27\n AB -> 28\n```\n\n### 自己的思路\n\n很像是“二十六进制”转十进制\n\n```java\npublic int titleToNumber(String s) {\n\tint col = 0;\n\tchar[] chars = s.toCharArray();\n\tint i = 0;\n\twhile (i < chars.length) {\n\t\tint charInt = chars[chars.length - 1 - i] - 64;\n\t\tcol += Math.pow(26, i) * charInt;\n\t\ti++;\n\t}\n\treturn col;\n}\n```\n\n在算法问题中常常用到:\n\n- `1` 的 ascii 为 33,十六进制为 21H\n- `A` 的 ascii 为 65,十六进制为 41H\n- `a` 的 ascii 为 97,十六进制为 61H\n\n上面有个写的不好的地方就是 64,好像的很莫名的一个数字,其实时在将 A 折算为 1\n","tags":["leetcode"]},{"title":"【蛤蟆的油】随笔","url":"/2016/08/17/something-like-an-autobiography-reading-notes/","content":"\n> 日本民间流传着这样一个故事:在深山里,有一种特别的蛤蟆,它和同类相比不仅外表更丑,而且还多长了几条腿。人们转到它后,将其放在镜前或者玻璃箱内,蛤蟆一看到自己丑陋不堪的真面目,不禁吓出一身油。这种油,也是民间用来治疗烧伤烫伤的珍贵药材。\n\n第一次写人文类书籍的笔记。\n\n故事是从黑泽明一岁多光着身子洗澡开始回忆的,然后按时间,一个个小故事讲述自己。\n\n(一)\n\n二年级是他得到了“糖酥”这个绰号,因为有人揪他的头发,往他西装上抹鼻涕,让他哭了好几次。\n\n想到了自己大概三四年级的一件事:一次在公交车站等车被人嘲笑嘴巴大,是陌生的同龄人。自己当时被蒙掉了,也不知道说什么或者做点什么,傻傻的,后来车来了,上车回家了。我记得我是在车上委屈的哭了。十几年来这件事我一直都记得,总是把这事归为黑历史、很怂的事,也从来没和别人讲过。我当时为什么没有还击?是被大人教育的打架不是好孩子?还是自己的胆小?还是正中下怀?细思这些都有,是太小没有自己的判断力,也缺少一份对自己信心与肯定。\n\n要能再来我一定会把那小子打到哭。\n\n<!-- more -->\n\n(二)\n\n小黑遇到了“崇尚自由、以鲜活的感性及创造精神从事教育的老师”,立川老师对智力发育缓慢、性格乖僻的他多方庇护,使他第一次有了自信。立川老师还为了使植草(小黑好友)尽快地开出灿烂的花,把植草移栽到了副班长这个盆里,而且放在向阳之处。\n\n遇到一个能指导你成长的同学、朋友、老师、哥哥姐姐真的是一件很幸运的事情。我觉的他们在有智慧的人群里,不是瞎折腾或者很世俗。\n\n(三)\n\n给客人吃的鱼,鱼头朝左,鱼腹朝着客人。给剖腹者上的鱼,大概是鱼头朝右,鱼背朝着本人。或许是因为,如果让剖腹者看到剖开的鱼腹,未免太残酷了。\n\n(四)\n\n小黑在学剑道时,道场的主人被汽车撞了,那是汽车本来是稀罕之物,却又让这稀罕之物撞伤,简直像挨马踢一样可笑。而对道场主人的尊敬烟消云散。自己在高野佐三郎的道场在学交叉坎时,被弹回来撞到墙上,眼前一阵发黑,两眼直冒金星,对自己剑道的自豪化为乌有。\n\n人世并不像想象的那么简单。人外有人,天外有天。自己不免是井底之蛙,总是管中窥豹。很多像这样的道理,我都听过,我也认同,可在没有遇到一件具体的事情的时候,总是看不到认不清自己的浅薄。多多经历,多多思考。\n\n(五)\n\n在体育课上,体育老师教跳高采取比赛的方法,撞掉竿就被淘汰下去。黑泽明刚一起跑,同学们就哄堂大笑。他们准是觉得我会头一个把横杆撞下来。出乎意料的是我轻松越过了横杆。横杆逐渐上移,然而,挑战的人中间总有我。看热闹的人们寂然无声了。不知道什么原因,居然出现了奇迹:只剩我一个人在挑战了。一个个无不呆呆地看着我。而且,只剩下我一个人之后,仍然几次跳过了横杆。也许是天使哀怜我体操课总得零分,给我的背插上了翅膀。\n最后一句,写的真美。\n\n(六)\n\n人是可笑的,过分受惊时,头脑的一部分会脱离现实,想入非非,看起来显得十分沉着。当大地震时,抱着电线杆忍受着强烈的摇晃时,仍然想到了这些,而且非常佩服日式建筑的优越性。然而这绝不意味着我遇事沉着冷静。\n\n(七)\n\n没有经历过的人无法想象对人类来说何谓正真的黑暗,这黑暗又是多么的可怕。这恐怖夺走了人的正气。无论朝哪里望,什么都看不见,这是最使人感到孤立无援的地方,它使人内心深处产生了惊慌和不安,也使人处于名副其实的疑心生暗鬼状态。\n\n好像有点像我国的“吃盐防辐射”,都是巧妙地利用黑暗对人的威胁制造的阴谋?还是易受蛊惑的人心是社会发展的过程中要的经历?\n\n(八)\n\n结束这趟可怕的远足,当天晚上,我以为一定难以入睡,还会大做噩梦,但头刚一沾枕就到了第二天早晨。哥哥说:“面对可怕的事物闭眼不敢看,就会觉得它可怕;什么都不在乎,哪里还有什么可怕的呢?”。现在看来,那趟远足,对哥哥来说可能也是可怕的。正因为可怕,所以必须征服它。这次远足也是一次征服恐怖的远征。\n\n可是面对了自己可怕的东西后,还是怕的原因是什么呢?是还没有正面面对?\n\n(九)\n\n高山仰止\n\n对有气质、有修养或有崇高品德之人的崇敬、仰慕之情。\n\n(十)\n\n人能把卫星送进宇宙,在精神层面却不会向上看,而是像野狗一样,只注意脚下,徘徊不已。\n\n(十一)\n\n后娘用艾苦我身/我为后娘买大艾/为讨她欢心。继母为什么虐待前房孩子?如果说出于憎恨丈夫的前妻而虐待其子,这是没有道理的。认为这完全出于愚昧。愚昧是人的疯狂病症之一,以虐待没有反抗能力的孩子或者小动物为乐的人,纯粹是疯子。然而这类疯子并不认为这是犯罪,却认为是理所当然,所以难以对付。我能解开绑她的带子,然而无法把她从捆绑她的境遇中救出。对这个孩子来说,人们的同情是毫无意义的。那种温情反倒给她招来更多的麻烦。\n\n想到了打劫的故事,说:被劫持的人只要交的钱比上一人交的钱多一倍,就可以走。人争先恐后的交赎金,不再有人反抗。\n\n(十二)\n\n本来是红的,却不老实说它是红的,等到能坦率说出来的时候,已经到了晚年。很多人年轻时表现欲过强,这样反倒迷失了自己。我也毫不例外,拼命的考技巧作画,那画的俗气使我对自己心生憎恶,逐渐丧失了对自己才能有的信心,把绘画看作痛苦了。为了买油彩和画布还得干些不愿干的杂活儿赚钱才行。\n\n少搞些套路,玩花活,踏踏实实才是真。别瞎折腾,没有用。\n\n(十三)\n\n产生这种急于就业的焦虑和降格以求的心情,主要是以为哥哥突然去世,我要继他之后负起长子的责任。\n\n在年初有类似的经历,最后一个学期,都找工作去了,也要毕业要长大了,急着确定个工作。其实现在也着急着,想快有所作为。前几日,看到好友暗叶的朋友圈发:年轻人想法没多,都是还得慢慢来。我初看是不赞同。黑泽明的父亲也告诫他说:“不要着急,也没有着急的必要”,“要等下去,前面的道路自然会打开的”。这些话我确实还不太理解为什么,得慢慢的体会。\n\n(十四)\n\n凡事想用人的,必须先培养人。培养出人,激发出人的才能,这才能用。\n\n纷扰的现在,还有多少人或公司愿意培养人?应该说人培养应该是必须的。\n\n(十五)\n\n山本先生对于追随他的副导演,绝不干改变他们个性的事,而是一心一意的着力于发掘他们的个性;而且丝毫不让我们有从师学习的拘束心情,而是让我们充分的发展自己。\n山本先生为了培养副导演,不惜牺牲自己的作品。\n“那是卖香荷包的招牌。随随便便说话可不行!不知道的事情就该说不知道!”\n\n(十六)\n\n提出批评不难。但是,提出批评的人能够按照自己的批评意见亲自把剧本改好,却不是普通人能做到的事。\n\n太有同感了,常常有人在旁边指手画脚,却说不出什么切实可行的办法建议。\n\n(十七)\n\n人往往习惯于认为价值与辛苦成正比。这在电影界剪辑上是最要不得的。人们说电影是时间的艺术,所以,没有用的时间就应删去。\n\n这观点在互联网行业也行的通,做好的功能不合适了,说废也就废掉了。\n\n(十八)\n\n我常常把群众演员的名字忘掉,所以只好按他们的衣服颜色招呼。结果被山本先生训了几句:“黑泽明,那可不行。人都有个名嘛!”。名不见经传的演员听到山本先生如此亲切的招呼他,无不感到。难道能说山本先生有些滑头吗?我看应该说他善于用人。\n\n自己还不是什么重要人物,但是记住别人的名字应该是件重要的事情。\n\n(十九)\n\n关于演员的关键问题主要有以下几点:第一,人很难了解自己,不能客观的观察自己的说话方式和行为举止。第二,凡事有意识的动作,首先注意的不是动作本身,而是意识。第三,教给演员怎么做,同时必须告诉他为什么怎么做,而且让他充分理解、心悦诚服。\n\n做事情的时候应该反向考虑第三点,为什么要怎么做事情?目的是什么?\n\n(二十)\n\n电影评论家中冲有这样的人,看到电影中一些自己认为是缺点的地方就如获至宝,大发谬论,但是电影导演也乐于此道可就错了。\n\n现在这样的人越来越多,哗众取宠。\n\n(二十一)\n\n他们以为一部卖座影片的续集也必然会成功,于是不想再开拓新的领域,总想旧梦重温。重拍的影片绝不如前作,这虽然是已经证明的事实,但他们还要重蹈覆辙,这才是地地道道的愚蠢之举。\n\n学习新东西,找新路子。\n\n(二十二)\n\n我为了抵抗人的苦恼,戴上一幅强者的面具;而植草却为了沉溺与人的苦恼,戴上了一幅弱者的面具。事实不过如此。但这只是表面的不同,就本质来说。我们都是弱者。我不是特别的人。我既不是特别强的人,也不是得天独厚的有特殊才能的人。我不过是个不愿示弱于人,不愿输给别人,因而不懈努力的人。\n\n在原来看到这样的句子是很不屑的,觉的成功的人,说什么都行。现在信了,真的人都本是普通人,只是有的人一直在进步成长就现在不再普通了。\n\n(二十三)\n\n有一天,我在电车里看到这种杂志的广告,简直目瞪口呆。那上面用大字标题写着:是谁夺走了 XX 的贞操?乍看起来,这似乎在为 XX 女士鸣不平,实际则是把 XX 当作玩物恣意戏耍。我认为,这不是言论自由,而是言论暴力。\n\n没想到在那么早,就提到了“言论暴力”的话题,社交平台的评论区常常有的血雨腥风。\n\n(二十四)\n\n本民族人为什么对于本民族的存在毫无自信呢?为什么对异域的东西那么尊重,对于自己的东西就那么轻视呢?\n\n黑泽明提到是:可悲的国民性。自己没太理解。\n\n> 人是很难如实的谈论自己的。人总是本能的美化自己。人不会老老实实的说自己是怎样一个人,常常是假托别人才能老老实实的谈论自己。\n> 因为,在没有什么能比作品更好的说明作者了。\n","tags":["reading-notes"]},{"title":"LeetCode Move Zeroes 283","url":"/2016/08/16/leetcode-move-zeroes-283/","content":"\n[283. Move Zeroes](https://leetcode.com/problems/move-zeroes/)\n  Given an array nums, write a function to move all 0's to the end of it while maintaining the relative order of the non-zero elements. For example, given nums = [0, 1, 0, 3, 12], after calling your function, nums should be [1, 3, 12, 0, 0].\n\n<!-- more -->\n\nNote:\n\n- You must do this in-place without making a copy of the array.\n- Minimize the total number of operations.\n\n---\n\n### 大体意思\n\n将数组中 0 元素移动到数组的末尾,不改变其他元素的顺序;不能 copy 数组,最小化的操作\n\n### 自己的思路\n\n#### 交换冒泡\n\n```java\npublic static void moveZeroes(int[] nums) {\n\n\tboolean hasChange = false;\n\tdo {\n\t\thasChange = false;\n\t\tfor (int i = 0; i < nums.length - 1; i++) {\n\t\t\tif (nums[i] == 0 && nums[i + 1] != 0) {\n\t\t\t\tnums[i] = nums[i + 1];\n\t\t\t\tnums[i + 1] = 0;\n\t\t\t\thasChange = true;\n\t\t\t}\n\t\t}\n\t} while (hasChange);\n\tSystem.out.println(Arrays.toString(nums));\n}\n```\n\n#### 与 0 换位置,参考了别人的想法\n\n```java\npublic static void moveZeroes2(int[] nums) {\n\tint firstZoneIndex = -1;\n\tfor (int i = 0; i < nums.length; i++) {\n\t\tif (firstZoneIndex != -1) {\n\t\t\t// 发现了过0\n\t\t\tif (nums[i] != 0) {\n\t\t\t\tnums[firstZoneIndex] = nums[i];\n\t\t\t\tnums[i] = 0;\n\t\t\t\tfirstZoneIndex++;\n\t\t\t}\n\t\t} else {\n\t\t\t// 没有发现过0\n\t\t\tif (nums[i] == 0) {\n\t\t\t\tfirstZoneIndex = i;\n\t\t\t}\n\t\t}\n\t}\n\tSystem.out.println(Arrays.toString(nums));\n}\n```\n\n### 别人的算法\n\n- [西施豆腐渣 - leetcode 283: Move Zeroes](http://blog.csdn.net/xudli/article/details/48574521)\n\n```java\npublic void moveZeroes(int[] nums) {\n\tint i = 0;\n\tint j = 0;\n\twhile (j < nums.length) {\n\t\tif (nums[j] != 0) {\n\t\t\tif (j != i) {\n\t\t\t\tnums[i++] = nums[j];\n\t\t\t\tnums[j] = 0;\n\t\t\t} else {\n\t\t\t\t++i;\n\t\t\t}\n\t\t}\n\t\t++j;\n\t}\n}\n```\n","tags":["leetcode"]},{"title":"LeetCode Ransom Note 383","url":"/2016/08/11/leetcode-ransom-note-383/","content":"\n[383. Ransom Note](https://leetcode.com/problems/ransom-note/)\n  Given an arbitrary ransom note string and another string containing letters from all the magazines, write a function that will return true if the ransom note can be constructed from the magazines ; otherwise, it will return false.\n  Each letter in the magazine string can only be used once in your ransom note.\n\n<!-- more -->\n\nNote:\nYou may assume that both strings contain only lowercase letters.\n\n```\ncanConstruct(\"a\", \"b\") -> false\ncanConstruct(\"aa\", \"ab\") -> false\ncanConstruct(\"aa\", \"aab\") -> true\n```\n\n---\n\n自己第一次做 LeetCode 上面的题,也没什么算法方面的学习,摸着石头过河,步步提高。\n\n### 大体意思\n\n判断第二个字符串中的字母是否可以组成第一个字符串,每个字母只能用一次;两个字符串仅包含小写字母\n\n### 自己的思路\n\n将 ransom 转成 char 数组,遍历数组,然后在 magazines,进行对比,然后除去一个一样的字母\n\n```java\npublic static boolean canConstruct(String ransom, String magazine) {\n\n\tif (ransom.length() > magazine.length()) {\n\t\treturn false;\n\t}\n\n\tchar[] ransomChar = ransom.toCharArray();\n\tfor (int i = 0; i < ransomChar.length; i++) {\n\t\tString str = String.valueOf(ransomChar[i]);\n\t\tif (magazine.indexOf(str) == -1) {\n\t\t\treturn false;\n\t\t} else if (magazine.indexOf(str) == magazine.length() - 2) {\n\t\t\tmagazine = magazine.substring(0, magazine.indexOf(str));\n\t\t} else {\n\t\t\tmagazine = magazine.substring(0, magazine.indexOf(str)) + magazine.substring(magazine.indexOf(str) + 1);\n\t\t}\n\t}\n\treturn true;\n}\n```\n\n### 别人的算法\n\n- [leetcode Ransom Note - 细语呢喃](https://www.hrwhisper.me/leetcode-ransom-note/)\n\n```java\npublic class Solution {\n public boolean canConstruct(String ransomNote, String magazine) {\n int[] cnt = new int[26];\n for (int i = 0; i < magazine.length(); i++) cnt[magazine.charAt(i) - 97]++;\n\t\tfor (int i = 0; i < ransomNote.length(); i++) if (--cnt[ransomNote.charAt(i) - 97] < 0) return false;\n\t\treturn true;\n }\n}\n```\n\n- 我的理解\n 26 个元素的数组,每个元素存放字母的个数,如果需要的多于存在的 return false\n","tags":["leetcode"]},{"title":"IMOOC 与MySQL的零距离接触","url":"/2016/07/31/imooc-meet-mysql-notes/","content":"\n涵盖全部 MySQL 数据库的基础,MySQL 数据库的基础知识、数据表的常用操作及各种约束的使用,以及综合的运用各种命令实现记录进行 CURD 等操作。\n\n- MySQL 安装与配置\n- 数据类型\n- 流程控制与运算符\n- DDL、DCL、DQL、DML\n- 常用函数\n- 表类型(存储引擎)\n- 图形化工具\n\n<!-- more -->\n\n## 修改 MySQL 提示符\n\nMySQL 客户端的默认提示符是 `mysql>`,基本上没什么实际作用。其实可以修改这个提示符,让它显示一些有用的信息,例如当前所在的数据库等。修改方法有四种,其中前两种只对当前连接有效,后两种则对所有连接有效。\n\n### 连接客户端时通过参数指定\n\n```bash\nmysql --prompt=\"(\\u@\\h) [\\d]> \"\n```\n\n`my.cnf`\n\n```conf\n[mysql]\nprompt=mysql(\\\\u@\\\\h:\\\\d)>\n```\n\n这样提示符就会变成 `(user@host) [database]>`。其中常用的字符参数有:\n\n- `\\D` 完整的日期\n- `\\d` 当前数据库\n- `\\h` 服务器名称\n- `\\u` 当前用户\n\n连接上客户端后,通过 `prompt` 命令修改\n\n```sql\nprompt (\\u@\\h) [\\d]>\n```\n\n在 MySQL 的配置文件中配置。\n\n```sql\nprompt=(\\\\u@\\\\h) [\\\\d]>\\\\_\n```\n\n通过环境变量配置。\n\n```sql\nexport MYSQL_PS1=\"(\\u@\\h) [\\d]> \"\n```\n\n> 参考:<http://renial.iteye.com/blog/773675>\n\n## MySQL 语法规范\n\n- 关键字和函数名称全部 **大写**\n- 数据库名称,表名称,字段名称全部 **小写**\n- SQL 语句必须以 **分号结尾**\n\n## 数据库操作\n\n### 数据库创建:CREATE\n\n- 语法:CREATE {DATABASE SCHEMA} [IF NOT EXISTS] db_name [DEFAULT] CHARACTER SET [=] charset_name\n- DATABASE 和 SCHEMA 是相同的,任选其一\n- IF NOT EXISTS:如果创建的数据库存在,则不只报出 warning,不写会报错\n- CHRARCTER SET utf8:为表设置编码方式,如果不设置则用 mysql 默认的编码方式\n\n### 查看数据库列表:SHOW\n\n- SHOW { DATABASE SCHEMAS } [LIKE 'pattern' WHERE expr]\n- SHOW CREATE DATABASE xx:显示 xx 数据库信息\n\n### 数据库的修改:ALTER\n\n- 修改数据库编码方式:ALTER { DATABASE SCHEMAS } [db_name][default] CHARACTER SET [=] charset_name\n\n### 删除数据库:DROP\n\n- 删除数据库:DROP { DATABASE SCHEMAS } [IF EXISTS] db_name;\n\n```sql\n--修改mysql操作符为当前日期\nmysql -uroot -proot prompt \\D\n--展示所有数据库\nshow databases;\n--创建数据库\ncreate database if not exists t1 character set gbk;\n--展示数据库t1的创建命令和编码形式\nshow create database t1;\n--修改数据库编码格式\nalter database t2 character set = utf8;\n--删除数据库\ndrop database if exists t1;\n--展示警告信息\nshow warnings;\n```\n\n## 四、数据类型\n\n![160731-imooc-mysql-tutorials-notes-int](https://user-images.githubusercontent.com/9289792/80199388-d5776280-8653-11ea-9182-29a23e53ba50.png)\n\n![160731-imooc-mysql-tutorials-notes-float](https://user-images.githubusercontent.com/9289792/80199384-d4decc00-8653-11ea-88ea-81e6e1f29f5a.png)\n\n![160731-imooc-mysql-tutorials-notes-char](https://user-images.githubusercontent.com/9289792/80199382-d3150880-8653-11ea-9c7c-2385530649ad.png)\n\n- char 型字符串有 0-255 之间的字节,通常被称作定长类型。所存字节不满 char 型所给它的字节,剩下的以空格补齐,仍会存储满你所给它的字节。\n- varchar 型的字符串是变长类型,存多少字节它就存储多少字节,不会在后面补上空格。字节长度在 0-65535 之间。\n- 1 个字节等于 8 个 bit,L+ 几个字节后面的几个字节指的是他的最大存储范围。8 个 bit 等于就是 8 个 1,相当于 255,就是 2 的 8 次方。16 个 1 就相当于 2 的 16 次方。3 个字节四个字节依次类推。\n- ENUM 枚举值 就是给他几个选项,它从这几个选项中做选择,最多有 65535 个值。\n- SET 我们称之为集合,集合最多有 64 个成员,它在这些集合成员中做任意的排列组合。\n\n![160731-imooc-mysql-tutorials-notes-time](https://user-images.githubusercontent.com/9289792/80199391-d60ff900-8653-11ea-9836-4fa28ac1cb33.png)\n\n- YEAR:2 位或者 4 位,1970 到 2069 年,允许 70 到 69\n- TIME:-8385959 到 8385959\n- DATE:1000.01.01 到 9999.12.31\n- DATETIME:1000.01.01 00:00:00 到 9999.12.31 23:59:59\n- TIMESTAMP:1970.01.01 00 点起 到 2037 年之间的一个值\n\n## 五、数据表操作\n\n- 数据表(或表)是数据库最重要的组成部分之一,是其他对象的基础\n- 表是一个二维表,行称为 `记录`,列称为 `字段`\n\n### 创建数据表据表\n\n1、打开数据库:`USE db_name;`\n2、查看当前数据库:`SELECT DATABASE();`\n3、创建数据表:\n\n```sql\nCREATE TABLE [IF NOT EXISTS] table_name(\ncolumn_name data_type,\n...\ncolumn_name data_type\n)\n```\n\n```sql\nCREATE TABLE tb1(\nusername VARCHAR(20),\nuserage TINYINT UNSIGNED,\nsalary FLOAT(8,2) UNSIGNED,\n);\n```\n\n### 查看数据表\n\n```sql\nSHOW TABLES [FROM db_name] [LIKE 'pattern' WHERE expr];\n```\n\n查看当前选择的数据库的所有表\n\n```sql\nSHOW TABLES;\n```\n\n查看 MYSQL 数据库中的所有表,当前选择数据库位置不变\n\n```sql\nSHOW TABLES FROM MYSQL;\n```\n\n查看当前选择的数据库\n\n```sql\nSELECT DATABASE();\n```\n\n查看数据表结构\n\n```sql\nSHOW COLUMNS FROM table_name;\n```\n\n### 插入与查找\n\n插入记录\n\n```sql\nINSERT [INTO] tb_name [(col_name,col_name,...)] VALUES (va1,...)\n```\n\n查找记录\n\n```sql\nSELECT expr,.. FROM tb_name\n```\n\n### 空值与非空\n\nNULL,字段值可以为空\nNOT NULL,字段值禁止为空\n\n```sql\nCREATE TABLE tb2(\nusername VARCHAR(20) NOT NULL;\nage TINYINT UNSIGNE NULL;\n);\n```\n\n### 自动编号\n\n自动编号 `AUTO_INCREMENT`\n自动编号,且必须与主键配合使用\n\n自动编号 AUTO_INCREMENT 作用\n1、自动编号:保证记录的唯一性\n2、类型必须为整型(可以是 FLOAT(5,0)等,小数点后必须为 0),必须和主键 PRIMARY KEY 组合使用\n3、默认情况下,起始值为 1,每次的增量为 1\n\n```sql\nCREATE TABLE tb3(\nid SMALLINT UNSIGNED AUTO_INCREMENT,\nusername VARCHAR(30) NOT NULL);\n-- 报错,自动增量字段必须设置成主键\n```\n\n### 主键约束\n\n`PRIMARY KEY`,也可以写成`KEY`\n1、每张数据表只能存在一个主键\n2、主键保证记录的唯一性\n3、主键自动为`NOT NULL`,即必须要为主键赋值。但如果记录中添加了`AUTO_INCREMENT`,那么不需要手动赋值\n4、`auto_increment`必须和主键`primary key`一起使用,但主键`primary key`不一定要和`auto_increment`一块使用\n\n```sql\nCREATE TABLE tb3(\nid SMALLINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,\nusername VARCHAR(30) NOT NULL);\n```\n\n### 唯一约束\n\n`PRIMARE KEY`\n主键约束 一张表中只能有一个\n\n`UNIQUE KEY`\n1、唯一约束\n2、唯一约束可以保证记录的唯一性\n3、唯一约束的字段可以为空值\n4、每张数据表可以存在多个唯一约束\n\n```sql\nCREATE TABLE tb3(\nid SMALLINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,\nusername VARCHAR(30) NOT NULL UNIQUE KEY,\nage tinyint UNSIGNED\n);\n```\n\n- 在创建索引时也是有区别的\n\n### 默认约束\n\n`DEFAULT` 当插入记录时,如果没有明确为字段赋值,则自动赋予默认值。\n\n```sql\nCREATE TABLE tb6(\nid SMALLINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,\nusername VARCHAR(20) NOT NULL UNIQUE KEY,\nsex ENUM('1','2','3') DEFAULT '3'\n);\n```\n\n其中对性别字段默认选择`3`\n\n## 六、约束以及修改数据表\n\n- FOREIGN KEY:保持数据一致性,完整性;实现一对一或一对多关系。\n- 要求:父表和子表必须使用相同的存储引擎,而且禁止使用临时表;数据表的存储引擎只能为 InnoDB;外键列和参照列必须具有类似的数据类型。其中数字的长度或是否有符号位必须相同;而字符的长度则可以不同;外键列和参照列必须创建索引。如果外键列不存在索引的话,MySQL 将自动创建索引。\n- 在 MY.ini 文件中编辑默认的存储引擎:default-storage-engine=INNODB;\n- 显示创建表的语句:SHOW CREATE TABLE table_name;\n- 查看表是否有索引:SHOW INDEXS FROM table_name;\n- 以网格查看表是否有索引:SHOW INDEXS FROM table_name\\G;\n\n```sql\nCREATE TABLE table_name1(\nid SMALLINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,\nname VARCHAR(20) NOT NULL\n)\n```\n\n```sql\nCREATE TABLE table_name2(\nid SMALLINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,\nusername VARCHAR(20) NOT NULL,\npid SMALLINT UNSIGNED,\nFOREIGN KEY (pid) REFERENCES table_name1(id) /* 外键 pid 参照 table_name1中的 id 字段 */\n)\n```\n\n-- EOF --\n","tags":["mysql"]},{"title":"Linux SSH 密钥登陆免密码","url":"/2016/07/29/linux-ssh-key-login-without-password/","content":"\n最近有需求使用 SSH 进行通信,而且要需免密码,总结了 SSH 密钥登陆免密码的方法。\n\n## 快速配置\n\n- 本机 ip:192.168.1.1\n- 服务器 ip:192.168.1.2\n\n要实现本机免密码登录服务器,执行如下命令:\n\n```bash\nssh-copy-id [email protected]\n```\n\n如果命令成功,则说明配置成功。如果执行失败,则需要参考下面的步骤进行配置。\n\n## 本地配置步骤\n\n### 客户端生成公钥、私钥\n\n```bash\nssh-keygen -t rsa -P ''\n```\n\n- `-t` 表示 `key` 的类型\n- `-P` 表示密码,`-P ''` 就表示空密码,也可以不用 `-P` 参数\n\n运行完之后在 `~/.ssh` 目录下生成私钥 `id_rsa` 和公钥 `id_rsa.pub`。\n\n### 将公钥添加到服务器\n\n将客户端公钥 `id_rsa.pub` 写入到服务端 `~/.ssh/authorzied_keys` 之中。\n\n```bash\ncat ~/.ssh/id_rsa.pub\n```\n\n复制输出的内容,登陆服务端:\n\n```bash\nvim ~/.ssh/authorized_keys\n```\n\n粘贴到最后一行。如果没有这个 `authorzied_keys` 文件,可以创建一个。\n\n### 设置文件权限\n\n服务端,非必须。\n\n```bash\nsudo chmod 755 ~/.ssh\nsudo chmod 600 ~/.ssh/authorized_keys\n```\n\n### 客户端重启服务\n\n```bash\nsudo service ssh restart\n```\n\n此时就可以不免密码登陆服务端了。\n\n## 以证书登录后 使用 sudo 操作\n\n设置用户以证书登录后,使用 sudo 操作。在服务端设置:\n\n```bash\nsudo visudo\n```\n\n```bash\n%sudo ALL=(ALL:ALL) ALL\n\n# 将上一行替换为:\n\n%sudo ALL=(ALL) NOPASSWD:ALL\n```\n\n## 类似 AWS PEM 配置方法\n\n亚马逊 AWS 虚拟服务器使用一个预先生成的 `*.pem` 证书文件(密钥)为客户端和服务器之间建立连接。 例如:\n\n```bash\nssh -i ~/ec2.pem [email protected]\n```\n\n首先确定你可以以密码的形式连接远程服务器,也可以 [创建一个非超级管理员用户,并增加 sudo 权限](http://blog.csdn.net/hanshileiai/article/details/51141854)。\n\n```bash\nsudo ssh [email protected]\n```\n\n### 客户端生成验证没有密码密钥对\n\n```bash\nssh-keygen -t rsa -b 2048 -v\n```\n\n执行上述命令首先会让你输入生成密钥的文件名:我这里输入的 `myPemKey` 之后一路回车。\n\n```bash\nGenerating public/private rsa key pair.\nEnter file in which to save the key (/home/anonymouse/.ssh/id_rsa): myPemKey\nEnter passphrase (empty for no passphrase):\nEnter same passphrase again:\nYour identification has been saved in hetzner.\nYour public key has been saved in hetzner.pub.\nThe key fingerprint is:\nbb:c6:9c:ee:6b:c0:67:58:b2:bb:4b:44:72:d3:cc:a5 localhost@localhost\nThe key's randomart image is:\n```\n\n在执行命令的当前目录下会生成一个 `myPemKey.pub`、`myPemKey` 两个文件。\n\n### 服务端添加公钥\n\n把生成的 `myPemKey.pub` 通过本地命令推送到服务器端,使服务器自动添加认证这个证书\n\n```bash\nssh-copy-id -i ~/myPemKey.pub [email protected]\n```\n\n输入你的 `root` 用户密码\n\n### 测试连接\n\n```bash\nsudo ssh -i ~/myPemKey [email protected]\n```\n\n或者把 `myPemKey` 重命名为 `myPemKey.pem`\n\n```bash\nsudo ssh -i ~/myPemKey.pem [email protected]\n```\n\n### 禁用密码连接\n\n**注意:要保证 .pem 连接成功的状态下,禁用密码连接。**\n\n```bash\nsudo vi /etc/ssh/sshd_config\n```\n\n找到这一行 `#PasswordAuthentication yes` 取消前边的 # 注释,改为:\n\n```bash\nPasswordAuthentication no\n```\n\n重启 ssh 服务\n\n```bash\nsudo service ssh restart\n```\n\n## References\n\n- [韩世磊-ubuntu 生成 .pem 证书连接服务器,取消 OpenSSH 密钥密码认证](http://blog.csdn.net/hanshileiai/article/details/51141638)\n- [韩世磊-ubuntu ssh 证书登录(不输入密码)](http://blog.csdn.net/hanshileiai/article/details/50381467)\n\n-- EOF --\n","tags":["linux","ssh"]},{"title":"IMOOC MySQL开发技巧","url":"/2016/07/25/imooc-mysql-development-skills-notes/","content":"\n主要涉及:JOIN 、JOIN 更新、GROUP BY HAVING 数据查重/去重。\n\n<!-- more -->\n\n## INNER\n\nINNER JOIN、LEFT JOIN、RIGHT JOIN、FULL JOIN(MySQL 不支持)、CROSS JOIN\n\n这是在网上找到的非常好的一篇博文,图解 JOIN 语句:\n\n> [CODING HORROR-A Visual Explanation of SQL Joins](https://blog.codinghorror.com/a-visual-explanation-of-sql-joins/)\n\n下图可以很清楚的明白,JOIN 的数据选取范围:\n\n![160725-imooc-mysql-development-skills-notes-001](https://user-images.githubusercontent.com/9289792/80199172-9d701f80-8653-11ea-8d20-498f54e41708.png)\n\n![160725-imooc-mysql-development-skills-notes-002](https://user-images.githubusercontent.com/9289792/80199188-9fd27980-8653-11ea-9386-e76dca7b4d0c.jpeg)\n\n## 更新使用过滤条件中包括本身的表\n\n更新 `t1` `t2` 表中 `col_a` 重复的字段:\n\n```sql\nUPDATE t1\nSET col_a = 'hi'\nWHERE t1.col_a IN (\n SELECT b.col_a\n FROM t1 a INNER JOIN t2 b on\n a.col_a = b.col_a\n)\n;\nERROR:1093\n```\n\n可转换为:\n\n```sql\nUPDATE t1 aa JOIN(\n SELECT b.col_a\n FROM t1 a INNER JOIN t2 b on\n a.col_a = b.col_a\n)bb on aa.col_a= bb.col_a\nSET col_a = 'hi'\n;\n```\n\n## 查询重复数据、删除重复数据\n\n利用 `GROUP BY` 和 `HAVING` 查询重复数据:\n\n```sql\nSELECT col_a, COUNT(*)\nFROM t1\nGROUP BY col_a HAVING COUNT(*) > 1\n;\n```\n\n删除重复数据,对于相同数据保留 ID 最大的:\n\n```sql\nDELETE a\nFROM t1 a JOIN (\n SELECT col_a,COUNT(*),MAX(id) AS id\n FROM t1\n GROUP BY col_a HAVING COUNT(*) > 1\n)b ON a.col_a = b.col_a\nWHERE a.id < b.id\n;\n```\n\n-- EOF --\n","tags":["mysql"]},{"title":"写一些人文的日志","url":"/2016/07/11/write-some-humanities-diary/","content":"\n还是越来越觉的思想重要。每周给自己博客的计划是写三篇,稍稍改动下:其中一篇要是关于思想上的。\n\n周末的时候和北京的朋友们:龙、梦、思,小聚了下。问起毕业一个月感觉有啥变化,都说没啥。是,刚刚才一个月。可我心里却有些小慌张,总想搞个大新闻,让自己不那么平凡。是眼高手低吗?\n\n<!-- more -->\n\n现在每天都会跑跑步,已经坚持了 6 天了也习惯了,挺赞的。也总是去了解新鲜的东西技术,并且尝试运用它们,才有点原来学长们不喜欢用用过的东西做事情的劲。关于英语的学习,现在找到一个法:阅读英文的技术教程或者技术文档。这些东西肯定和计算机有关,也写的比较通俗,感觉挺有收获的。\n\n最近状态还不错,就是晚上想看会书,可眼睛挺累的。职业生涯的第一个月,经济有些困难。想参与个开源项目。现在手头项目就自己一个人在编,有点孤独,也不知道自己设计的和写的哪里好哪里不好。还有就是在 GitHub 或者 StackOverflow 有人给自己点赞,感觉挺激动的。\n\n因为公司都是 Google 的服务,出门很方便,GitHub 也用的越来越多。时不时总能搜到、看到很多工程师写的博客,有教程、有分享,还有我最喜欢看的:他们的思想。我觉的他们现在都是比我棒的,我想知道他们是怎么想的、怎么解决问题、怎么看待问题、看待工作生活。\n\n最后毒奶一口自己:\n\n> 与那些名校的同学的差距不是毕业后的薪水,也不是学业水平的差距,而是一种思维方式与做事标准。\n> 对自己的标准会不由自主的降低以适应这个环境,减少自身与环境的冲突,在一个低标准下,自觉“满意”的度过每一天。\n> \n> 为什么名企喜欢要名校的学生?其实名企要的不是多么高的 GPA 成绩,而是一种内在的精神状态。他们的辛苦不叫辛苦,也不为百万年薪。辛苦是他们获得自我实现的途径,自我是实现使他无穷快乐。这世界就是一拨人在昼夜不停的高速运转,另一拨人起床发现世界变了。\n> \n> 我们大部分人的工作和生活状态是怎样的呢?\n> 上班稍微努力点就开始讲究公平,自己不得志就开始抱怨公司和领导,下班后看几页书就觉得自己特别上进,辛苦上几天就觉得自己要赶紧去享受一下生活了,加几天班就担心自己会过劳死。\n> 遇到些鸡毛蒜皮的小事儿就郁郁寡欢,仿佛遇到了天大的人生难题磨磨唧唧解决不清。看见牛逼的人也会心生羡慕,但总也突破不了努力却总不得要领魔咒。\n> \n> 越能干,越努力;越有钱,越上进\n> 而造成这一切的差异,最主要的来自人的精神内核。\n> \n> 你要用牛人的标准要求自己,不断的走到牛人当中去,拉近和牛人之间的距离。当你觉得自己能够成为他们中的一员的时候,你才能成为了真正的牛人。\n\n成功?我才刚上路呢。\n\n-- EOF --\n","tags":["thinking"]},{"title":"Ubuntu JDK Nginx WildFly MySQL 环境配置","url":"/2016/07/07/ubuntu-jdk-nginx-wildfly-mysql-config/","content":"\n新项目部署上线,主要参考 [世雷博客](http://blog.csdn.net/hanshileiai) 的内容,自己也总结了下。从 JDK 安装、Web 容器、数据库,都有涉及比较全面。\n\n## JDK8\n\n### 安装 JDK8\n\n1、添加软件源\n\n```bash\nsudo add-apt-repository ppa:webupd8team/java\n```\n\n2、更新软件源\n\n```bash\nsudo apt-get update\n```\n\n3、安装 jdk1.8\n\n```bash\nsudo apt-get install oracle-java8-installer\n```\n\n### 查看 Java 安装路径\n\n```bash\nsudo update-alternatives --config java\nsudo update-alternatives --config javac\n```\n\n### 查看 Java 安装后的版本\n\n```bash\njava -version\n```\n\n### (扩展)增加多版本 JDK 和切换方法\n\n1、安装 JDK 6 和 JDK 7\n\n```bash\nsudo apt-get install oracle-java6-installer\nsudo apt-get install oracle-java7-installer\n```\n\n2、查看所有 JDK 安装版本\n\n```bash\nsudo update-java-alternatives -l\njava-6-oracle 3 /usr/lib/jvm/java-6-oracle\njava-7-oracle 4 /usr/lib/jvm/java-7-oracle\njava-8-oracle 2 /usr/lib/jvm/java-8-oracle\n```\n\n3、通过 `-s` 参数可以方便的切换到其它的 JDK 版本\n\n```bash\nsudo update-java-alternatives -s java-6-oracle\n```\n\n```bash\nsudo update-java-alternatives -s java-7-oracle\n```\n\n```bash\nsudo update-java-alternatives -s java-8-oracle\n```\n\n4、再次查看 JDK 版本\n\n```\njava -version\n```\n\n```\njava version \"1.6.0_45\"\nJava(TM) SE Runtime Environment (build 1.6.0_45-b06)\nJava HotSpot(TM) 64-Bit Server VM (build 20.45-b01, mixed mode)\n```\n\n<!-- more -->\n\n## Nginx\n\n### Nginx 安装\n\n1、更新软件源\n\n```\nsudo apt-get update\n```\n\n2、安装 Nginx\n\n```\nsudo apt-get install nginx\n```\n\n3、查看 Nginx 位置\n\n```\nwhereis nginx\n```\n\n### Nginx 禁止外网访问\n\n防止 Google 收录\n\n```\nsudo vi /etc/nginx/sites-enabled/default\n```\n\n在 `server` 中添加\n\n```\n# server add\n# 公司代理IP\nallow *.*.*.*;\ndeny all;\n```\n\n### Nginx 域名调整\n\n域名 `www` 跳转 `non www`,`server` 中添加配置\n\n```\nserver_name www.aaa.org aaa.org;\nif ($host != 'aaa.org'){\n\trewrite ^/(.*)$ http://aaa.org/$1 permanent;\n}\n```\n\n### Nginx GZip\n\n```\n##\n# Gzip Settings\n##\n\ngzip on;\ngzip_disable \"msie6\";\n\ngzip_vary on;\ngzip_min_length 1k;\ngzip_proxied any;\ngzip_comp_level 6;\ngzip_buffers 16 8k;\ngzip_http_version 1.1;\ngzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/x-httpd-php image/jpeg image/gif image/png image/x-icon\n```\n\n### Nginx 静态文件缓存\n\n```\nlocation ~*\\.(gif|jpg|jpeg|png|bmp|swf|woff|icon)$ {\n\tproxy_cache my_zone;\n\tproxy_cache_bypass $http_cache_control;\n\tproxy_cache_valid 200 1d;\n\tadd_header X-Proxy-Cache $upstream_cache_status;\n\texpires 15d;\n}\n\nlocation ~ .*\\.(js|css|ttf)$ {\n\tproxy_cache my_zone;\n\tproxy_cache_bypass $http_cache_control;\n\tproxy_cache_valid 200 1d;\n\tadd_header X-Proxy-Cache $upstream_cache_status;\n\texpires 15d;\n}\n```\n\n静态文件放在配置的 `root` 下\n\n```\nroot /opt/***;\n```\n\n还有个技巧,项目中大量资源文件,例如 PDF,在设计访问 url 时可以 `**/pdf/**`,这样在 Nginx 进行配置就可以将文件分离出项目;这些文件也放在 `root` 路径下。\n\n```\nlocation ^~ /html/ {\n}\nlocation ^~ /pdf/ {\n}\n```\n\n### Nginx Commands\n\n```\nsudo service nginx stop\nsudo service nginx start\nsudo service nginx restart\n```\n\n### Nginx Show Log\n\n```\nsudo tail -f /var/log/nginx/error.log\n```\n\n## WildFly 10.0.0.Final\n\n### WildFly 安装\n\n1、下载 WildFly,并提取到 /opt 目录 WildFly 10.0.0.Final [下载地址](http://wildfly.org/downloads/)\n\n```\ncd /opt\nsudo wget -c http://download.jboss.org/wildfly/10.0.0.Final/wildfly-10.0.0.Final.tar.gz\nsudo tar -xzvf wildfly-10.0.0.Final.tar.gz\n```\n\n2、创建 WildFly 用户和组\n\n```\nsudo addgroup wildfly\nsudo useradd -g wildfly wildfly\n```\n\n改变 wildfly 文件夹的所有权:\n\n```\nsudo chown -R wildfly:wildfly /opt/wildfly-10.0.0.Final\n```\n\n创建一个链接映射(好处:如果你改变 WildFly 版本,不需要更新其他配置)\n\n```\nsudo ln -s wildfly-10.0.0.Final /opt/wildfly\n```\n\n3、安装 init.d 脚本\n设置并使用 init.d 脚本来启动和停止 WildFly。复制`/opt/wildfly/bin/init.d/wildfly-init-debian.sh`脚本到 `/etc/init.d/wildfly`,更改权限,并使其可执行\n\n```\nsudo cp /opt/wildfly/docs/contrib/scripts/init.d/wildfly-init-debian.sh /etc/init.d/wildfly\nsudo chown root:root /etc/init.d/wildfly\nsudo chmod ug+x /etc/init.d/wildfly\n```\n\n启动/停止 WildFly 命令\n\n```\nsudo /etc/init.d/wildfly start\nsudo /etc/init.d/wildfly stop\n```\n\n4、WildFly 做为系统服务,开机启动\n\n```\nsudo update-rc.d wildfly defaults\n```\n\n### 配置 WildFly 允许所有 ip 访问\n\n1、打开配置文件 `standalone.xml`\n\n```\nsudo vi /opt/wildfly/standalone/configuration/standalone.xml\n```\n\n2、替换此处\n\n```\n<interface name=\"management\">\n <inet-address value=\"${jboss.bind.address.management:127.0.0.1}\"/>\n</interface>\n<interface name=\"public\">\n <inet-address value=\"${jboss.bind.address:0.0.0.0}\"/>\n</interface>\n```\n\n改为\n\n```\n<interface name=\"management\">\n <any-address/>\n</interface>\n<interface name=\"public\">\n <any-address/>\n</interface>\n```\n\n3、保存后,重新启动 WildFily\n\n```\nsudo service wildfly restart\n```\n\n### 删除默认欢迎内容(可选)\n\n如果你部署了应用程序在上下文根目录里,欢迎你 将需要从 WildFly 配置删除默认内容。在 standalone.xml 文件里删除粗体突出显示的行\n\n```\n<server name=\"default-server\">\n <http-listener name=\"default\" socket-binding=\"http\"/>\n <host name=\"default-host\" alias=\"localhost\">\n **<!-- <location name=\"/\" handler=\"welcome-content\"/> -->**\n <filter-ref name=\"server-header\"/>\n <filter-ref name=\"x-powered-by-header\"/>\n </host>\n</server>\n<handlers>\n **<!-- <file name=\"welcome-content\" path=\"${jboss.home.dir}/welcome-content\"/> -->**\n</handlers>\n```\n\n### 其它设置\n\n改为可以修改 JSP 页面不用重启\n\n```\n<servlet-container name=\"default\">\n <jsp-config development=\"true\"/>\n</servlet-container>\n```\n\n### 注意事项\n\n1、项目以站点根目录访问\n你现在可以将应用程序部署到 WildFly 视图在 your_ip:8080\n在你的项目目录 WEB-INF 下添加 jboss-web.xml,确保你的配置 context-root 设置为 /\n\n```\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<jboss-web>\n <context-root>/</context-root>\n</jboss-web>\n```\n\n2、Linux 里设置端口 80 到 8080\n**注意:在之后的配置会使用 Nginx 反向代理,所用 WildFly 端口不用映射为 80,这里只是个方法的笔记**\n\n注意,在 linux 里,由于内核的限制,普通用户不能使用 1024 一下的端口。所以在配置文件(standalone.xml)里改成 80,用普通用户是启动不了的。\n\n此时,我们需要在 linux 下使用 root 用户运行一个命令,使访问 80 端口的应用转到 8080 上:\n\n```\nsudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 8080\n```\n\n以上端口转发为临时操作,重启 linux 服务器后失效。如果要重启服务器不丢失 FORWARD 转发”操作,可写入配置。\n\n> 在 Linux 的下面部署了 tomcat,为了安全我们使用非 root 用户进行启动,但是在域名绑定时无法直接访问 80 端口号。众所周知,在 unix 下,非 root 用户不能监听 1024 以上的端口号,这个 tomcat 服务器就没办法绑定在 80 端口下。所以这里需要使用 linux 的端口转发机制,把到 80 端口的服务请求都转到 8080 端口上。\n\n2.1、安装 iptables-persistent\n\n```\nsudo apt-get update\nsudo apt-get install iptables-persistent\n```\n\n2.2、添加 80 端口跳转到 8080 规则\n\n```\nsudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080\n```\n\n2.3、保存跳转规则\n\n```\nsudo service iptables-persistent save\n```\n\n3、wildfly 不支持 struts2 的配置文件(.xml)里用通配符\n**这是原博文的内容,我没有使用 struts2;在 spring 中有用通配符,但是好像没什么影响**\njboss wildfly 不支持 struts2 配置文件里用通配符 `*.xml`,如下:\n\n```\n <!-- <include file=\"struts/*.xml\"></include> -->\n <include file=\"struts/struts_post.xml\"></include>\n <include file=\"struts/struts_user.xml\"></include>\n```\n\n4、增加部署扫描仪的超时设置\n位置:\n\n```\n<subsystem xmlns=\"urn:jboss:domain:deployment-scanner:2.0\">\n <deployment-scanner path=\"deployments\" relative-to=\"jboss.server.base.dir\" scan-interval=\"5000\" />\n</subsystem>\n```\n\n`<deployment-scanner>` 内增加属性 `deployment-timeout=\"1200\"` 如下:\n\n```\n<subsystem xmlns=\"urn:jboss:domain:deployment-scanner:2.0\">\n <deployment-scanner path=\"deployments\" relative-to=\"jboss.server.base.dir\" scan-interval=\"5000\" deployment-timeout=\"1200\" />\n</subsystem>\n```\n\n### Nginx 反向代理 WildFly\n\n1、在 `http` 中添加\n\n```\nupstream jboss {\n\tserver 127.0.0.1:8080;\n}\n```\n\n2、在 `server` 中修改\n\n```\nlocation /{\n\tproxy_pass http://jboss;\n\tproxy_redirect off;\n\tproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n\tproxy_set_header X-Real-IP $remote_addr;\n\tproxy_set_header Host $http_host;\n\tindex index.html index.htm index.jsp;\n\t# 动态网站要小心这个 缓存 选项\n\tadd_header Cache-Control max-age=1728000;\n}\n```\n\n## MySQL\n\n### MySQL5.6 安装\n\n```bash\nsudo apt-get install mysql-server-5.6\n```\n\n### MySQL 创建数据库\n\n```bash\nCREATE DATABASE IF NOT EXISTS scrapy DEFAULT CHARSET utf8 COLLATE utf8_general_ci;\n```\n\n### MySQL 数据库导出/导入\n\n```bash\nmysqldump -u root -p123456 db_name | gzip > /home/backup/db_name.sql.gz\nmysqldump -u root -p123456 --all-databases | gzip > /home/backup/all.sql.gz\n\nmysqldump -h192.168.1.100 -uroot -p db_name > db_name.sql\nmysqldump -uroot -p db_name_1 db_name_2 > db_name_1_and_2.sql\n```\n\n```bash\ngunzip < db_name.sql.gz | mysql -u root -p123456 db_name\ngunzip < all.sql.gz | mysql -u root -p123456\n\nmysql -uroot -p\nshow databases;\nuse db_name;\nsource ~/db_name.sql\n```\n\n### MySQL 查看表结构\n\n```sql\ndesc user\n```\n\n## Project\n\n### robots.txt config 禁止所有爬虫爬取\n\n```\nUser-agent: *\nDisallow: /\n```\n\n## Renferences\n\n- [韩世雷-ubuntu 配置 java jdk1.8 环境,增加多版本 jdk 和切换方法](http://blog.csdn.net/hanshileiai/article/details/46968275)\n- [韩世雷-ubuntu14.04 Terminal 配置 wildfly-10.0.0.Final 服务器](http://blog.csdn.net/hanshileiai/article/details/46987859)\n- [韩世雷-Ubuntu14.04 配置 iptables 把 80 端口转到 8080](http://blog.csdn.net/hanshileiai/article/details/47757217)\n","tags":["java"]},{"title":"GitHub Webhook 自动部署 Hexo","url":"/2016/07/01/github-webhook-example/","content":"\n在 [GitHub Pages 不被百度收录解决方案](/2016/06/30/github-pages-forbidden-baiduspide-solution/) 中,思路二是通过 Dnspod 的智能 DNS 服务。简而言之就是搭建一个 Server,做一个 Blog 的镜像站,专为百度收录使用。\n\n但是每次将新建的博客文章 `PUSH` 到 GitHub 后,还要再登陆 Server `PULL` 一下,简直是太蠢了。那有什么解决办法吗?答:GitHub Webhook。\n\n<!-- more -->\n\n## Webhook\n\nWebhook,也就是人们常说的钩子,是一个很有用的工具。你可以通过定制 Webhook 来监测你在 Github.com 上的各种事件,最常见的莫过于 push 事件。\n\n如果你设置了一个监测 push 事件的 Webhook,那么每当你的这个项目有了任何提交,这个 Webhook 都会被触发,这时 Github 就会发送一个 HTTP POST 请求到你配置好的地址。\n\n如此一来,你就可以通过这种方式去自动完成一些重复性工作;比如,你可以用 Webhook 来自动触发一些持续集成(CI)工具的运作,比如 Travis CI;又或者是通过 Webhook 去部署你的线上服务器。\n\nGithub 开发者平台的文档中对 Webhook 的所能做的事是这样描述的:\n\n> You’re only limited by your imagination.\n\n## 响应 Webhook\n\n在参考文章里博主是使用 Node.js 编写的服务端响应代码,但考虑到自己对 Node.js 不熟悉,还要部署环境,所以改用 Python 语言编写响应代码。\n\n自己在 GitHub 搜索下 `github webhook`,`language` 选择 `Python` 便找到了 Python 编写的:[razius/github-webhook-handler](https://github.com/razius/github-webhook-handler)\n\n自己的 VPS 是在 [Bandwagon Host](https://bandwagonhost.com/aff.php?aff=5403) 上购买的,最合适的配置:\n\n- Self-managed service\n- SSD: 10 GB\n- RAM: 512 MB\n- CPU: 1x Intel Xeon\n- BW: 1000 GB/mo\n- Link speed: 1 Gigabit\n- VPS technology: OpenVZ/KiwiVM\n- Linux OS: 32-bit and 64-bit Centos, Debian, Ubuntu, Fedora\n\n费用:\n\n- \\$2.99 USD Monthly\n- \\$7.99 USD Quarterly\n- \\$12.99 USD Semi-Annually\n- \\$19.99 USD Annually\n\nBandwagon Host 支持支付宝付款很是方便,优惠码:`IAMSMART52J3NC` 再打个 9.5 折左右。这配置有时会 `(out of stock)` 等等随缘会有的。\n\n### 安装 Git\n\n```\napt-get update\napt-get install git\n```\n\n### clone [razius/github-webhook-handler](https://github.com/razius/github-webhook-handler)\n\n```bash\ngit clone https://github.com/razius/github-webhook-handler.git\n```\n\n根据 github-webhook-handler Gettings started:\n\n#### Installation Requirements\n\nInstall dependencies found in requirements.txt\n\n```bash\npip install -r requirements.txt\n```\n\n#### Repository Configuration\n\nEdit repos.json to configure repositories, each repository must be registered under the form `GITHUB_USER/REPOSITORY_NAME`.\n\n```\n{\n \"razius/puppet\": {\n \"path\": \"/home/puppet\",\n \"key\": \"MyVerySecretKey\",\n \"action\": [[\"git\", \"pull\", \"origin\", \"master\"]]\n },\n \"d3non/somerandomexample/branch:live\": {\n \"path\": \"/home/exampleapp\",\n \"key\": \"MyVerySecretKey\",\n \"action\": [[\"git\", \"pull\", \"origin\", \"live\"],\n [\"echo\", \"execute\", \"some\", \"commands\", \"...\"]]\n }\n}\n```\n\n过程如果出现:`pip: command not found` 可以执行:\n\n```\nsudo apt-get install python-pip\n```\n\n#### Set environment variable for the repos.json config\n\n```\nexport REPOS_JSON_PATH=/path/to/repos.json\n```\n\n#### Start the server\n\n```\npython index.py 80\n```\n\n#### clone and pull your Blog\n\n```\ngit clone [email protected]:imzyf/imzyf.github.io.git\ngit pull origin master\n```\n\n过程如果出现:`Permission denied (publickey).` 可以参考:\n\n- [Stack Overflow-Git - Permission denied (publickey)](http://stackoverflow.com/questions/2643502/git-permission-denied-publickey)\n\n```\ncd ~/.ssh && ssh-keygen\ncat id_rsa.pub | xclip\n```\n\n然后添加到你的 GitHub 账户:Settings -> SSH keys 中。\n\n### config imzyf.github.io Webhooks\n\n首先进入你的 repo 主页,通过点击页面上的按钮 [settings] -> [Webhooks & service] 进入 Webhooks 配置主页面。也可以通过下面这个链接直接进入配置页面:\n\n```\nhttps://github.com/[ 用户名 ]/[ 仓库名称 ]/settings/hooks\n```\n\n此处只需要配置 Webhook 所发出的 POST 请求发往何处即可,于是我们就配置我们所需要的路径:\n[http://104.236.xxx.xxx:9988/deploy/]()。这个地址指向的就是那个能够响应 Webhook 所发出请求的服务器。\n\n配置好 Webhook 后,Github 会发送一个 ping 来测试这个地址。如果成功了,那么这个 Webhook 前就会加上一个绿色的勾;如果你得到的是一个红色的叉,那就好好检查一下哪儿出问题了吧!\n\n### config Nginx server\n\n## 最后想说的\n\n为了一个百度收录大可不必这么麻烦,刚刚查到这些解决方案时心里也是烦的、虚的,但还是硬的头皮搞了。过程中已经不单单是为了百度收录自己的博客,而是变成学习了东西、思考问题。这些解决方案网上都有,也不是自己创造的,但是别人的东西自己不尝试,就还是别人的。\n\n现在百度已经收录我的博客了,imzyf.github.io 到 Server 也是自动部署的。解决问题后的快乐和信心,才是我这次最大的收获。\n\n## References\n\n- [jerryzou-Webhook 实践 —— 自动部署](http://jerryzou.com/posts/webhook-practice/)\n","tags":["git","github","hexo"]},{"title":"GitHub Pages 不被百度收录解决方案","url":"/2016/06/30/github-pages-forbidden-baiduspide-solution/","content":"\n2019-12-02 更新:现在我的 Blog 还是用的 GitHub Pages,反正没有备案的域名不会被百度收录。\n\n2017-04-22 更新:现在我的 Blog 使用的是 [UFOVPS](https://www.ufovps.com/) 直接部署的。\n\n---\n\n在 [使用 Hexo 和 Github 搭建个人独立博客](/2016/06/24/hexo-github-blog/) 几天后,发现百度并不对博客进行收录。\n\n在天朝使用百度搜索毕竟多数,使用百度站长工具-抓取诊断,在百度 Spider 抓取结果返回 HTTP 头:HTTP/1.1 403 Forbidden,原来是 GitHub 禁止了百度爬虫的爬去。\n\n<!-- more -->\n\nGoogle 后早已有许多热心网友给出了解决方案,自己在这里总结下。\n\n## 思路一:利用 CDN 解决百度爬虫被 Github Pages 拒绝的问题\n\n### 解决思路\n\n既然 Github 彻底和百度决裂了,那我们也只能自己动手来解决了。Github 可能是封了百度的 IP,也有可能是封了百度爬虫的 User-Agent。\n\n所以要解决这个问题,最好就不要让百度爬虫直接访问 Github 了,需要在中间套一层 [反向代理](https://zh.wikipedia.org/wiki/%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86)。\n\n那么问题又来了,既然我可以搭一个反向代理服务器了,那我为什么不直接把博客放在这台服务器上?放 Github Pages 上不就是为了少一台服务器,少一点费用吗?\n\n那有没有免费的第三方反向代理服务呢?当然有,其实现在各种 **CDN 服务** 不就是吗?而且还额外提供了各种网络环境下的加速功能。\n\n但是使用 CDN 也会有一个非常大的缺点:只能对 **静态资源** 做,因为 CDN 和反向代理有一个很大的不同就是:它会做缓存,并向各个节点分发。\n\n所以 CDN 一般都是用来给静态资源做加速的。如果你对动态页面做加速,用户看到的页面在一段时间内就一直不会变了。但是我们不怕!因为 Github Pages 本来就是全静态的!\n\n国内提供 CDN 服务的有:加速乐、七牛云存储、又拍云等。\n\n最后选择了 [**又拍云**](https://www.upyun.com/index.html)\n\nCDN 回源配置\n\n有设置缓存时间的功能,上面明确写了:CDN 缓存时间是指 UPYUN 回源取得文件后,文件在 UPYUN CDN 网络中的缓存的时间,超过缓存时间则重新回源获取文件。\n\n是的,这才是真正满足我需求的功能!\n\n这个解决方案我尝试了,但是在 CDN 域名配置时,有天朝特色要求域名必须备案,我的域名 `zyf.im` 是在 [name.com](http://www.name.com) 上注册的,`.im` 的尾缀也无法在国内备案。所以这个方案不适合我。\n\n## 思路二:通过 Dnspod 的智能 DNS 服务\n\n在这里博主首先分析了:使用 CDN 可能 **无法** 真正解决百度被拒绝的问题。理由是:**CDN 节点众多,百度爬取的节点可能并没有页面的缓存**。\n\n通过 Dnspod 的智能 DNS 服务可以变相解决这个问题,大概的设置如下图所示,只要为你所有的主机记录重复添加 A 记录,把线路类型设置为百度,并将记录值指向你自己的云主机即可。\n\n你说什么?等下!自己的云主机?没错,其实这种方式就是 **专门为百度的爬虫增开了一个小窗口**,使得它可以在你自己的服务器上爬取内容,而不是直接去爬取 Github Pages 的内容。你需要自己搭个服务器,并将你的静态网站架在上面。\n\n我博客解决方案就是采用这种方法。看起来有些“笨”因为我又搭建了一个 Server 用于博客,相当于没有完全利用 GitHub Pages,实际上普通用户访问我的博客就是有利用 GitHub 的 CDN 服务的,更快一些。而且我这么做还利用了 GitHub Webhook,当我把新的页面提交到 GitHub 时,我的博客服务器会自动 `pull` 最新的项目,这么看也就不那么“傻”了。\n\n请参看:[GitHub Webhook 自动部署 Hexo](/2016/06/30/github-webhook-example/)\n\n经过这么一配置,我的博客现在已经被百度收录了。百度搜索:`site:zyf.im`\n\n## References\n\n- [DOZER-利用 CDN 解决百度爬虫被 Github Pages 拒绝的问题](http://www.dozer.cc/2015/06/github-pages-and-cdn.html)\n- [jerryzou-解决 Github Pages 禁止百度爬虫的方法与可行性分析](http://jerryzou.com/posts/feasibility-of-allowing-baiduSpider-for-Github-Pages/)\n","tags":["git","github","hexo"]},{"title":"Eclipse Maven Spring SpringMVC Mybatis 整合","url":"/2016/06/27/eclipse-maven-spring-springmvc-mybatis-example/","content":"\n新项目自己撘框架,想着用点新的。看慕课网 [Java 高并发秒杀 API](http://www.imooc.com/index/search?words=%E7%A7%92%E6%9D%80) 的系列课程时很受益。所以想着仿着来使用:Mavan-Spring-SpringMVC-Mybatis 的架构。框架整合的代码我已上传到我的 Github:[maven-mybatis-spring-springmvc](https://github.com/imzyf/maven-mybatis-spring-springmvc)。\n\n本示例是在:Ubuntu15 上实现的;Windows 上安装 Maven 将不太相同。\n\n## Maven Install\n\n> 2016-09-10 更新:较新版 Eclipse 都有集成 Maven,所以并不需要安装\n\n1. Run command `sudo apt-get install maven`, to install the latest Apache Maven.\n2. Run command `mvn -version to verify` your installation.\n3. Where is Maven installed?\n The command `apt-get` install the Maven in `/usr/share/maven`\n The Maven configuration files are stored in `/etc/maven`\n\n## Eclipse Maven Plugin - m2e\n\n> 2016-09-10 更新:较新版 Eclipse 都有集成 Maven,所以并不需要安装\n\n1. open Eclipse -> Help -> click \"Install New Software\" -> click \"add\"\n\n```txt\nName:m2e\nLocation:http://download.eclipse.org/technology/m2e/releases\n```\n\n2. click \"ok\" -> click \"Maven Integration for Eclipse\" -> click \"Next\"\n3. restrat Eclipse\n4. config m2e -> Window -> Preferences -> Maven -> Installations -> click \"Add…\" -> select Maven\n\n<!-- more -->\n\n## Create a Maven Project\n\n1. File -> New -> New Maven project\n2. select \"Use default Workspace location\"\n3. select \"maven-archetype-j2ee-simple\"\n4. input info -> Finish\n5. 选中项目右键菜单中选择 Properties -> Project Facets -> select \"Dynamic Web Module\" Version \"3.1\"\n\nTips:\n\n- 如果在 `Project Facets` 选择版本时“can not change”,可以在项目目录下手动修改 `.settings/org.eclipse.wst.common.project.facet.core.xml` 文件配置\n- 项目自动生成的 `web.xml` 版本较低,手动修改\n\n```xml\n<web-app xmlns=\"http://xmlns.jcp.org/xml/ns/javaee\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://xmlns.jcp.org/xml/ns/javaee\n http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd\"\n\tversion=\"3.1\" metadata-complete=\"true\">\n</web-app>\n```\n\n- 项目结构\n\n```\n├── src\n ├── main\n | ├── java //java源代码\n | ├── resources //配置资源文件\n | └── webapp //web文件\n |\n └── test\n\t └── java //junit测试\n```\n\n## pom.xml Config\n\n[Github-maven-mybatis-spring-springmvc pom.xml](https://github.com/imzyf/maven-mybatis-spring-springmvc/blob/master/pom.xml)\n\n```xml\n<!-- junit4 -->\n<dependency>\n\t<groupId>junit</groupId>\n\t<artifactId>junit</artifactId>\n\t<version>4.11</version>\n\t<scope>test</scope>\n</dependency>\n<!-- 日志 -->\n<!-- 实现slf4j接口整合 -->\n<!-- JDBC MySQL Driver -->\n<!-- DAO框架 mybatis -->\n<!-- Servlet API -->\n<!-- 1 Spring 核心依赖 -->\n<!-- 2 Spring DAO依赖 -->\n<!-- 3 Spring web相关依赖 -->\n<!-- 4 Spring Test相关依赖 -->\n```\n\n## logback.xml Config\n\n```xml\n<configuration>\n\t<appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n\t\t<!-- encoders are assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder\n\t\t\tby default -->\n\t\t<encoder>\n\t\t\t<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n\n\t\t\t</pattern>\n\t\t</encoder>\n\t</appender>\n\t<root level=\"info\">\n\t\t<appender-ref ref=\"STDOUT\" />\n\t</root>\n</configuration>\n```\n\n## Mybatis Config\n\n[Github-maven-mybatis-spring-springmvc mybatis-config.xml](https://github.com/imzyf/maven-mybatis-spring-springmvc/blob/master/src/main/resources/mybatis-config.xml)\n\n```xml\n<configuration>\n\t<settings>\n\t\t<!-- 使用jdbc的getGeneratedKays 获取数据库自增主键 -->\n\t\t<setting name=\"useGeneratedKeys\" value=\"true\" />\n\t\t<!-- 使用列别名替换列名 -->\n\t\t<setting name=\"useColumnLabel\" value=\"true\" />\n\t\t<!-- 是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射。 -->\n\t\t<setting name=\"mapUnderscoreToCamelCase\" value=\"true\" />\n\t</settings>\n</configuration>\n```\n\n## Spring Config\n\n[Github-maven-mybatis-spring-springmvc spring](https://github.com/imzyf/maven-mybatis-spring-springmvc/tree/master/src/main/resources/spring)\n\n### Spring-DAO Config\n\n```xml\n<!-- 1 数据库配置文件位置 -->\n<context:property-placeholder location=\"classpath:jdbc.properties\" />\n\n<!-- 2 数据库连接池 -->\n<!-- Employee DB data source. -->\n<bean id=\"dataSource\" class=\"com.mchange.v2.c3p0.ComboPooledDataSource\">\n\n\t<property name=\"driverClass\" value=\"${jdbc.driverClassName}\" />\n\t<property name=\"jdbcUrl\" value=\"${jdbc.dburl}\" />\n\t<property name=\"user\" value=\"${jdbc.username}\" />\n\t<property name=\"password\" value=\"${jdbc.password}\" />\n\n\t<!-- c3p0连接池 私有属性 -->\n\t<property name=\"maxPoolSize\" value=\"${jdbc.maxPoolSize}\" />\n\t<property name=\"minPoolSize\" value=\"${jdbc.minPoolSize}\" />\n\t<!-- 关闭连接后不自动commit -->\n\t<property name=\"autoCommitOnClose\" value=\"false\" />\n\t<!-- 获取连接超时时间 -->\n\t<property name=\"checkoutTimeout\" value=\"1000\" />\n\t<!-- 获取连接失败重试次数 -->\n\t<property name=\"acquireRetryAttempts\" value=\"2\" />\n</bean>\n\n<!-- 设计原则:约定大于配置 -->\n\n<!-- 3 配置 SqlSessionFactory 对象 -->\n<bean id=\"sqlSessionFactory\" class=\"org.mybatis.spring.SqlSessionFactoryBean\">\n\t<!-- 注入数据库连接池 -->\n\t<property name=\"dataSource\" ref=\"dataSource\" />\n\t<!-- 配置mybitis 全局配置文件 -->\n\t<property name=\"configLocation\" value=\"classpath:mybatis-config.xml\" />\n\t<!-- 扫描entity包 使用别名 -->\n\t<property name=\"typeAliasesPackage\" value=\"com.moma.dmv.entity\" />\n\t<!-- 扫描sql配置文件 mapper 需要的xml -->\n\t<property name=\"mapperLocations\" value=\"classpath:mapper/*.xml\" />\n</bean>\n\n<!-- 4 配置扫描Dao接口包,动态实现Dao接口,注入到spring容器中 -->\n<bean class=\"org.mybatis.spring.mapper.MapperScannerConfigurer\">\n\t<!-- 注入sqlsessionFactory -->\n\t<property name=\"sqlSessionFactoryBeanName\" value=\"sqlSessionFactory\" />\n\t<!-- 给出需要扫描Dao接口包 -->\n\t<property name=\"basePackage\" value=\"com.moma.dmv.dao\" />\n</bean>\n```\n\n### Spring-Service Config\n\n```xml\n<!-- 扫描service包下 所有使用注解的类型 -->\n<context:component-scan base-package=\"com.moma.dmv.service\" />\n\n<!-- 配置事务管理器 -->\n<bean id=\"transactionManager\"\n\tclass=\"org.springframework.jdbc.datasource.DataSourceTransactionManager\">\n\t<property name=\"dataSource\" ref=\"dataSource\" />\n</bean>\n\n<!-- 配置基于注解的声明式事务 -->\n<tx:annotation-driven transaction-manager=\"transactionManager\" />\n\n<!-- 使用注解控制事务方法的优点\n1:开发团队达成一致约定,明确标注事务方法的编程风格\n2:保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部\n3:不是所有的方法都需要事务,比如只有一条修改操作,只读操作不需要事务控制\n-->\n```\n\n### Spring-Web Config\n\n```xml\n<!-- 1:开启springMVC 注解模式 -->\n<mvc:annotation-driven />\n\n<!-- 2 静态资源默认servlet配置 1 加入对静态资源的处理 js gif png 2 允许使用“/”做整体映射 -->\n<mvc:default-servlet-handler />\n\n<!-- 3:配置jsp 显示ViewResolver -->\n<bean\n\tclass=\"org.springframework.web.servlet.view.InternalResourceViewResolver\">\n\t<!-- 决定视图类型,如果添加了jstl支持(即有jstl.jar),那么默认就是解析为jstl视图 -->\n\t<property name=\"viewClass\"\n\t\tvalue=\"org.springframework.web.servlet.view.JstlView\" />\n\t<!-- 视图前缀 -->\n\t<property name=\"prefix\" value=\"/WEB-INF/jsp/\" />\n\t<!-- 视图后缀 -->\n\t<property name=\"suffix\" value=\".jsp\" />\n</bean>\n\n<mvc:resources location=\"/resources/\" mapping=\"/resources/**\" />\n\n<!-- 4:扫描web相关的bean -->\n<context:component-scan base-package=\"com.moma.dmv.web\" />\n```\n\n## DAO Mapper Example\n\n```xml\n<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\"\n\"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n<mapper namespace=\"com.moma.dmv.dao.InfoDao\">\n\n \t<select id=\"queryById\" resultType=\"Info\" parameterType=\"long\">\n\t\t<![CDATA[\n\t\tselect id,`key`,`value` from info where id = #{id}\n\t\t]]>\n\t</select>\n\n\t<select id=\"queryAll\" resultType=\"Info\">\n\t\t<![CDATA[\n\t\tselect id,key,value\n\t\tfrom info\n\t\tlimit #{offset},#{limit}\n\t\t]]>\n\t</select>\n\n</mapper>\n```\n\n## web.xml Config\n\n```xml\n<servlet>\n\t<servlet-name>dmv-dispatcher</servlet-name>\n\t<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>\n\t<!-- 配置springMVC需要加载的配置文件 spring-dao.xml spring-service.xml spring-web.xml\n\t\tmybatis -> spring -> springMVC -->\n\t<init-param>\n\t\t<param-name>contextConfigLocation</param-name>\n\t\t<param-value>classpath:spring/spring-*.xml</param-value>\n\t</init-param>\n</servlet>\n<servlet-mapping>\n\t<servlet-name>dmv-dispatcher</servlet-name>\n\t<!-- 默认匹配所有的请求 -->\n\t<url-pattern>/</url-pattern>\n</servlet-mapping>\n```\n\n## JUnit Example\n\n```java\nimport javax.annotation.Resource;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.test.context.ContextConfiguration;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\n\nimport com.moma.dmv.dao.InfoDao;\nimport com.moma.dmv.entity.Info;\n\nRunWith(SpringJUnit4ClassRunner.class)\nContextConfiguration(locations = { \"classpath:spring/spring-dao.xml\" })\npublic class InfoDaoTest {\n\n\t@Resource\n\tprivate InfoDao infoDao;\n\tprivate Logger logger = LoggerFactory.getLogger(this.getClass());\n\n\t@Test\n\tpublic void testQueryById() throws Exception {\n\t\tlong id = 1;\n\t\tInfo info = infoDao.queryById(id);\n\t\tlogger.info(info.toString());\n\t}\n}\n```\n\n## References\n\n- [MKyong-How to install Maven on Ubuntu](http://www.mkyong.com/maven/how-to-install-maven-in-ubuntu/)\n- [Java 之道-使用 Eclipse 构建 Maven 项目 (step-by-step)](http://blog.csdn.net/qjyong/article/details/9098213)\n","tags":["java"]},{"title":"使用 GitHub 和 Hexo 搭建个人独立博客","url":"/2016/06/24/hexo-github-blog/","content":"\n`Wordpress` 这类博客系统功能强大,可对与我只想划拉的写点东西的人,感觉大材小用了。而且 `Wordpress` 需要部署,网站的服务器也会带来问题,国内的服务器首先需要备案,费用不低,国外服务器访问速度受影响。\n\n近来接触到一种新的博客系统 Hexo,它的不同地方就是将:**在上线编写博客和页面渲染的过程在线下完成**。\n\n在本地编写博文的 Markdown 文件,使用 Hexo 将博客网站的所有前台 HTML 等全部生成,让后将生成的文件上传的服务器就行了。\n\n那么原来 wp 中的评论等动态功能怎么办呢?放心第三方服务商早已为我们考虑了。例如:[disqus](https://disqus.com/)就是一家第三方社会化评论系统,主要为网站主提供评论托管服务。\n\n本文的操作的系统环境是 Ubuntu 15,Windows 下的搭建可触类旁通。\n\n## 了解 Hexo\n\n> A fast, simple & powerful blog framework\n\n[Hexo](https://hexo.io/) 是一个快速、简洁且高效的博客框架。Hexo 使用 Markdown(或其他渲染引擎)解析文章,在几秒内,即可利用靓丽的主题生成静态网页,[Hexo setup 官方文档](https://hexo.io/zh-cn/docs/setup.html)。\n\n<!--more-->\n\n## 安装 Git\n\n> [Download for Linux and Unix | git-scm](https://git-scm.com/download/linux)\n\n## 安装 Node.js\n\n> [Linux | Mac 安装 Node.js 与常见问题 | zyf.im](/2017/07/06/install-node-js-in-ubuntu-and-faq/)\n\n## 安装 Hexo\n\n所有必备的应用程序安装完成后,即可使用 npm 安装 Hexo:\n\n```bash\nnpm install -g hexo-cli\n```\n\n## 建站\n\n安装 Hexo 完成后,请执行下列命令,Hexo 将会在指定文件夹中新建所需要的文件:\n\n```bash\nhexo init <folder>\ncd <folder>\nnpm install\n```\n\n新建完成后,指定文件夹的目录如下:\n\n```txt\n├── _config.yml\n├── package.json\n├── scaffolds\n├── source\n| ├── _drafts\n| └── _posts\n└── themes\n```\n\n### 新建一篇文章\n\n```bash\nhexo new [layout] <title>\n```\n\n如果没有设置 layout 的话,默认使用 `_config.yml` 中的 default_layout 参数代替。如果标题包含空格的话,请使用引号括起来。\n\n### 生成静态文件\n\n```bash\nhexo generate\n\n# 可以简写为\n\nhexo g\n```\n\n### 启动服务器\n\n```bash\nhexo server\n\n# 可以简写为\n\nhexo s\n```\n\n网站会在 [http://localhost:4000](http://localhost:4000) 下启动。在服务器启动期间,Hexo 会监视文件变动并自动更新,您无须重启服务器。\n\n## 部署静态网页到 GitHub\n\n### 注册设置 GitHub\n\n1. 登录 GitHub,注册自定义用户名如:`imzyf`\n2. 在主页右下角创建 New repository,name 必须和用户名一致如:`imzyf.github.io`\n3. 等待 3 分钟左右,之后即可访问静态主页如:`https://imzyf.github.io`\n\n### 同步内容至 GitHub\n\n1. 在 Hexo 目录下 `git clone [email protected]:imzyf/imzyf.github.io.git`\n2. 将 `public` 文件下的所有文件拷贝到 `imzyf.github.io` 下\n3. `git add .` 增加当前子目录下所有更改过的文件至 index\n4. `git commit -m 'xxx'` 提交到本地\n5. `git push origin master` 将当前分支 push 到远程 master 分支\n6. 最后访问主页`http://imzyf.github.io` 观察效果\n\n## 绑定个人域名\n\n在 GitHub 项目页面,Settings -> GitHub Pages,Source 选择 master branch,Custom domain 填写自己的域名,同时勾选 Enforce HTTPS 让你的网址支持 HTTPS。\n\n在你的域名服务商那里,将填写的域名解析到:`<username>.github.io` 利于 `imzyf.github.io`。\n\n## References\n\n- [HellDog-使用 GitHub 和 Hexo 搭建免费静态 Blog](https://wsgzao.github.io/post/hexo-guide/)\n","tags":["git","github","hexo"]},{"title":"Ubuntu 下连接蓝牙键盘","url":"/2016/06/14/ubuntu-bluetooth-keyboard/","content":"\n新买了 `Filco Majestouch Convertible 2` 键盘。在自己的笔记本上连接没什么问题,搬到公司 Ubuntu 的 IBM 笔记本这么都连接不上,查找解决。\n\n安装蓝牙的 `hcidump`:\n\n```bash\nsudo apt-get install bluez-hcidump\n```\n\n然后,监测蓝牙事件\n\n```bash\nsudo hcidump -at\n```\n\n再次连接蓝牙键盘,可以看到输出事件中有一条 `Pin ...` 键盘输入对应的 Pin,Enter,连接成功。\n\n-- EOF --\n","tags":["ubuntu"]},{"title":"Java final 修饰符","url":"/2016/06/13/java-modifier-final/","content":"\n## final 修饰符\n\nfinal 修饰符表示不可变。类似 C 中的 constant。用于修饰变量表示不可变的变量。用于修饰方法表示不可被重写。用于修饰类表示不可被继承。\n\n## final 的成员变量\n\n成员变量随着类或者实例的初始化而初始化。在类初始化时,静态变量就会被分配内存并初始化。对于实例变量,系统会在实例初始化的时候初始化这些变量。\n\n由于成员变量会被系统隐式的初始化。如果程序员不显式的初始化它们,那他们会变成 0,false,null 这样的值。失去了意义。\n\n所以 **final 修饰的成员变量必须显式的初始化。**\n\n- 类变量:必须在静态初始化块,或者在声明的时候初始化\n- 实例变量:必须在非静态初始化块,声明时,或者构造函数中初始化。\n\n<!-- more -->\n\n## final 的局部变量\n\n对于形参:在方法内部无法对其进行赋值。\n方法体内的 final 局部变量:只能赋值一次。\nfinal 的基本类型变量和引用类型变量\n\n基本类型变量:值不可变。\n引用类型变量:指针不可变,但是指向的内存区域可变。\n\n```java\npublic class FinalVariableTest {\n\tpublic static void main(String[] agrs){\n\t\tfinal int[] arr = {1,23,4,5};\n\t\tSystem.out.println(Arrays.toString(arr));\n\t\tarr[2] = 20;\n System.out.print(Arrays.toString(arr));\n }\n}\n\n//Console\n//[1, 23, 4, 5]\n//[1, 23, 20, 5]\n```\n\n## final 的宏替换\n\n对于 final 的变量来说,不管是类变量,实例变量还是局部变量。只要在编译的时候可以确定,那么编译器就会把它看成一个直接量而不是变量。\n比如:\n\n```java\npublic class FinalVar{\n public static void main(String[] args){\n final int a =5;\n System.out.println(a);\n }\n}\n```\n\n实际编译器在执行 System.out.println(a);等同于执行 System.out.println(5);\n**什么叫做编译时可以确定**\n下面这个例子。\nstr1 在编译的时候就可以确定。而 str2 需要调用 valueOf() 方法才能得到后面一个字符串。在编译的时候无法确定。从结果也可以看出来。\n\n```java\nfinal String str1 = \"hello world\"+2016;\nfinal String str2 = \"hello world\"+String.valueOf(2016);\n\nSystem.out.println(str1==\"hello world2016\");\nSystem.out.println(str2==\"hello world2016\");\n```\n\n再看这个\n\n```java\nString str1 = \"hello world\";\nString str2 = \"hello \" + \"world\";\n\nString s1 = \"hello \";\nString s2 = \"world\";\n\nString str3 = s1 + s2;\n\nSystem.out.println(str1 == str2);\nSystem.out.println(str1 == str3);\n```\n\nstr1 == str3 返回是 false。因为 str3 在编译是无法确定。所以不会被当作直接量。但是如 s1 和 s2 都加上 final。编译器会进行宏替换。str3 就等同于”hello “ + ”world”。\n\n## final 的方法\n\nfinal 的方法不会被重写。\n\n## final 的类\n\nfinal 的类不能被继承。\n\n## 不可变类\n\nJava 提供的基础变量类型的包装类和 String 类都是不可变类。如果要自己创建不可变的类需要注意几点:\n\n- 使用 private 和 final 修饰成员变量。\n- 提供带参数的构造器,用于根据需要传入参数来初始化类里的成员变量。\n- 仅提供 getter 方法,不提供 setter 方法。\n- 如果有必要重写 hashCode() 和 equals() 方法。还要保证两个用 equals 方法判断相等的对象 hasdCode() 也相等。\n\n```java\npublic class Address {\n private final String detail;\n private final String postCode;\n public Address(){\n this.detail=\"\";\n this.postCode=\"\";\n }\n public Address(String detail,String postCode){\n this.detail=detail;\n this.postCode=postCode;\n }\n public String getDetail(){\n return this.detail;\n }\n public String getPoetCode(){\n return this.postCode;\n }\n public boolean equals(Object obj){\n if(this == obj){\n return true;\n }\n if(obj!=null&&obj.getClass()==Address.class){\n Address ad = (Address)obj;\n if(this.getClass().equals(ad.getDetail())&&this.getPoetCode().equals(ad.getPoetCode())){\n return true;\n }\n }\n return false;\n }\n public int hashCode(){\n return this.detail.hashCode()+this.postCode.hashCode();\n }\n}\n```\n\n编写不可变类的时候,只需要提供 getter 方法,不提供 setter 方法就可以了。但是对引用传递一定要注意。因为引用传递传的是尼玛指针。下面就是一个失败的例子。\n\n```java\npublic class Person {\n public final Name name;\n public Person(Name name){\n this.name = name;\n }\n public Name getName(){\n return this.name;\n }\n public static void main(String[] agrs){\n Name n = new Name(\"Brady\",\"Gao\");\n Person p = new Person(n);\n System.out.println(p.getName().getFirstName());\n n.setFirstName(\"heihei\");\n System.out.println(p.getName().getFirstName());\n }\n}\nclass Name {\n private String firstName;\n private String lastName;\n public Name(){}\n public Name(String firstName,String lastName){\n this.firstName = firstName;\n this.lastName = lastName;\n }\n public String getFirstName(){\n return this.firstName;\n }\n public void setFirstName(String firstName){\n this.firstName = firstName;\n }\n public String getLastName(){\n return this.lastName;\n }\n public void setLastName(String lastName){\n this.lastName = lastName;\n }\n}\n```\n\n要想解决这个,就要做到,避免引用传递带来的指针的直接传递。\n重写 Person 的构造方法。\n\n```java\npublic Person (Name name){\n\tthis.name = new Name(name.getFirstName(),name.getLastName());\n}\n```\n\n## References\n\n- 番茄博客-final 修饰符(原链接失效)\n","tags":["java"]},{"title":"【Core Java】读书笔记","url":"/2016/05/06/core-java-reading-notes/","content":"\n自己是第一次把一本厚厚的的技术类书读一遍。不过 7、8、9、10 章讲的是关于图形的就是翻了翻,没怎么看。第 4 章 对象与类,里面有很多非常基础,可以补充一些知识细节。第 14 章 多线程,是自己最陌生的,慕课网上有节课 [深入浅出 Java 多线程](http://www.imooc.com/view/202) 讲到的例子就是书上例子的变形,可以对照理解。第二遍阅读做做笔记。\n\n本文总结的是书中的:第 3 章 Java 的基本程序设计结构、第 4 章 对象与类。\n\n<!-- more -->\n\n## 3 Java 的基础程序设计结构\n\n### 3.3 数据类型\n\n- Java 是一种强类型语音。在 Java 中,一共有 8 中基本类型(primitive type),其中有 4 种整型、2 种浮点类型、1 种用于表示 Unicode 编码的字符单元的字符类型 char 和 1 种用于表示真值的 boolean 类型。\n- Java7 开始,还可以为数字字面量加下划线,如用 1_000_000 表示一百万。这些下划线只是为了让人更易读。Java 编译器会去除这些下划线。\n- 浮点数值不适用于出现舍入误差的金融计算中。其主要原因时浮点数值采用二进制系统表示,而在二进制系统中无法精确的表示分数 1/10。这就好像十进制无法精确地表示 1/3 一样。如果需要在数值计算中不含有任何舍入误差,就应该使用 BigDecimal 类。\n\n### 3.4 变量\n\n- 尽管\\$是一个合法的 Java 字符,但不要在自己的代码中使用这个字符。它只用在 Java 编译器或其他工具生成的名字中。\n\n### 3.6 字符串\n\n- 由于不能修改 Java 字符串中的字符,所以在 Java 文档中将 String 类对象称为不可变字符串。\n- 如果虚拟机始终将相同的字符串共享,就可以使用 == 运算符检测是否相等。但实际上只有字符串常量是共享的,而 + 或者 substring 等操作产生的结果并不是共享的。因此,千万不要使用 == 运算符测试字符串的相等性,以免在程序中出现糟糕的 bug。从表面上看,这种 bug 很像随机产生的间歇性错误。\n\n### 3.10 数组\n\n- 在 Java 中,允许将一个数组变量拷贝给另一个数组变量。这时,两个变量将引用同一个数组:\n\n```java\nint[] luckyNumbers = smallPrimes;\nluckNumbers[5] = 12; // now smallPrimes[5] is also 12\n```\n\n- 如果希望将一个数组的所用值拷贝到一个新的数组中去,就要使用 Arrays 类的 copyOf 方法:\n\n```java\nint[] copiedLuckyNumbers = Arrays.copyOf(luckyNumbers, luckyNumbers.length);\n```\n\n## 4 对象与类\n\n### 4.1 面向对象程序设计概述\n\n- 类之间的关系\n 依赖(uses-a)\n 聚合(has-a)\n 继承(is-a)\n- 依赖(dependence),即 uses-a 关系。例如,Order 类使用 Account 类是因为 Order 对象需要访问 Account 对象查看信用状态。因此,如果一个类的方法操纵另一个类的对象,我们就说一个类依赖于另一个类。\n 应该尽可能的将相互依赖的类减至最少。我们如果 A 不知道 B 的存在,他就不会关心 B 的任何改变(这意味着 B 的改变不会导致 A 产生任何 bug)。用软件工程的术语来说,就是让类之间的耦合度最小。\n- 聚合(aggressive),即 has-a 关系。例如,一个 Order 对象包含一些 Item 对象。聚合关系意味着类 A 的对象包含类 B 的对象。\n- 继承(inheritance),即 is-a 关系。例如,RushOrder 类由 Order 类继承而来。在具有特殊性的 RushOrder 类中包含了一些用于优先处理的特殊方法,以及一个计算运费的不同方法;而其他的方法从 Order 类继承了来的。一般而言,如果类 A 扩展类 B,类 A 不但包含从类 B 继承的方法,还会拥有一些额外的功能。\n\n### 4.2 使用预定义类\n\n- 在对象与对象变量之间存在着一个重要的区别。例如:\n\n```java\nDate deadline; // deadline doesn't refer to any object\n```\n\n- 定义了一个对象变量 deadline,它可以引用 Date 类型的对象。但是,一定要认识到:变量 deading 不是一个对象,实际上也没有引用对象。此时,不能将任何 Date 方法应用与这个变量上。\n\n```java\ns = deadline.toString(); // not yet\n```\n\n- 必须首先初始化变量 deadline,可以用新构造的对象初始化这个变量,也可以让这个变量引用一个已存在的对象。\n- 一定要认识到:一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。\n- 在 Java 中,任何对象变量的值都时对存储在另一个地方的一个对象的引用。new 操作符的返回值也是一个引用。\n- 可显示地将对象变量设置为 null,表明这个对象变量目前没有引用任何对象。\n\n### 4.3 用户自定义类\n\n- 类文件必须与 public 类的名字相匹配。在一个原文件中,只能有一个共有类,但可以有任意数目的非共有类。\n- 可以用 public 标记实例域,但这是一种极为不提倡的做法。public 数据域允许程序中的任何方法对其进行读取和修改。这就完全破坏了封装。这里强烈建议将实例域标记为 private。\n- 构造器:\n 构造器与类同名\n 每个类可以有一个以上的构造器\n 构造器可以有 0 个、1 个或多个参数\n 构造器没有返回值\n 构造器总是伴随着 new 操作一起调用\n- 类:\n 私有的数据域\n 公有的域服务器方法\n 公有的域改变方法\n- 不要编写返回引用可变对象的访问器方法。\n\n```java\nclass Employee {\n ...\n private Date hireDay;\n public Date getHireDay() {\n return Date hireDay;\n }\n}\n```\n\n- 这样会破坏封装性!代码:\n\n```java\nEmployee harry = ...;\nDate d = harry.getHireDay();\ndoule tenYearsInMilliSeconds = 10 * 365.25 * 24 * 60 * 60 * 1000;\nd.setTime(d.getTime() - (long)tenYearsInMilliSeconds);\n// let's give Harry ten years of added seniority\n```\n\n- 出错的原因很微妙。d 和 harry.hireDay 引用同一个对象。对 d 调用更改器方法就可以自动地改变这个雇员的私有状态!\n- 如果需要返回一个可变对象的引用,应该首先对它进行克隆(clone)。对象 clone 是指存放在另一个位置上的对象副本。\n\n```java\n public Date getHireDay() {\n return Date hireDay.clone();\n }\n```\n\n- Employee 类的方法可以访问 Emloyee 类的任何一个对象的**私有域 ** 。\n\n```java\nclass Employee {\n public boolen equals(Employee other) {\n return name.equals(other.name);\n }\n}\n```\n\n- final 修饰符大都应用于基本(primitive)类型域,或不可变(immutable)类的域(如果类中的每个方法都是不会改变其对象,这中类就是不可变的类。例如,String 类就是一个不可变的类)。对于可变的类,使用 final 修饰符可能会对读者造成混乱。代码\n\n```java\nprivate final Date hiredate;\n```\n\n- 仅仅意味着存储子 hiredate 变量中的对象引用在对象构造之后不能被改变,而并不意味着 hiredate 对象是一个常量。任何方法都可以对 hiredate 引用的对象调用 setTime 更改器。\n\n### 4.4 静态域与静态方法\n\n- 每一个雇员对象都有一个自己的 id 域,但这个类的所有实例将共享一个 nextId 域。\n\n```java\nclass Employee {\n private static int nextId = 1;\n private int id;\n}\n```\n\n- 换句话说,如果有 1000 个 Employee 类的对象,则有 1000 个实例域 id。但是,这有一个静态域 nextId。即使没有一个雇员对象,静态域 nextId 也是存在。它属于类,而不属于任何独立的对象。\n- 可以使用对象调用静态方法。例如,如果 harry 是一个 Employee 对象,可以用 harry.getNextId() 代替 Employee.getNextId()。不过这中方式很容易造成混淆,其原因是 getNextId 方法计算的结果与 harry 毫无关系。我们建议使用类名来调用静态方法。\n- 每一个类可以有一个 main 方法。这是一个常用于对类进行单元测试的技巧。\n\n### 4.5 参数方法\n\n- Java 程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝。\n- Java 程序设计语言中方法参数的使用情况:\n 一个方法不能修改一个基本数据类型的参数。\n 一个方法可以改变一个对象参数的状态。\n 一个方法不能让对象参数引用一个新的状态。\n\n### 4.6 对象构造\n\n- 要完整的描述一个方法需要指出方法以及参数类型,这叫做方法的签名(signature)。\n- 返回类型不是方法签名的一部分。也就是说,不能有两个名字相同、参数类型也相同却返回不同类型值的方法。\n- 如果在编写一个类时没有编写构造器,那么系统就会提供一个无参数构造器。这个构造器将所有的实例域设置为默认值。实例域中的数值类型数据设置为 0、布尔型数值设置为 false、所有对象变量将设置为 null。\n- 如果类中提供了至少一个构造器,但是没有提供无参数的构造器,则在构造对象时如果没有提供参数就会被视为不合法。\n- 对每一个实例域都可以被设置为一个有意义的初值,这是一种很好的设计习惯。\n\n```java\nclass Employee {\n private String name = \"\";\n ...\n}\n```\n\n### 4.10 类设计技巧\n\n- 一定要保证数据私有。绝对不要破坏封装性。\n- 一定要对数据初始化。\n- 不要在类中使用过多的基本类型。就是说,用其他的类代替多个相关的基本类型的使用。\n- 不是所有的域都需要独立的域访问器和域更改器。\n- 将职责过多的类进行分解。\n- 类名和方法名要能够体现它们的职责。\n","tags":["java"]},{"title":"【Core Java】对象与类-方法参数传递","url":"/2016/04/19/core-java-method-parameter/","content":"\n提问:Java 对象采用的是值传递还是引用传递?\n\n有些程序员认为 Java 对象采用的是引用调用,实际上,这种理解是不对的。下面给出一个反例来详细的阐述一下这一问题。\n\n首先,编写一个交换两个雇员对象的方法:\n\n```java\npublic static void swap(Employee x, Employee y)\n{\n Employee temp = x;\n x = y;\n y = temp;\n}\n```\n\n<!-- more -->\n\n如果 Java 程序时引用调用,那么这个方法就应该能都实现交换实际的效果。\n\n```java\nSystem.out.println(\"\\nTesting swap:\");\n\nEmployee a = new Employee(\"Alice\", 70000);\nEmployee b = new Employee(\"Bob\", 60000);\n\nSystem.out.println(\"Before: a=\" + a.getName());\nSystem.out.println(\"Before: b=\" + b.getName());\n\nswap(a, b);\n\nSystem.out.println(\"After: a=\" + a.getName());\nSystem.out.println(\"After: b=\" + b.getName());\n```\n\n但是结果 a 仍然时 Alice,b 是 Bob。对象引用进行的是值传递。\n\n其实不必取扣是值传递还是引用传递的字眼,理解其中的原因是最好。\n\n![160419-core-java-method-parameter-001](https://user-images.githubusercontent.com/9289792/80198302-72d19700-8652-11ea-9ddf-595a2be198b5.jpg)\n\n在调用 swap 时,a 将一个 **引用的副本** 传递给了 x,让 x 也指向了,相同与 a 指向的内存单元中的 Employee 对象 Alice,b 将一个引用的副本传递给了 y,让 y 也指向了,相同与 b 指向的内存单元中的 Employee 对象 Bob。\n\n这时执行 swap 方法内部的内容,交换了 x,y 的引用指向,这是 x 指向的时 Bob,y 是 Alice,但是在方法结束后,方法外的 a,b 的指向是仍然没有变化,a 是 Alice,b 是 Bob。\n\n## 书上总结\n\n一个方法不能修改一个基本数据类型的参数。\n一个方法可以改变一个对象参数的状态。\n一个方法不能让对象参数引用一个新的对象。\n\n-- EOF --\n","tags":["java"]},{"title":"【Core Java】彩票选取中奖数字-数组例子","url":"/2016/03/23/core-java-array-dome/","content":"\n从 1,2,3...h 中随机取 k 个中奖号码。\n\n<!-- more -->\n\n```java\npackage im.zyf.javacore;\n\nimport java.util.Arrays;\nimport java.util.Scanner;\n\npublic class LotteryDrawing {\n\n public static void main(String[] args) {\n\n Scanner in = new Scanner(System.in);\n System.out.println(\"how many numbers do you need?\");\n int k = in.nextInt();\n\n System.out.println(\"what is the highest number?\");\n int h = in.nextInt();\n\n int[] harr = new int[h];\n for (int i = 0; i < h; i++) {\n harr[i] = i + 1;\n }\n\n int[] karr = new int[k];\n for (int i = 0; i < k; i++) {\n int random = (int) (Math.random() * h);\n karr[i] = harr[random];\n //将数组最后的值,代替掉被取走的值\n harr[random] = harr[h - 1];\n //数组长度减1\n h--;\n }\n Arrays.sort(karr);\n System.out.println(Arrays.toString(karr));\n }\n}\n```\n\nConsole:\n\n```bash\nhow many numbers do you need?\n3\nwhat is the highest number?\n4444444\n[433177, 827621, 2607294]\n```\n\n有几个点:\n\n- 取过的数不能再取\n- 取后升序排列\n\n关键点:\n\n- 每次取的都是下标,而不是实际的值。下标指向包含尚未抽取的数组元素\n\n-- EOF --\n","tags":["java"]},{"title":"试着写东西","url":"/2016/03/18/try-to-write-something/","content":"\n现在是大四下从学校出来实习。在面试一家公司时,面试官看到我的邮箱(168#yifans.com)是自己的域名后说:\n\n> 问:“有自己的网站吗?”\n> “没有。”\n> 问:“域名都买了不自己搭个网站?”\n> “……”\n\n其实这也不是自己要搭博客的根本原因。\n\n在大学里,学了些做了些东西,可思考的不多。现在越发觉的一个人的强大在于思想。看书写些东西我想是提高一个人思想的方法吧。文字写下来了也方便交流。\n\n出来了换了一个环境,遇到新的人,新的事,可以换个角度看人看事。原来我理解的一些技术博客,就是解决问题后的笔记本,有问题从中找,而且觉得很多网上的技术博客里的内容都是来回的转来转去,很多解决方案都是过时的,没什么的。可最近在浏览博客园、csdn 发现了很多精华的前人分享自己所理解的知识、技术中走的弯路,阅读后很受益。\n\n想想自己,很多事、很多情况下就是这样:没见多少就下结论,管中窥豹,too young.\n\n其中有这样的一个签名:\n\n> 我不能保证写的每个地方都是对的,但是至少能保证不复制、不黏贴,保证每一句话、每一行代码都经过了认真的推敲、仔细的斟酌。每一篇文章的背后,希望都能看到自己对于技术、对于生活的态度。\n> 我相信乔布斯说的,只有那些疯狂到认为自己可以改变世界的人才能真正地改变世界。面对压力,我可以挑灯夜战、不眠不休;面对困难,我愿意迎难而上、永不退缩。\n> 其实我想说的是,我只是一个程序员,这就是我现在纯粹人生的全部。\n\n再认同不过了。\n\n## References\n\n- [为什么你要写博客? - 知乎](https://zhuanlan.zhihu.com/p/19743861)\n\n-- EOF --\n","tags":["thinking"]}]