-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.json
More file actions
1 lines (1 loc) · 183 KB
/
content.json
File metadata and controls
1 lines (1 loc) · 183 KB
1
{"meta":{"title":"Karakarua's blog","subtitle":"","description":"","author":"Ferrilata","url":"http://blog.karakarua.com","root":"/"},"pages":[{"title":"关于","date":"2021-04-03T04:59:25.500Z","updated":"2021-04-03T04:59:25.500Z","comments":false,"path":"about/index.html","permalink":"http://blog.karakarua.com/about/","excerpt":"","text":"一个记录学习过程的博客"},{"title":"404 Not Found:该页无法显示","date":"2020-09-04T15:34:25.000Z","updated":"2020-09-04T15:34:25.000Z","comments":false,"path":"/404.html","permalink":"http://blog.karakarua.com/404","excerpt":"","text":""},{"title":"标签","date":"2020-09-04T15:34:25.000Z","updated":"2020-09-04T15:34:25.000Z","comments":false,"path":"tags/index.html","permalink":"http://blog.karakarua.com/tags/","excerpt":"","text":""},{"title":"分类","date":"2020-09-04T15:34:25.000Z","updated":"2020-09-04T15:34:25.000Z","comments":false,"path":"categories/index.html","permalink":"http://blog.karakarua.com/categories/","excerpt":"","text":""}],"posts":[{"title":"Apache Flink入门-应用场景","slug":"Apache-Flink入门-应用场景","date":"2021-10-03T23:27:00.000Z","updated":"2021-10-04T15:30:31.090Z","comments":true,"path":"2021/10/03/84aa55076b99/","link":"","permalink":"http://blog.karakarua.com/2021/10/03/84aa55076b99/","excerpt":"","text":"Apache Flink主要应用在保证实时性的一些场景,比如(1)社交网站关注用户后,被关注者的粉丝数量立即变化;(2)疫情人数发生变化后防控级别的变化;(3)实时性ETL。 参考:Flink 应用场景 flink常见应用场景: 事件驱动型应用事件驱动型应用是一类具有状态的应用,该应用会根据一个或多个事件流中到来的事件触发计算、更新状态或进行外部操作。事件驱动型应用常见于实时计算业务中,比如实时推荐、金融反欺诈、实时规则预警等。事件驱动型应用将状态保存到本地,相比于传统数据库远程查询数据库,可以很大程度减少处理时间。 实时性数据分析应用数据分析应用是指从原始数据中提取有价值的信息和指标,分为批处理分析和流处理分析。批处理主要处理某个时间区间内的数据,当要分析最新时间的数据时,必须将新的数据再加入处理的数据集中才能进行分析。流处理分析是实时性分析,其维护了从开始时间点至现在的所有数据,最新的数据到来时会产生新一轮分析,从而保证分析结果的实时性。比如实时分析疫情人数的变化确定防控级别。 数据管道应用数据管道型应用类似于ETL,可以看作是实时性ETL。ETL是提取(extract)、转换(transform)、加载(load)的是缩写,是一种在各存储系统之间进行数据转换和迁移的常用方法。ETL一般周期性地进行,将数据从事务型数据库拷贝到分析型数据库或数据仓库。数据管道可以持续性地从某个数据源端读取数据,并将他们转换加载到目的存储端。flink处理数据管道作业的优势: 丰富的Connector连接数据源和目的端 强大的数据转换能力 内置多种聚合函数,具备强大的数据聚合能力 最重要的是”低处理延迟” Q&A:Q:为什么有ETL?A: ETL的目的大多是为了解决信息孤岛的问题,举个例子,一个公司可能有客户数据、产品数据、销售数据等多个模块的数据,他们之间都是相互隔离的,任一单独的数据并不能发挥任何价值,ETL的存在,就是将这些数据聚集起来,然后就可以通过全局型的数据分析进行战略决策等。Q:新概念ELT和数据湖是什么?ELT:ELT的缩写同ETL,不同于ETL的数据提取、执行特定转换后加载到目的存储系统,ELT只是将数据提取后就加载到目的存储系统,然后交由目的存储系统做数据转换。ELT的优势在于目的存储系统可以根据不同应用的不同需求而选择不同的转换引擎对数据执行不同方式的处理。由此可以引出的概念是”数据湖(data lake)”,广义的数据湖概念就是上面讲到的目的存储系统,既支持存储操作,也支持多种引擎的分析和转换操作。狭义的数据湖的概念是具备统一存储的系统。 参考: 【Apache Flink 知其然,知其所以然(原理&实战)-哔哩哔哩】 ETL还是ELT?这个工具玩转数据清洗实现高效率 数据同步工具ETL、ELT傻傻分不清楚?3分钟看懂两者区别","categories":[{"name":"大数据","slug":"大数据","permalink":"http://blog.karakarua.com/categories/%E5%A4%A7%E6%95%B0%E6%8D%AE/"}],"tags":[{"name":"大数据","slug":"大数据","permalink":"http://blog.karakarua.com/tags/%E5%A4%A7%E6%95%B0%E6%8D%AE/"},{"name":"Flink","slug":"Flink","permalink":"http://blog.karakarua.com/tags/Flink/"}],"author":"Ferrilata"},{"title":"设计模式分类目录","slug":"设计模式目录","date":"2021-09-20T11:44:00.000Z","updated":"2021-11-13T13:09:16.417Z","comments":true,"path":"2021/09/20/f4d7469a17ff/","link":"","permalink":"http://blog.karakarua.com/2021/09/20/f4d7469a17ff/","excerpt":"","text":"设计模式是一套软件设计经验的总结,目的是”提高代码的复用性,应对将来的需求变化,提升代码的可扩展性”。设计模式一共23种。 常规分类方法常规分类主要分为创建型、结构型以及行为型。 创建型:(5种) 工厂方法模式、抽象工厂模式 建造者模式 原型模式 单例模式 结构型:(7种) 组合模式 装饰器模式 代理模式 门面模式 适配器模式 桥接模式 享元模式 行为型:(11种) 模板方法 策略模式 状态模式 命令模式 中介模式 观察者模式 访问者模式 迭代器模式 责任链模式 备忘录模式 解释器模式 推荐:学习设计模式的网站:C语言中文网-设计模式 独特分类方法B站李建忠老师的另外一种分类方法:参考链接:设计模式 李建忠-哔哩哔哩组件协作: 模板方法 策略模式 观察者模式 单一职责: 装饰器模式 桥接模式 对象创建: 工厂方法 抽象工厂 原型方法 建造者模式 对象性能: 单例模式 享元模式 接口隔离: 门面方法 代理模式 中介者模式 适配器模式 状态变化: 备忘录模式 状态模式 数据结构: 组合模式 迭代器模式 责任链模式 行为变化: 命令模式 访问者模式 领域问题: 解释器模式","categories":[],"tags":[{"name":"Design Pattern","slug":"Design-Pattern","permalink":"http://blog.karakarua.com/tags/Design-Pattern/"}],"author":"Ferrilata"},{"title":"大数据零碎笔记","slug":"大数据零碎笔记","date":"2021-08-29T07:33:00.000Z","updated":"2021-10-04T15:34:44.175Z","comments":true,"path":"2021/08/29/ded3a62c87dc/","link":"","permalink":"http://blog.karakarua.com/2021/08/29/ded3a62c87dc/","excerpt":"","text":"大数据关键技术: 数据采集 数据存储与管理 数据处理与分析 数据隐私与安全 大数据核心技术: 分布式存储技术 分布式处理技术 大数据计算模式及相关框架 \\begin{cases} 批处理: & MapReduce、Spark \\\\ 流处理: & Flink、Storm \\\\ 图形计算: & Pregel \\\\ 查询分析: & Hive、Dremel、Cassandra \\end{cases} Hadoop生态 Hadoop生态组件图: YARN:在没有YARN之前,由于多种计算框架在资源调度机制方面的不同,为了避免各框架之间互相抢夺资源,只能一个集群部署一种框架,大大浪费了集群资源。而YARN恰是一套资源调度框架,可以根据需求对多种计算框架进行资源调度,从而实现了”一个集群多种框架”,提高了资源利用率。 Hive 与 HBase的区别: HIVE是一种计算工具,在封装Map Reduce的基础上使用SQL方式实现离线处理,可以简单认为是一套SQL翻译语言,只不过翻译为MapReduce计算,HIVE本质上还是依靠MapReduce执行离线批量处理。HIVE目的是处理分析,所以仅支持查询及插入,不支持更改和删除。 HBase是一种面向列的key/value数据库,是把hdfs的数据整理后形成的非关系型数据库,可以做到随机实时访问。HBase本质是数据库,支持增删改查,Hbase使用方式并不是SQL方式,其不支持SQL。 参考链接:Hive和Hbase各自的应用场景 Zookeeper:分布式应用程序协调服务框架,提供配置维护、域名服务、分布式同步、组服务。 Sqoop:ETL工具,支持Hadoop与关系型数据库之间的数据转移。 Ambari:hadoop集群的集中监控、供应、管理工具,支持Web UI。 Spark vs Hadoop MapReduce核心对比 Hadoop的三大缺陷:表达力差(仅支持Map和Reduce)、延迟高(使用磁盘IO)、链式过程(必须map计算结果写入磁盘后才可以开始reduce操作) Spark针对上述三点提出了改进:支持多种运算(比如groupBy、filter等)、基于内存计算(中间结果保存到内存)、基于DAG(有向无环图)的调度执行机制。 Spark和MapReduce都需要依赖于HDFS Spark vs Flink核心对比 计算模型: Spark基于RDD(弹性数据集)进行小批量处理,是一种微批处理机制,flink是一种基于操作符的连续的流处理模型 spark处理流处理的延迟比flink高,spark的流处理延迟在秒级别,而flink的流处理延迟在毫秒级别。 Spark和Flink都同时支持批处理、流处理、图处理和查询分析计算。","categories":[{"name":"大数据","slug":"大数据","permalink":"http://blog.karakarua.com/categories/%E5%A4%A7%E6%95%B0%E6%8D%AE/"}],"tags":[{"name":"大数据","slug":"大数据","permalink":"http://blog.karakarua.com/tags/%E5%A4%A7%E6%95%B0%E6%8D%AE/"}],"author":"Ferrilata"},{"title":"Java泛型类与泛型方法","slug":"泛型类与泛型方法","date":"2021-08-06T17:03:00.000Z","updated":"2021-08-07T17:09:23.711Z","comments":true,"path":"2021/08/06/0aa8d759c327/","link":"","permalink":"http://blog.karakarua.com/2021/08/06/0aa8d759c327/","excerpt":"","text":"泛型类定义一个泛型类和普通类的过程类似,区别就在于要在类名的地方使用尖括号”<>”声明泛型变量。比如下面的例子:123456789101112131415// T是泛型变量,在类名处被声明,除此之外与定义普通类无差别// 泛型变量被声明后,可以在类中被使用public class Pair<T>{ T first; T second; // 定义类的构造函数时不必再次声明泛型变量 public Pair(){ first = null; second = null; } public Pair(T first, T second){ this.first = first; this.second = second; }}也可以声明多个泛型变量,如下面的例子:123456789// K,V都是泛型变量public class Pair<K,V>{ K key; V value; public Pair(K k, V v){ key = k; value = v; }} 泛型方法泛型方法是指定义一个带有泛型参数的方法,该泛型方法在使用泛型参数之前需要声明泛型变量。下面是一个例子123456public class Main { // T是泛型变量,使用尖括号声明,声明要放在修饰符(public static)和返回类型之间 public static <T> T getMiddle(T[] arr){ //示例方法含义是取数组中间的数 }}下面有个java程序可以运行一下试一试。1234567891011121314151617181920public class Main{ public static void main (String[] args) { print(new man()); } public static <T extends Person> void print(T p){ p.print(); }}abstract class Person{ int sex; void print(){};}class man extends Person{ @Override void print(){ System.out.println("i am men"); }} Q&A:Question:上面这个例子中,第7行的泛型变量T实际上只被使用了一次,是否将代码改为public static void print(<T extends Person> p),即同时声明和使用?Answer:不可以,泛型变量使用前必须被声明,且声明应该被放在修饰符(public static)和返回类型之间,否则Java无法识别泛型变量。Java程序会对此段代码public static void print(<T extends Person> p)编译出错,错误结果为123456Main.java:7: error: illegal start of typepublic static void print(<T extends Person> p){ ^Main.java:7: error: > expected public static void print(<T extends Person> p){ ^","categories":[{"name":"Java","slug":"Java","permalink":"http://blog.karakarua.com/categories/Java/"}],"tags":[{"name":"Java","slug":"Java","permalink":"http://blog.karakarua.com/tags/Java/"}],"author":"Ferrilata"},{"title":"LaTeX设置标题样式","slug":"LaTeX设置标题样式","date":"2021-05-30T05:28:00.000Z","updated":"2021-08-07T17:07:14.771Z","comments":true,"path":"2021/05/30/ff68a983e1db/","link":"","permalink":"http://blog.karakarua.com/2021/05/30/ff68a983e1db/","excerpt":"","text":"需求需要定制标题样式,比如标题的前后间距,标题的字体样式 解决方法通过@startsection命令 \\@startsection命令参数可见 1\\@startsection {NAME}{LEVEL}{INDENT}{BEFORESKIP}{AFTERSKIP}{STYLE} 参数 解释 NAME 表示所定义的节标题的名称(不要带反斜杠),比如section和subsection。 LEVEL 是一个数字,可以定义节标题的命令层次。这个数决定了定义的节标题是否编号(若是小于等于 secnumdepth则被编号)也决定了标题是否会被编进目录(若是小于等于tocdepth则被编号)。 INDENT 定义节标题到版心左边的距离。此度量若是负数则标题进入边空。 BEFORESKIP 是一个长度,其绝对值表示标题到上文之间的距离。若是此距离为负数,则标题后面的第一个段落不缩进。此度量最好是一个可以被伸长和缩短的长度。另外标题总是另起一段的。因而parskip已被加入到标题与上下文的距离。 AFTERSKIP 是一个长度,其绝对值表示独立显示的标题到下文之间的垂直间距或者是段内显示的标题到下文之间的距离。此度量若是负的,则定义的标题是段内显示的。对于独立显示的标题,parskip也已被加进标题与下文的距离。 STYLE 决定标题的内容形式。可以是任意影响文本排版结构的命令如加入尺寸\\huge \\large \\bfseries 对齐命令等 以section为例, beforeskip {-3.5ex plus -1ex minus -.2ex} 表示最大-4.5ex, 最小-3.7ex. 示例编写在xxx.cls中的标题示例 123456789101112131415161718\\renewcommand\\section{% \\@startsection {section}{1}{\\z@}% {-8bp \\@plus -.1bp \\@minus -.2bp}% {8bp \\@plus .1bp}% {\\bfseries\\csname bupt@title@font\\endcsname\\sihao[1.429]}% 此处控制section标题的字体}\\renewcommand\\subsection{% \\@startsection{subsection}{2}{\\z@}% {-6bp \\@plus -.1bp \\@minus -.2bp}% {6bp \\@plus .1bp}% {\\bfseries\\csname bupt@title@font\\endcsname\\xiaosi[1.538]}% 此处控制subsection标题的字体}\\renewcommand\\subsubsection{% \\@startsection{subsubsection}{3}{\\z@}% {-4bp \\@plus -.1bp \\@minus -.2bp}% {4bp \\@plus .1bp}% {\\song\\csname bupt@title@font\\endcsname\\xiaosi[1.667]}%} 参考链接:LaTeX技巧356:修改Latex默认section,subsection样式","categories":[{"name":"LaTeX","slug":"LaTeX","permalink":"http://blog.karakarua.com/categories/LaTeX/"}],"tags":[{"name":"LaTeX","slug":"LaTeX","permalink":"http://blog.karakarua.com/tags/LaTeX/"}]},{"title":"转载:Gerrit Missing Change-Id","slug":"转载:Gerrit Missing Change-Id","date":"2021-05-28T08:48:00.000Z","updated":"2021-05-28T08:48:35.376Z","comments":true,"path":"2021/05/28/e792a0bb33b0/","link":"","permalink":"http://blog.karakarua.com/2021/05/28/e792a0bb33b0/","excerpt":"","text":"转载:Gerrit “Missing Change-Id” 出处:gerrit “missing Change-Id” 场景你用 git push 向 gerrit 提交了待审核代码,一切都很顺利,你脑袋里冒出了”代码头上加了’佛祖保佑’果然有效”的想法.此时 git 打印出如下提示,你的内心OS同步打印 “心情 -5” : 123456789101112remote: Resolving deltas: 100% (14/14)remote: Processing changes: refs: 1,done remote: ERROR: missing Change-Idincommit message footerremote:remote: Hint: To automatically insert Change-Id,installthe hook:remote: gitdir=$(git rev-parse --git-dir);scp-p -P 29418 liux@gerrit.xxxxx.com:hooks/commit-msg${gitdir}/hooks/remote: And then amend the commit:remote: git commit --amendremote:To ssh://liux@121.xx.xx.xx:29418/kaiba_admin_yunying.git ! [remote rejected] HEAD -> refs/for/master(missing Change-Idincommit message footer)error: failed to push some refs to'ssh://liux@121.xx.xx.xx:29418/sample_project.git' 套路大前提: commit-msg 文件必须已经在该项目中存在.使用ls命令检查该文件是否存在:12$ cd project_dir$ ls.git/hooks/commit-msg如果该文件不存在,则按照 git push 时产生的提示信息,获取该文件:1$ gitdir=$(git rev-parse --git-dir);scp-p -P 29418 liux@gerrit.xxxxx.com:hooks/commit-msg${gitdir}/hooks/上面的命令可以直接从 git push 产生的错误信息中复制出来.如果要手敲该命令,别忘了把用户名换成自己的. 方法一: 使用 amend 选项生成 Change-Id:如果缺失 Change-Id 的是最后一个 (head) commit, 使用以下命令即可解决问题:1$ git commit --amend该命令会打开默认的 commit message 编辑器,一般是 vi.这时什么都不用修改,直接保存退出即可 (:wq).再次查看 git log,就会发现缺失的 Change-Id 已经被补上了. 再次git push 即可. 方法二: 如果缺失 Change-Id 的不是最后一个 commit, 可用 reset 方法:比如,如果缺失 Change-Id 的 commit 是 git log 中的第二个 commit, 则可以用 git reset 命令将本地分支回退到该 commit.(但其实用 git reset 找回 Change-Id 是普通青年才干的事情,文艺青年有更优雅的办法.见方法三)首先执行 git log, 找出缺失了 Change-Id 的 commit,并复制其 commit-id:12345678910111213141516$ git logcommit 8e1cad33bcd98e175cba710b1eacfd631a5dda41Author: liux <liux@xxxx.cn>Date: Mon Dec 19 17:43:00 2016 +0800 testcommit"with amended commit message" Change-Id: I9d2af0cc31423cf808cd235de0ad02abf451937dcommit 1a9096a34322885ac101175ddcac7dab4c52665dAuthor: liux <liux@xxxx.cn>Date: Mon Dec 19 15:23:36 2016 +0800 testcommit-msg hook......发现是第二个 commit 缺失 Change-Id. 将代码 reset 到这个 commit, 并执行 amend:12$ git reset 1a9096a34322885ac101175ddcac7dab4c52665d$ git commit --amend注: 上面的 git reset 用法不会毁灭你写的代码,放心执行即可.这时 git log 可以发现该 commit 已经补全了 change-Id.下一步是把 git reset 撤消掉的代码重新 commit, 然后 push 即可:123$ git add ......$ git commit -m "你的提交日志"$ git push review HEAD:refs/for/master 方法三: 使用交互式 rebase 找回任意提交位置的 Change-Id:前面方法二中给出的例子是第二个提交缺失 Change-Id,这时用 git reset 还可以解决问题.但如果你在一个方案上已经工作了一个月,生成了100个本地 commit,提交时才发现 git log 中第99个 commit 缺失 Change-Id. 如果这时还用 git reset 来找回 Change-Id ……不要香菇,不要蓝瘦.文艺青年表示有办法优雅的解决问题: 交互式 rebase. 第一步,找到缺失 Change-Id 的那个 commit:12345678910111213141516171819202122232425262728293031$ git logcommit 8aaaa749db4a5b105aa746659c5cd266ac82fffeAuthor: liux <liux@xxxx.cn>Date: Mon Dec 19 17:43:24 2016 +0800 I am commit message 3 Change-Id: Ic89d5ce6ce4de70d1dcb315ce543c86a2b3ac003commit 8e1cad33bcd98e175cba710b1eacfd631a5dda41Author: liux <liux@xxxx.cn>Date: Mon Dec 19 17:43:00 2016 +0800 I am commit message 2 Change-Id: I9d2af0cc31423cf808cd235de0ad02abf451937dcommit 1a9096a34322885ac101175ddcac7dab4c52665dAuthor: liux <liux@xxxx.cn>Date: Mon Dec 19 15:23:36 2016 +0800 I am commit message 1commit d714bcde0c14ba4622d28952c4b2a80882b19927Author: shangsb <shangsb@czfw.cn>Date: Wed Dec 14 09:20:52 2016 +0800 这是一个提交 Change-Id: I629b2bedff95491875f63634ad3da199612735b6...... 发现是 “I am commit message 1” 这个提交没有 Change-Id. 第二步,编辑交互式 rebase 的命令文件:执行git rebase -i, 参数为 该提交的上一个提交的 commit-id (本例中为 “表单” 那个提交):12345678910111213141516171819202122$ git rebase -i d714bcde0c14ba4622d28952c4b2a80882b19927这个命令会打开默认的编辑器,一般为 vi. 内容如下:pick 1a9096a I am commit message 1pick 8e1cad3 I am commit message 2pick 8aaaa74 I am commit message 3# Rebase d714bcd..8aaaa74 onto d714bcd## Commands:# p, pick = use commit# r, reword = use commit, but edit the commit message# e, edit = use commit, but stop for amending# s, squash = use commit, but meld into previous commit# f, fixup = like "squash", but discard this commit's log message# x, exec = run command (the rest of the line) using shell## These lines can be re-ordered; they are executed from top to bottom.## If you remove a line here THAT COMMIT WILL BE LOST.## However, if you remove everything, the rebase will be aborted.## Note that empty commits are commented out可以将这个文件理解为 git rebase 的内嵌脚本.其命令写法已经在下面的注释里给出了.这里不赘述,仅给出最终要将该文件编辑成什么样子:1234567891011121314151617181920reword 1a9096a I am commit message 1pick 8e1cad3 I am commit message 2pick 8aaaa74 I am commit message 3# Rebase d714bcd..8aaaa74 onto d714bcd## Commands:# p, pick = use commit# r, reword = use commit, but edit the commit message# e, edit = use commit, but stop for amending# s, squash = use commit, but meld into previous commit# f, fixup = like "squash", but discard this commit's log message# x, exec = run command (the rest of the line) using shell## These lines can be re-ordered; they are executed from top to bottom.## If you remove a line here THAT COMMIT WILL BE LOST.## However, if you remove everything, the rebase will be aborted.## Note that empty commits are commented out即: 将缺失了 Change-Id 的 commit 前面的pick 改为 reword即可 (简写 r也可以). 保存退出 (:wq)注1: 上述文件中 commit 的顺序是和 git log 显示的顺序相反的: git log 为最新的在最前; 上述文件为 最新的在最后.注2: 如果进入该模式后,却不确定该怎么改,这时不要担心,直接退出编辑则什么都不会发生 (:q!)注3: 如果没有搞清楚原理,就要注意,除了按需把 pick 改为 reword外,不要做其他改动.尤其注意不要删除任何行 (被删除的那行对应的提交将丢失).注4: 你应该已经发现,有多个 commit 缺失 Change-Id 的情况也可以用该方法一次性处理. 第三步,逐个编辑 commit-msg:上一步打开的文件保存退出后,git会逐个打开被你标注了 reword 的提交日志页面.不需要修改任何东西,逐个保存退出即可 (一路 :wq). 第四步,再次提交:用 git log 查看提交日志,会发现缺失的 Change-Id 都生成了. 愉快的提交代码吧!1$ git push review HEAD:refs/for/master 心法:gerrit 的 Change-Id 机制:首先要明确, Change-Id 是 gerrit (代码审核平台)的概念, 与 git (版本管理) 是没有关系的.简单来说, Change-Id 是 gerrit 用以追踪具体提交的机制. 这里不贴网上已有的解释,举两个栗子大家体会下: 你已经用 git push 将代码提交 gerrit 审核了,这时你发现代码中有疏漏,修改了一下,执行 git commit —amend, 再次推送还可以成功. 这就是因为 gerrit 检查到两次 push 的 commit 有同一个 change-id, 就认为是同一个提交,因此可以 amend. git push 将代码提交到 gerrit 审核,到 gerrit 网站一看,大红字标着 Can Not Merge 字样. 我想常用 gerrit 的同学肯定都遇到过这问题. 之前我的做法是, git reset 后,更新代码,再重新提交. 现在的做法是,不用 git reset 了,直接 git commit —amend, 删掉 commit log 中的 change-id 那行,然后wq保存退出.这时 gerrit 的那个钩子脚本会再生成一个不同的 change-id ,这时再更新代码,重新提交即可成功. 这里只简要介绍该方法,具体步骤将在 代码冲突 场景中详解.Change-Id 的生成机制请继续向下看.git 的 hook 机制:钩子(hooks)是一些在$GIT-DIR/hooks目录的脚本, 在被特定的事件(certain points)触发后被调用。当git init命令被调用后, 一些非常有用的示例钩子脚本被拷到新仓库的hooks目录中; 但是在默认情况下它们是不生效的。 把这些钩子文件的”.sample”文件名后缀去掉就可以使它们生效。hook机制可以理解为回调.各个钩子其实就是一段 bash 脚本,各钩子脚本的名字都是固定的.可以查看git项目根目录下的 .git/hooks 这个文件夹,看看都有哪些可用的钩子.1234$ cdproject_dir$ ls .git/hooks/applypatch-msg.sample commit-msg.sample pre-applypatch.sample prepare-commit-msg.sample pre-rebase.samplecommit-msg post-update.sample pre-commit.sample pre-push.sample update.sample 如果有自己感兴趣的 git 事件要处理,修改相应的钩子脚本罗辑即可.然后把 .sample 后缀去掉,钩子就生效了.在 gerrit 的 Change-Id 生成机制中,其实 gerrit 就是利用了 commit-msg 的钩子,在我们提交代码后,按一定规则去修改了我们的提交日志,在其末尾添加了这么一行:1Change-Id: ....... 这个钩子脚本是什么时候被加入我们的项目中的呢? 其实就是你在 git push 出错时 gerrit 网站给你的提示中的那句命令:1$ gitdir=$(git rev-parse --git-dir);scp -p -P 29418 liux@gerrit.kaiba315.com:hooks/commit-msg${gitdir}/hooks/ 执行该命令即可得到生成 Change-Id 的钩子脚本. 这条命令做了以下事情:123456// git rev-parse --git-dir 这条命令将找到该项目的 git 目录,并将其赋值给 gitdir 这个变量.// 一般就是项目根目录下的 .git/ 这个目录.$ gitdir=$(git rev-parse --git-dir)// 执行 scp 命令,从 gerrit 代码服务器将钩子脚本文件 commit-msg 下载到项目的钩子目录下 (一般是 .git/hooks/)$ scp -p -P 29418 liux@gerrit.kaiba315.com:hooks/commit-msg ${gitdir}/hooks/ 查看该脚本,会发现它是用 awk 命令处理了 .git/COMMIT_EDITMSG 这个文件.所以如果想手动生成 Change-Id ,只要执行下面命令,就可以生成一个可用的 Change-Id:1234567$ cdproject_dir$ echo "some commit" > /tmp/test_generate_change_id$ .git/hooks/commit-msg/tmp/test_generate_change_id$ cat /tmp/test_generate_change_idsome commitChange-Id: Ic89d5ce6ce4de70d1dcb315ce543c86a2b3ac003 利用 git commit —amend 重新生成 Change-Id 的原理:git commit —amend , 看名字就知道,是对某个 commit 做出修改的.这种修改既可以包含文件修改,也可以仅包含提交日志修改.我们用 —amend 对 commit 做出修改后, commit-msg 的钩子会被重新触发, Change-Id 就会被生成出来.用交互式 git rebase 来生成 Change-Id 也是同一个道理.另:通过总结历次缺失 Change-Id 的例子,发现基本我们自己通过 git commit 生成的提交都会很顺利的生成 Change-Id.通过 git merge, git revert 等命令由 git 自己生成的 commit 则有较高概率会缺失 Change-Id.嗯,我们发现了一个伟大的定律! 然并卵… 并不知道怎么解决这个问题.因此提倡尽量用 git rebase 代替 git merge 来更新代码.事实上, git rebase 更新代码 相较 git merge 更新代码,有诸多优势,只是略复杂些.强烈建议用 git rebase 方式更新代码.","categories":[{"name":"Git","slug":"Git","permalink":"http://blog.karakarua.com/categories/Git/"}],"tags":[{"name":"Git","slug":"Git","permalink":"http://blog.karakarua.com/tags/Git/"},{"name":"Gerrit","slug":"Gerrit","permalink":"http://blog.karakarua.com/tags/Gerrit/"}]},{"title":"转载:BibTeX 定制自己的参考文献模板 BST","slug":"转载BST语法","date":"2021-05-21T14:53:40.000Z","updated":"2021-05-23T10:35:01.217Z","comments":true,"path":"2021/05/21/adbbbfaecb54/","link":"","permalink":"http://blog.karakarua.com/2021/05/21/adbbbfaecb54/","excerpt":"","text":"引用:BibTeX 定制自己的参考文献模板 bst_Xue Shengke 博客-CSDN博客 感谢作者付出,本文谨防CSDN丢失。 逆波兰语法是一种后缀语句。其内容并不算难,但是网上的资料几乎没有,而且也一般情况下没有人会去编写它。但是在需要自己设计 bibliography-style 文件 bst 之时,就需要了解这方面的知识了。 btxhak.pdf 是介绍 BibTeX 配置文件中的逆波兰代码基本语法的,位于系统 CTeX 安装目录下: /doc/texlive-base/bibtex/base/btxhak.pdf 命令 命令 功能 ENTRY 定义词条变量 EXECUTE 执行一个函数 FUNCTION 函数 INTEGERS 整数变量 ITERATE 执行一个函数 MACRO 宏定义:用于定义一些月份缩写,或是期刊名缩写 READ 未知 REVERSE 和 ITERATE 一样,不过以逆序执行 SORT 排序 STRINGS 字符串变量 内置函数这里一共介绍 37 个内置函数。每一个函数都以 $ 为结尾。这里的第一个和第二个指的是弹出堆栈的顺序(与压入堆栈时相反)。 built-in 函数 功能 > 弹出堆栈最上层的两个整数,并比较它们,如果第二个大于第一个,则压入 1;否则,压入 0 。 < 弹出堆栈最上层的两个整数,并比较它们,如果第一个大于第二个,则压入 1;否则,压入 0 。 = 弹出堆栈最上层的两个元素,并比较它们,如果二者相等,则压入 1;否则,压入 0 。 + 弹出堆栈最上层的两个整数,两者相加,结果压入栈 。 - 弹出堆栈最上层的两个整数,第二个减去第一个,结果压入栈 。 * 弹出堆栈最上层的两个元素,并将它们连接起来,顺序为 “第二个”“第一个”,并把结果压入堆栈中。 := 弹出堆栈最上层的两个元素,将第二个元素赋值给第一个(变量)。 add.period$ 弹出堆栈最上层的(字符串)元素,如果最后一个非 } 字符不是 .,?,或 !,加一个 .,然后将结果压入堆栈。 call.type$ 执行一个名称和词条类型一致的函数。比如词条类型是 book,那么执行 book 函数。 change.case$ 弹出堆栈最上层的两个字符串,根据第一个字符串的值来改变。如果第一个字符串是“t”,那么将第二个字符串的所有字母都改为小写(除了第一个字母,和冒号之后的第一个字母);如果第一个字符串是“l”,那么将第二个字符串所有的字母都转为小写;如果第一个字符串是“u”,那么将第二个字符串所有的字母都转为大写;然后将结果压入堆栈中。如果任意一个字符串的格式不对,那么会报错,并压入一个空字符串。如果类型对了,但是指定的值不是“l,t,u”,直接将第二个字符串压回堆栈。(“t”或“T”大小写没有区别) chr.to.int$ 弹出堆栈最上层的字符串,确保其只是一个简单的字符,将其转化为对应的 ASCII 码整数,然后压回堆栈。 cite$ 将这个 \\cite 命令参数的词条,以字符串形式压入堆栈。 duplicate$ 弹出堆栈最上层的一个元素,复制一个后,两个都压回堆栈。 empty$ 弹出堆栈最上层的一个元素,如果它是空白字符串或没有内容,压入 1 至堆栈;否则,压入 0 。 format.name$ 弹出堆栈最上层的三个元素,分别是字符串,整数,字符串。第一个字符串代表一个名字列表(人名),第二个整数表示将列表中哪一个人名提出,最后一个字符串代表如何进行格式化。最后将格式化后的人名结果压回堆栈。 if$ 弹出堆栈最上层的三个元素,按顺序分别是(两个函数,一个整数),如果整数大于 0,执行第二个函数,否则,执行第一个函数。 int.to.chr$ 弹出堆栈最上层的整数,将其转化为对应的 ASCII 码单字符,然后压回堆栈。 int.to.str$ 弹出堆栈最上层的整数,将其转化为唯一对应的字符串(根据编码),然后压回堆栈。 missing$ 弹出堆栈最上层的一个元素,如果它是没有内容,压入 1 至堆栈;否则,压入 0 。 newline$ 将输出缓存中的内容写入到 bbl 文件中去。如果缓存是空的,就会写入一个空行。由于 write$ 函数能进行合理的断行,所以只在需要一个空行或是断行的时候使用该函数。 num.name$ 弹出堆栈最上层的字符串,计算其中的人名的个数,其中每出现一个 “and” (忽略大小写),并之前有非空的字符,则计数加一。最后将结果压入堆栈。 pop$ 弹出堆栈最上层的元素,但是不打印它,用于除去一些不需要的堆栈内容。 preamble$ 将所有 @premable 字符串(来自数据文件 bib)连接起来,并压入堆栈中。 purify$ 弹出堆栈最上层的字符串,除去其中所有的非字母数字符号,除去特殊字符,但空格例外,(连接符和波浪符会变成空格),结果压回堆栈。 quote$ 将含有双引号的字符串压入堆栈。 skip$ 一个空操作,什么也不执行。 stack$ 将此时堆栈中的所有内容按顺序弹出,并打印出来。仅供调试使用。 substring$ 弹出堆栈最上层的三个元素,按顺序分别是(两个整数,长度 len 和起始位置 start,一个字符串),取出长度为 len,从第 start 个字符开始(索引从 1 开始)的子字符串,并压回堆栈。如果 start 是负数,只将第 -start 个字符取出,压回堆栈。 swap$ 交换堆栈顶部的两个元素。 text.length$ 弹出堆栈最上层的字符串,计算其中的文本字符的数量,带音标的字符也记作一个字符,括号不计。结果压入堆栈。 text.prefix$ 弹出堆栈最上层的两个元素,按顺序分别是(一个整数 len,一个字符串),取出从第一个文本字符开始,长度为 len 的子字符串,并压回堆栈。类似于 substring$,但是带音标的字符也记作一个文本字符,括号不会被当作文本字符。另外,此函数会添加需要的匹配的右括号。 top$ 将堆栈最上层的元素弹出,并输出到终端或 log 文件中,用于调试。 type$ 将当前词条的类型压入堆栈中(比如,book,atricle,inproceedings),但是如果类型未知,会压入空字符串。 warning$ 弹出堆栈最上层的元素,并打印到 warning 信息中。 while$ 弹出堆栈最上层的两个函数,只要留在堆栈中的整数值大于 0(在第一个函数中判断),就继续执行第二个函数。 width$ 弹出堆栈最上层的字符串,计算其宽度,并压入堆栈中。使用一些相对单位,目前是 cmr10 字体,六月 1987 版。该函数会将字符串按真实打印处理。此函数用于比较标签字符串。 write$ 弹出堆栈最上层的元素,并写入到 *.bbl 文件中 注意到 while$ 和 if$ 命令都需要 2 个函数在堆栈中。一种是方法是在函数名前加一个单引号,或是直接将一段语句用 { } 包住,即表示一个函数定义,比如: 1234label "" = 'skip$ { label "a" * 'label := }if$ 人名格式化人名在 bibtex 中被看作四部分,分别是:First name,von,Last name, Jr 。每一部分都对应着名字的一部分,比如:1author = "Charles Louis Xavier Joseph de la Vall{\\'e}e Poussin"如果按照如下的格式进行格式化:1"{vv~}{ll}{, jj}{, f{.}.}"那么会得到如下的人名输出:1de~la Vall{\\'e}e~Poussin, C.L.X.J.仔细分析:vv 表示将 de la 部分不缩写(单个 v 表示缩写),vv~表示必要时会加上波浪号;ll 表示姓不缩写(单个 l 表示缩写);, jj 表示先输出一个逗号加一个空格,然后将 Jr 部分按原样输出(此例中没有出现);, f{.}. 表示名进行缩写为一个大写首字母,然后紧跟一个句号,多个之间格式一致,即 C.L.X.J. 。","categories":[{"name":"LaTeX","slug":"LaTeX","permalink":"http://blog.karakarua.com/categories/LaTeX/"}],"tags":[{"name":"LaTeX","slug":"LaTeX","permalink":"http://blog.karakarua.com/tags/LaTeX/"},{"name":"BibTeX","slug":"BibTeX","permalink":"http://blog.karakarua.com/tags/BibTeX/"}]},{"title":"LaTeX参考文献模板 BST语法","slug":"LaTeX BST语法","date":"2021-05-21T14:33:40.000Z","updated":"2021-05-23T10:35:02.205Z","comments":true,"path":"2021/05/21/e1331dc9bf12/","link":"","permalink":"http://blog.karakarua.com/2021/05/21/e1331dc9bf12/","excerpt":"","text":"BST表达式语法变量前要加上',比如表示变量A:’A BST语法是逆波兰表达式,即后缀表达式。 A B + 的含义是A+B,遇到符号,就把栈顶的两个元素拿出来做运算再压回栈内。 部分语法可见连接:关于修改.bst文件(bibtex模板)的几点说明 - 简书 (jianshu.com) if条件判断1234condition{function1} % function1是条件为真时的逻辑{function2} % function2是条件为false的逻辑。if$ if的示例见链接:通过修改bst文件手动设置LaTeX参考文献格式 | Weitang Li’s blog (liwt31.github.io) 示例:自己用过的一个示例, 中文文献,超过3个人名时,用小明,小红,小白等,等前面不用逗号, 英文文献,超过3个人名时,用Tom,Alice,Bob, et al,et al前面要有逗号。 123456789language empty$ %增加判断语句,判断中文文献还是英文文献{ punc.comma * bbl.etal * num.names 'idx.names :=}{ bbl.etal * % 中文作者数量超限时,等字前面没有逗号。 num.names 'idx.names :=}if$ while循环123{condition}{function} %条件为真时的逻辑while$ BST内置函数一般以$结尾的大部分是内置函数 示例: num.names$ 弹出堆栈最上层的字符串,计算其中的人名的个数,其中每出现一个 “and” (忽略大小写),并之前有非空的字符,则计数加一。最后将结果压入堆栈。 empty$ 弹出堆栈最上层的一个元素,如果它是空白字符串或没有内容,压入 1 至堆栈;否则,压入 0 。 format.name$ 弹出堆栈最上层的三个元素,分别是字符串,整数,字符串。第一个字符串代表一个名字列表(人名),第二个整数表示将列表中哪一个人名提出,最后一个字符串代表如何进行格式化。最后将格式化后的人名结果压回堆栈。 更多内部函数见链接:BibTeX 定制自己的参考文献模板 bst_Xue Shengke 博客-CSDN博客 人名格式化参考链接:latex写姓名_Latex中bibtex的命名_weixin_39713686的博客-CSDN博客","categories":[{"name":"LaTeX","slug":"LaTeX","permalink":"http://blog.karakarua.com/categories/LaTeX/"}],"tags":[{"name":"LaTeX","slug":"LaTeX","permalink":"http://blog.karakarua.com/tags/LaTeX/"},{"name":"BibTeX","slug":"BibTeX","permalink":"http://blog.karakarua.com/tags/BibTeX/"}]},{"title":"LaTeX参考文献模板 BST语法","slug":"LaTeX图表间距","date":"2021-05-21T14:33:40.000Z","updated":"2021-05-30T13:09:07.747Z","comments":true,"path":"2021/05/21/763fada28e4a/","link":"","permalink":"http://blog.karakarua.com/2021/05/21/763fada28e4a/","excerpt":"","text":"需求调整图标题与图的间距 解决方式利用宏包caption,使用\\captionsetup,代码如下 1234567\\usepackage{caption}% 使用captionsetup设置标题属性\\captionsetup[figure]{ % 适用于图表题 position=bottom, belowskip=3bp, % 控制标题下方间距 aboveskip=3bp % 控制标题上方间距} 附录图表与文本的间距出处:18.1 图形的间距 (ctex.org) 表 18.1 中给出的长度控制着两幅图形之间或图形与正文之间 的间距。与其它的 LATEX 长度不同的是,这三个都是弹性长度,这就使得 它们可以缩短或拉长来更好的排版页面。这些长度可用 \\setlength 命令 来设定。例如: 1\\setlength{\\floatsep}{10pt plus 3pt minus 2pt} 将正常的 \\floatsep 的值设定为 10pt。并且在需要时可缩短到 8pt 或拉长到 13pt。|tex命令|解释||—|—||\\floatsep|出现在页面的顶部或底部的浮动对象之间的垂直距离。 缺省为 12pt plus 2pt minus 2pt。||\\textfloatsep|出现在页面的顶部或底部的浮动对象与文本之间的垂直距离。 缺省为 20pt plus 2pt minus 4pt。||\\intextsep|出现在页面中间的浮动对象(如使用了 h 选项 的浮动对象)与上下方文本之间的垂直距离。 缺省为 12pt plus 2pt minus 2pt。| 表 18.1 中给出的长度不会影响浮动页上各浮动对象之间 的距离。它们由表 18.2 中给出的长度控制。单位 fil 允许无限伸长,就像由 \\vfill 产生的垂直距离 一样。当在一段距离中出现多个 fil 时,它们将按比例 充满这段距离。 tex命令 解释 \\@fptop 浮动页中顶部的浮动对象上方的空白。 缺省为 0pt plus 1.0fil。 \\@fpsep 浮动页中的浮动对象之间的距离。 缺省为 8pt plus 2.0fil。 \\@fpbot 浮动页中底部的浮动对象下方的空白。 缺省为 0pt plus 1.0fil。 在表 18.2 中的长度名字前的 @ 表示 这是一个 LATEX 内部命令18.1。所以,所有改变这些长度的 \\setlength 命令都必须放到 \\makeatletter 和 \\makeatother 之间。例如: 123\\makeatletter \\addtolength{\\@fpsep}{4pt} \\makeatother 将浮动页中浮动对象之间的距离增加了 4pt。 图形与标题的间距出处:18.3 图形与标题的间距 (ctex.org) LATEX 假定图形的标题位于图形的下方,故而在标题上方保留了更 多的空白。因此12345\\begin{figure} \\centering \\caption{Caption Above Graphic} \\includegraphics[width=2in]{graphic.eps} \\end{figure}生成的图 18.1 中标题和图形非常接近。 标题上下方的间距由长度 \\abovecaptionskip 和 \\belowcaptionskip (缺省分别为 10pt 与零)。可以用标准的 LATEX 命令 \\setlength 和 \\addtolength 来修改这些长度。 例如:1234567\\begin{figure} \\setlength{\\abovecaptionskip}{0pt} \\setlength{\\belowcaptionskip}{10pt} \\centering \\caption{Caption Above Graphic} \\includegraphics[width=2in]{graphic.eps} \\end{figure}得到图 18.2。其中标题的上方没有额外的 空白,与图形之间则有 10pt 的距离。如果一个文档的所有浮动对象的标题都位于该对象的上方,那么可将 命令12\\setlength{\\abovecaptionskip}{0pt}\\setlength{\\belowcaptionskip}{10pt}放到导言区里,从而对整个文档都起作用。如果只是有一部分标题 要求位于浮动对象的上方,那么可定义如下的命令:1234\\newcommand{\\topcaption}{% \\setlength{\\abovecaptionskip}{0pt}% \\setlength{\\belowcaptionskip}{10pt}% \\caption}在希望得到上方标题的时候可用 \\topcaption{标题文本} 来代替 \\caption{标题文本} 即可。 图形的放置图形(figure)环境有一个可选参数项允许用户来指示图形有可能 被放置的位置。这一可选参数项可以是下列字母的任意组合。 h当前位置。 将图形放置在 正文文本中给出该图形环境的地方。如果本页所剩的页面不够, 这一参数将不起作用。 t顶部。 将图形放置在页面的顶部。 b底部。 将图形放置在页面的底部 16.1。 p浮动页。 将图形放置在一只允许 有浮动对象的页面上。注: 如果在图形环境中没有给出上述任一参数,则缺省为 [tbp]。 给出参数的顺序不会影响到最后的结果。因为在考虑这些参数时 LATEX 总是尝试以 h-t-b-p 的顺序来确定图形的位置。所以 [hb] 和 [bh] 都使 LATEX 以 h-b 的顺序来排版。 给出的参数越多, LATEX 的排版结果就会越好。 [htbp], [tbp], [htp], [tp] 这些组合得到的效果不错。 只给出单个的参数项极易引发问题16.2。 如果该图形不适合所指定的位置,它就会被搁置并阻碍对后面的图形 的处理。一旦这些阻塞的图形数目超过了 18 幅这一 LATEX 所能容许 的最大值,就会产生 ``Too Many Unprocessed Floats’’ 的错误(见 第 16.3 节)。 当 LATEX ”试图“ 放置一浮动图形时, 它将遵循以下规则: 图形只能置于由位置参数所确定的地点。 图形的放置不能造成超过版心的错误(overfull page)。 图形只能置于当前页或后面的页中16.3。所以图形只能 “ 向后浮动” 而 不能 “向前浮动”。 图形必须按顺序出现。这样只有当前面的图形都被放置好之后才能被放置。 只要前面有未被处理的图形,一幅图形就不会被放在当前位置。 一幅 “不可能放置” 的图形将阻碍它后面的图形的放置。直到 文件结束或达到 LATEX 的浮动限制。参见第 16.4 节。 同样地,一表格也只能在其前面的表格都被处理完后才能被放置。 不过,表格在排版时是跳过图形而单独处理的。 必须符合在第 17 章中给出的审美条件。例如,一页上的 浮动对象的数目不能超过 totalnumber。 在浮动位置选项前加上一个惊叹号(如 \\begin{figure}[!ht]) 会使 LATEX 忽略应用于文本页的审美条件,试图用最严格的标准来 放置浮动图形。不过, ! 不会影响应用于浮动页的审美条件。 latex中长度单位 单位 名称 说明 mm 毫米 1 mm = 2.845 pt pt 点 1 pt = 0.351 mm bp 大点 1 bp = 0.353 mm > 1 pt dd 迪多 1 dd = 0.376 mm = 1.07 pt pc 排卡 1 pc = 4.218 mm = 12 pt sp 定标点 65536 sp = 1 pt cm 厘米 1 cm= 10 mm= 28.453 pt cc 西塞罗 1 cc= 4.513 mm= 12 dd = 12.84 pt in 英寸 1 in = 25.4 mm = 72.27 pt ex ex 1 ex = 当前字体尺寸中 x 的高度 em em 1 em = 当前字体尺寸中 M 的宽度 参考链接: 关键链接: LaTeX技巧884:如何用caption宏包格式化图表标题和子标题 latex如何调整段落、图片或者表格上下的空白 18.1 图形的间距 (ctex.org) 18.3 图形与标题的间距 (ctex.org) 非关键链接: latex图表 Control spacing around table caption 附录CTeX 在线文档—Contents (ctex.org)","categories":[{"name":"LaTeX","slug":"LaTeX","permalink":"http://blog.karakarua.com/categories/LaTeX/"}],"tags":[{"name":"LaTeX","slug":"LaTeX","permalink":"http://blog.karakarua.com/tags/LaTeX/"}]},{"title":"GitHub Wiki制作PDF","slug":"GitHub Wiki制作PDF","date":"2021-05-16T15:36:00.000Z","updated":"2021-06-01T03:13:58.878Z","comments":true,"path":"2021/05/16/eb58d48c52cb/","link":"","permalink":"http://blog.karakarua.com/2021/05/16/eb58d48c52cb/","excerpt":"","text":"GitHub Wiki制作PDF需求将GitHub的wiki导出PDF打印出来看 工具主要工具:github-wikito-converter辅助工具:wkhtmltopdf 步骤 下载wiki12## wiki可以通过git克隆,一般在wiki的右下角git clone github.com/xxx/yyy.wiki.git 安装wkhtmltopdf 将wkhtmltopdf加入环境变量 安装node npm安装github-wikito-converter1npm install -g github-wikito-converter 利用github-wikito-converter导出PDF命令:12## NAME是输出的pdf文件名,TITLE是PDF内容的标题,PATH是wiki的路径gwtc -f pdf -n NAME -t TITLE -v PATH 附github-wikito-converter的帮助手册12345678910111213141516171819202122Usage: gwtc [options] <wiki-dir>Convert a wikiOptions: -V, --version output the version number -f, --format <format> Format to convert to. Either html, pdf, or all [default: html] (default: "html") -o, --output <output-dir> Output dir [default: './'] (default: "./") -n, --file-name <file-name> Output file name [default: 'documentation'] (default: "documentation") -t, --title <title> Wiki title [default: Documentation] (default: "Documentation ") -d, --disable-inline-assets Disable inlining of css & js in html document --logo-img <logo-file> Logo image file --footer <footer> Wiki footer --pdf-page-count Enable PDF page count --toc <toc-file> Wiki TOC file --toctitle <toc title> Title of the toc [default: Table of contents] (default: "Table of contents") --toc-level <level> Table of contents deep level [default: 3] (default: 3) --highlight-theme <theme> Highlighter theme [default: github] (default: "github") --css <css-file> Additional CSS file -v --verbose Verbose mode -h, --help output usage information","categories":[{"name":"Git","slug":"Git","permalink":"http://blog.karakarua.com/categories/Git/"}],"tags":[{"name":"Git","slug":"Git","permalink":"http://blog.karakarua.com/tags/Git/"}]},{"title":"CentOS使用ptrash防止rm误删","slug":"CentOS使用ptrash防止rm误删","date":"2021-04-10T16:00:00.000Z","updated":"2021-09-21T04:06:45.978Z","comments":true,"path":"2021/04/10/4abf9b4212e8/","link":"","permalink":"http://blog.karakarua.com/2021/04/10/4abf9b4212e8/","excerpt":"","text":"背景我删除并重建了某个文件夹,由于最近的历史命令中包含rm -rf,向上找命令时误触Enter键,导致误删除了重建的文件夹。 解决办法如果习惯了rm -rf后,要记住:常在河边走,哪有不湿鞋,偶尔失误可能就是大灾难。所以最好的解决办法就是:习惯其他删除命令,忘记rm -rf! 比如使用trash-cli或者ptrash等将移入文件至回收站的命令 安装ptrash1sudo yum install ptrash 日后需要删除文件时,使用命令1ptrash xxxxxx会被移动到~/.trash目录下。 补充另外一种极易发生误删除的情况: 使用rm -rf选择要删除的文件时,使用tab补全文件失败,导致删错文件。 尽管是tab的锅,但无论如何还是避免使用rm吧。 提示在删除后的第一时间里,可以尝试过恢复数据,使用工具包括extundelete(适合ext3或者ext4文件系统)、testdisk(适合xfs文件系统,比如我的CentOS7就是xfs文件系统) 使用的工具是:TestDisk,下载链接:TestDisk转载:linux误删文件找回方法(xfs文件系统) 查看系统分区的文件类型命令:df -T 但是,即便上述恢复工具也不一定完整的找回删除的文件,所以: 放弃数据可以恢复的幻想,从现在开始改掉以往使用rm -rf删除文件的习惯! 积极备份,如果误删尚可以恢复! 望吸取教训!","categories":[{"name":"Linux","slug":"Linux","permalink":"http://blog.karakarua.com/categories/Linux/"}],"tags":[{"name":"Linux","slug":"Linux","permalink":"http://blog.karakarua.com/tags/Linux/"},{"name":"CentOS","slug":"CentOS","permalink":"http://blog.karakarua.com/tags/CentOS/"}]},{"title":"Nginx反向代理多个docker容器","slug":"Nginx反向代理多个docker容器","date":"2021-04-03T07:23:00.000Z","updated":"2021-08-08T09:16:41.033Z","comments":true,"path":"2021/04/03/73582942da17/","link":"","permalink":"http://blog.karakarua.com/2021/04/03/73582942da17/","excerpt":"","text":"场景:配置一个docker nginx容器,希望可以反向代理多个docker容器。 注意 所有docker容器(包括nginx)之间是相互隔离的,即nginx容器的不能直接访问其他docker容器,而且nginx容器不可以与其他容器连接到一个bridge网络内(这么做会失去隔离性)。 所有的docker容器都可以访问外网(即docker网络以外的网络,包括宿主机和因特网) 解决方式 除了docker-nginx以外的每个容器对外映射端口,通过宿主机的映射端口可以直接访问docker容器。 配置nginx容器的nginx.conf,将特定域名和端口的服务反向代理到宿主机的不同映射端口(此映射端口关联的是docker容器) 1234567891011121314server { listen 80; server_name md.me.com; location / { # 172.17.0.1是宿主机docker0网卡的位置 # 如果将docker-nginx连接到host网络,该IP可直接写localhost proxy_pass http://172.17.0.1:88; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }} 其中,proxy_pass的地址——172.17.0.1是宿主机的docker0网卡的地址,因机器而异。 原因:宿主机上任意一个网卡的内网IP地址都可以代表宿主机本身,由于docker0的地址比较稳定,故采用该地址。 如果直接将docker-nginx链接到host网络,也就是设定docker-nginx与主机同享物理网络,则proxy_pass可以直接填写localhost。 如果docker-nginx链接的是bridge网络,localhost代表的地址是容器内部,这一点请与host网络相区分。 这部分涉及到的知识点是“docker访问宿主机网络”,此处提供一处链接:Docker容器访问宿主机网络的方法 优势减少服务器防火墙或者安全组暴露的端口,只通过域名对外访问 更直观一点的图:参考博客:Docker nginx部署二级域名访问多个web项目 本解决方案的架构图与该博客的架构类似,如下图所示。 最后,在nginx.conf所在目录,使用docker构建并启动nginx容器,命令如下 12sudo docker run -d --name proxy-nginx -p 80:80 -v \\$(pwd)/nginx.conf:/etc/nginx/nginx.conf --restart always nginx 参考链接: Docker nginx部署二级域名访问多个web项目 nginx配置:server_name的作用 Docker容器访问宿主机网络的方法","categories":[{"name":"Nginx","slug":"Nginx","permalink":"http://blog.karakarua.com/categories/Nginx/"}],"tags":[{"name":"Docker","slug":"Docker","permalink":"http://blog.karakarua.com/tags/Docker/"},{"name":"Nginx","slug":"Nginx","permalink":"http://blog.karakarua.com/tags/Nginx/"}]},{"title":"docker-compose中顶级volumes自定义绑定路径","slug":"docker-compose中volumes自定义绑定路径","date":"2021-04-02T11:15:00.000Z","updated":"2021-04-02T12:06:12.341Z","comments":true,"path":"2021/04/02/0326889327e6/","link":"","permalink":"http://blog.karakarua.com/2021/04/02/0326889327e6/","excerpt":"","text":"问题举例子能说明问题: docker-compose.yml中的一段代码: 1234567891011121314151617service: database: image: mariadb:10 environment: - MYSQL_USER=xxx - MYSQL_PASSWORD=xxx - MYSQL_DATABASE=xxx - MYSQL_ALLOW_EMPTY_PASSWORD=true # volume下使用 volumes: - database:/var/lib/mysql volumes: # 由docker volume创建volumes定义的卷,可以通过参数指定属性 # 可以不填写任何参数,按默认属性在/var/lib/docker/volumes下创建卷 ### 本文想在这里自定义路径 database: 解决方式123456volumes: database: driver_opts: #更多选项可以查看Linux的Mount命令,与Mount相似 type: ext4 # 文件类型,必需 o: bind device: <absolute path> # 绝对路径 附录: 本文问题的缘由:一开始只是以为顶级volumes的含义只是用来缩写,由于services下<path1>:<path2>中的宿主机路径太长,想缩减<path1>:<path2>的长度,使代码更加美观。 实际上对顶级volumes的理解是不对的,顶级volumes的意义并不是为缩写。 实现上述目的应该通过下面方式实现。 123456services: database: volumes: type: bind source: <path1> # 绝对路径或是相对路径 target: <path2> # 绝对路径或是相对路径 那顶级volume的含义真正是什么? 实际上,顶级volums的含义是指用docker volume create命令创建卷,并交由docker volume管理这些卷,关联容器中的路径。 那本文的一顿操作到底做了些什么? 实际上宿主机卷仍然是由docker volume管理并关联容器中的路径。也就是说,选项o和device只不过是将docker volume管理的卷再次绑定到device指向的位置 docker volume管理的卷截图 可以验证的是:device指向的位置、/var/lib/docker/volume/<name>/_data、容器内的路径,三者的内容是一样的。 device指向的位置截图: docker volume默认的宿主机位置(指向/var/lib/docker/volumes/<name>/_data) docker容器内部目录: 来自docker-compose中关于driver_opts的解释:docker-compose driver_opts 引用:以键值对的形式指定用来传递给该数据卷所使用的数据卷驱动的列表选项 123456volumes: example: driver_opts: type: "nfs" o: "addr=10.40.0.199,nolock,soft,rw" device: ":/docker/example" 这里的例子与本文很相似。 所以,本文的意义可能在于将docker volume管理的卷,再克隆一份到其他路径,多处进行持久化。🤔🤔🤔 所以,从一开始理解错了顶级volumes的用法,从而走了弯路。 最后,通过这次经历,我也有些明白了顶级volumes的含义。 参考链接: How to set a path on host for a named volume in docker-compose.yml Docker Compose Relative paths vs Docker volume Docker Compose配置文件详解(V3)","categories":[{"name":"Docker","slug":"Docker","permalink":"http://blog.karakarua.com/categories/Docker/"}],"tags":[{"name":"Docker","slug":"Docker","permalink":"http://blog.karakarua.com/tags/Docker/"}]},{"title":"CnetOS中配置vi语法高亮","slug":"CnetOS中配置vi语法高亮","date":"2021-04-02T07:17:00.000Z","updated":"2021-04-02T11:31:14.398Z","comments":true,"path":"2021/04/02/0045d88f497a/","link":"","permalink":"http://blog.karakarua.com/2021/04/02/0045d88f497a/","excerpt":"","text":"问题CentOS-minimal版本默认安装的是vi-minimal,不具备语法高亮,表现为整个屏幕全是一种颜色 解决步骤 安装vim-common和vi-enhanced 12yum install -y vim-common vim-enhanced# 或者只安装vim-enhanced,vim-common会作为依赖被安装 配置alias vi=’vim’(原因是上面两个组件只对vim起作用,不会影响vi) 123456789# 全局配置,对所有用户都起作用vi /etc/bashrc#--INSERT模式下alias vi='vim'# -------------或者是---------# 局部配置,只对当前用户都起作用vi ~/.bashrc#--INSERT模式下alias vi='vim' 参考链接:CentOS中的vi语法高亮","categories":[{"name":"Linux","slug":"Linux","permalink":"http://blog.karakarua.com/categories/Linux/"}],"tags":[{"name":"Linux","slug":"Linux","permalink":"http://blog.karakarua.com/tags/Linux/"},{"name":"CentOS","slug":"CentOS","permalink":"http://blog.karakarua.com/tags/CentOS/"}]},{"title":"CentOS中firewalld的Zone是什么","slug":"CentOS中firewalld的Zone是什么","date":"2021-04-01T13:56:00.000Z","updated":"2021-04-02T11:30:26.162Z","comments":true,"path":"2021/04/01/520417809f0a/","link":"","permalink":"http://blog.karakarua.com/2021/04/01/520417809f0a/","excerpt":"","text":"Zone是什么?Zone是firewalld引入的一种安全级别,定义不同的规则,以不同的方式处理数据包。 Zone的种类CentOS自带的zone有以下几种: drop: 丢弃所有进入的包,而不给出任何响应 block: 拒绝所有外部发起的连接,允许内部发起的连接 public: 允许指定的进入连接 external: 同上,对伪装的进入连接,一般用于路由转发 dmz: 允许受限制的进入连接 work: 允许受信任的计算机被限制的进入连接,类似 workgroup home: 同上,类似 homegroup internal: 同上,范围针对所有互联网用户 trusted: 信任所有连接 来自Firewalld的更详细的解释: 丢弃(drop) 任何接收的网络数据包都被丢弃,没有任何回复。仅能有发送出去的网络连接。 限制(block) 任何接收的网络连接都被 IPv4 的 icmp-host-prohibited 信息和 IPv6 的 icmp6-adm-prohibited 信息所拒绝。 公共(public) 在公共区域内使用,不能相信网络内的其他计算机不会对您的计算机造成危害,只能接收经过选取的连接。 外部(external) 特别是为路由器启用了伪装功能的外部网。您不能信任来自网络的其他计算,不能相信它们不会对您的计算机造成危害,只能接收经过选择的连接。 非军事区(dmz) 用于您的非军事区内的电脑,此区域内可公开访问,可以有限地进入您的内部网络,仅仅接收经过选择的连接。 工作(work) 用于工作区。您可以基本相信网络内的其他电脑不会危害您的电脑。仅仅接收经过选择的连接。 家庭(home) 用于家庭网络。您可以基本信任网络内的其他计算机不会危害您的计算机。仅仅接收经过选择的连接。 内部(internal) 用于内部网络。您可以基本上信任网络内的其他计算机不会威胁您的计算机。仅仅接受经过选择的连接。 信任(trusted) 可接受所有的网络连接。 有时,当CentOS中安装了docker后,zone中会多出一个docker,可以认为是一种自定义的zone。 Zone的字段zone通过字段来表达过滤规则和处理动作。 firewalld将按照过滤规则字段的优先级,唯一确定使用哪个zone,然后按照该zone指定的动作处理。 因此,每个zone可以修改字段值,更加详细的描述zone的过滤规则和处理动作。 zone的字段的过滤规则: service:表示服务,根据服务确定过滤规则 port:端口,使用port可以不通过service而直接对端口进行设置 interface:表示网卡,根据进入的网卡确定过滤规则 source:源地址,可以是ip地址也可以是ip地址段 zone的字段的处理动作: target:目标,可以理解为默认行为,有四个可选值:default、ACCEPT、%%REJECT%%、DROP,如果不设置默认为default ACCEPT:通过这个包。 %%REJECT%%:拒绝这个包,并返回一个拒绝的回复。 DROP:丢弃这个包,不回复任何信息。 default:不做任何事情。该区域不再管它,把它踢到“楼上” icmp-block:icmp报文阻塞,可以按照icmp类型进行设置 masquerade:ip地址伪装,也就是按照源网卡地址进行NAT转发 forward-port:端口转发 rule: 自定义规则 zone的过滤规则的优先级 source(先按照源地址确定zone) interface(如果无法根据source确定zone,则再按interface确定zone) firewalld.conf(如果上述两种方式都无法确定zone,则最后根据默认设定确定zone) 查看当前起作用的zone1sudo firewall-cmd --get-active-zones 参考链接: Firewalld firewall防火墙详解(持续更新中…) 用活firewalld防火墙中的zone","categories":[{"name":"Linux","slug":"Linux","permalink":"http://blog.karakarua.com/categories/Linux/"}],"tags":[{"name":"Linux","slug":"Linux","permalink":"http://blog.karakarua.com/tags/Linux/"},{"name":"CentOS","slug":"CentOS","permalink":"http://blog.karakarua.com/tags/CentOS/"}]},{"title":"docker安装HedgeDoc/CodiMD/HackMD","slug":"docker搭建hedgedoc","date":"2021-04-01T08:45:00.000Z","updated":"2021-04-07T09:39:51.185Z","comments":true,"path":"2021/04/01/32c9782a3042/","link":"","permalink":"http://blog.karakarua.com/2021/04/01/32c9782a3042/","excerpt":"","text":"HedgeDoc介绍HedgeDoc是一款在线Markdown软件,它的强大之处在于可以协作编写文档。 HedgeDoc的前身是codiMD,即hackMD的社区版分支(hackMD CE),它的历史:HedgeDoc History HedgeDoc的官网主页:HedgeDoc 关于HedgeDoc的前身的介绍:CodiMd 安装教程官方教程:123git clone https://github.com/hedgedoc/container.git hedgedoc-containercd hedgedoc-containerdocker-compose up hedgedoc-container文件下有官方给定的构建文件docker-compose.yml,可以自定义修改后使用docker-compose up -d创建并运行容器 自定义HedgeDoc我们在官方给出的docker-compose.yml文件中 使用MySQL数据库 利用顶级secrets的子键file替换明文用户密码、mysql的URL链接 docker-compose.yml:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140# Using version 3 to provide play-with-docker badge# You can change to version 2 without breaking.#version: '2'version: '3.5'services: # database: # Don't upgrade PostgreSQL by simply changing the version number # You need to migrate the Database to the new PostgreSQL version # image: postgres:9.6-alpine #mem_limit: 256mb # version 2 only #memswap_limit: 512mb # version 2 only #read_only: true # not supported in swarm mode please enable along with tmpfs #tmpfs: # - /run/postgresql:size=512K # - /tmp:size=256K # environment: # - POSTGRES_USER=hedgedoc # - POSTGRES_PASSWORD=password # - POSTGRES_DB=hedgedoc # volumes: # - database:/var/lib/postgresql/data # networks: # backend: # restart: always # MySQL example # Most of the documentation that applies to PostgreSQL applies also to MySQL database: # # You should be able to upgrade MySQL without problems # # but to make sure no even when a problem appears you # # should have a backup image: mariadb:10 container_name: hedgedoc_db environment: # - MYSQL_USER_FILE=/run/secrets/dbuser # - MYSQL_PASSWORD_FILE=/run/secrets/dbpassword - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/dbrootpassword - MYSQL_USER_FILE=/run/secrets/dbuser - MYSQL_PASSWORD_FILE=/run/secrets/dbpassword - MYSQL_DATABASE=hedgedoc - MYSQL_ALLOW_EMPTY_PASSWORD=true volumes: - database:/var/lib/mysql # # This config provides UTF-8 support to the database by default # # If this config is not used, HedgeDoc breaks as it tries to write # # UTF-8 to a latin database. - ./resources/utf8.cnf:/etc/mysql/conf.d/utf8.cnf networks: backend: restart: always # secret names in this service secrets: - dbuser - dbpassword - dbrootpassword hedgedoc: # Uncomment the following section to build the image yourself: #build: # context: . # dockerfile: debian/Dockerfile # args: # - "VERSION=master" # - "HEDGEDOC_REPOSITORY=https://github.com/hedgedoc/hedgedoc.git" image: quay.io/hedgedoc/hedgedoc:1.7.2 container_name: hedgedoc #mem_limit: 256mb # version 2 only #memswap_limit: 512mb # version 2 only #read_only: true # not supported in swarm mode, enable along with tmpfs #tmpfs: # - /tmp:size=10M # # Make sure you remove this when you use filesystem as upload type # - /hedgedoc/public/uploads:size=10M environment: # DB_URL is formatted like: <databasetype>://<username>:<password>@<hostname>:<port>/<database> # Other examples are: # - For details see the official sequelize docs: http://docs.sequelizejs.com/en/v3/ # - sqlite:///data/sqlite.db (NOT RECOMMENDED) # - CMD_DB_URL=mysql://hedgedoc:password@database:3306/hedgedoc # - CMD_DB_URL=postgres://hedgedoc:password@database:5432/hedgedoc # ATTITION: CMD_DB_URL will be meaningless when secret dbURL is defined. it can be deleted. # DB_URL has been passed via secret dbURL, see resources/docker-entrypoint.sh line 18 - CMD_DB_URL=/run/secrets/dbURL volumes: - uploads:/hedgedoc/public/uploads # ports: # # Ports that are published to the outside. # # The latter port is the port inside the container. It should always stay on 3000 # # If you only specify a port it'll published on all interfaces. If you want to use a # # local reverse proxy, you may want to listen on 127.0.0.1. # # Example: # # - "127.0.0.1:3000:3000" # - "3000:3000" ports: - "88:3000" networks: backend: restart: always depends_on: - database # secret names in this service secrets: - dbURL # Define networks to allow best isolationnetworks: # Internal network for communication with PostgreSQL/MySQL backend:# Define named volumes so data stays in place# volumes:# # Volume for PostgreSQL/MySQL database# database:# driver_opts:# type: ext4# o: bind# device: "/home/deepin/Applications/hedgedoc-container/database"# uploads:# driver_opts:# type: ext4# o: bind# device: "/home/deepin/Applications/hedgedoc-container/uploads"volumes: database: uploads:secrets: # source of all docker secrets dbuser: file: ./dbuser.txt dbpassword: file: ./dbpassword.txt dbrootpassword: file: dbrootpassword.txt dbURL: file: ./dbURL.txt 在服务端防火墙中开启http服务和88端口如果买了大厂ECS服务器,请在提供商网站内更改安全组,开启88端口。(一般默认开启http服务) 如果是本地服务器 centos主机开启http服务和88端口:123456# 针对外开启http服务sudo firewall-cmd --zone=public --add-service=http --permanent# 对外开放88端口sudo firewall-cmd --zone=public --add-port=88/tcp --permanent# 重新加载firewall规则sudo firewall-cmd --reload 查看是否开启成功: 12sudo firewall-cmd --zone=public --list-services #正确结果包含httpsudo firewall-cmd --zone=public --list-ports # 正确结果出现88/tcp Ubuntu开启端口(默认http等服务已开启)1sudo ufw allow 88 访问< your domain:88 >,可以看到HedgeDoc页面(与https://demo.hedgedoc.org/相同)","categories":[{"name":"HedgeDoc","slug":"HedgeDoc","permalink":"http://blog.karakarua.com/categories/HedgeDoc/"}],"tags":[{"name":"Docker","slug":"Docker","permalink":"http://blog.karakarua.com/tags/Docker/"},{"name":"HedgeDoc","slug":"HedgeDoc","permalink":"http://blog.karakarua.com/tags/HedgeDoc/"},{"name":"CodiMD","slug":"CodiMD","permalink":"http://blog.karakarua.com/tags/CodiMD/"},{"name":"HackMD","slug":"HackMD","permalink":"http://blog.karakarua.com/tags/HackMD/"}]},{"title":"Centos开启防火墙超时失败","slug":"CentOS开启防火墙失败","date":"2021-04-01T08:11:00.000Z","updated":"2021-04-02T11:30:46.811Z","comments":true,"path":"2021/04/01/d73364c2eba5/","link":"","permalink":"http://blog.karakarua.com/2021/04/01/d73364c2eba5/","excerpt":"","text":"CnetOS命令CentOS关闭防火墙命令: 123systemctl stop firewalld# 或者service firewalld stop CentOS启动防火墙 123systemctl start firewalld# 或者service firewalld start 关闭防火墙后再次启动防火墙,发现无法启动,错误是超时:123[root@localhost ~]service firewalld restartRedirecting to /bin/systemctl restart firewalld.serviceJob for firewalld.service failed because a timeout was exceeded. See "systemctl status firewalld.service" and "journalctl -xe" for details. 解决办法:需要关闭firewalld进程,再启动firewalld 123systemctl stop firewalld #停止firewalldpkill -f firewalld #关闭firewalld进程systemctl start firewalld #启动firewalld 官方解释可能原因: systemd didn’t know about the process that it didn’t start in the first place of course 参考链接:Centos执行开启防火墙命令超时","categories":[{"name":"Linux","slug":"Linux","permalink":"http://blog.karakarua.com/categories/Linux/"}],"tags":[{"name":"Linux","slug":"Linux","permalink":"http://blog.karakarua.com/tags/Linux/"},{"name":"CentOS","slug":"CentOS","permalink":"http://blog.karakarua.com/tags/CentOS/"}]},{"title":"转载:MySQL将查询结果存入新表","slug":"转载:MySQL将查询结果存入新表","date":"2021-03-30T09:28:16.000Z","updated":"2021-03-30T09:34:51.165Z","comments":true,"path":"2021/03/30/505a385dca9d/","link":"","permalink":"http://blog.karakarua.com/2021/03/30/505a385dca9d/","excerpt":"","text":"链接:MySQL将查询结果插入到另一个表中 如果两张表(查询表和插入表)的字段一致,并且希望插入查询表的全部数据,可以用此方法 123INSERT INTO 目标表 SELECT * FROM 来源表;-- 示例:INSERT INTO user_login1 SELECT * FROM user_login; 如果只希望插入指定字段,可以用此方法,注意两表的字段类型必须一致 123INSERT INTO 目标表(字段1,字段2,...) SELECT 字段1,字段2,... FROM 来源表;-- 示例:INSERT INTO user_info(login_name,password) SELECT login_name,password FROM user_login","categories":[{"name":"SQL","slug":"SQL","permalink":"http://blog.karakarua.com/categories/SQL/"}],"tags":[{"name":"SQL","slug":"SQL","permalink":"http://blog.karakarua.com/tags/SQL/"}]},{"title":"查询LaTeX英文论文中标点后无空格的句子","slug":"LaTeX英文论文查询标点后没有空格的句子","date":"2021-03-21T16:00:00.000Z","updated":"2021-03-22T08:30:30.963Z","comments":true,"path":"2021/03/21/a7d4b89ae5e4/","link":"","permalink":"http://blog.karakarua.com/2021/03/21/a7d4b89ae5e4/","excerpt":"","text":"背景LaTeX英文论文中,标点符号后需要存在空格,即开启下一句需要先输入一个空格。 在编写论文时有时会忘记敲空格,我们需要找出这些不符合规范的句子。 举个例子就明白了: 句子A It is a dog,it is a cat.It is a bird. 这么写是不规范的,正确的应该是 句子B It is a dog, it is a cat. It is a bird. 应该能看出差异,我们的目标是找出句子A这样的句子。 解决办法手动查找是很累的的,这里采用正则表达式寻找: 1(?<!\\{.*|\\&.*|\\[.*)\\w[,.:\\)]\\w 解读正则表达式从后向前说: \\w:下一句的英文字符(一般来说英文句子都是以英文开头,这里没有考虑数字开头) [,.:\\)]:代表标点符号,这里可以根据具体需求自由书写。 (?<!\\{.*|\\&.*|\\[.*): 这是一个整体符号,大框架是(?<!pattern),是一种断言结构,含义是排除左侧字符是pattern的字符串。 (?<!\\{.*|\\&.*|\\[.*)的含义是排除左侧是{或者左侧是[或者左侧是&的字符,用于排除图表等结构中的字符,可以根据具体需求,向(?<!pattern)结构中添加字符,以|间隔。 这样,我们就可以筛选类似句子A这种不规范的书写方式。 当然,这个正则表达式还有考虑不周之处,但基本可以解决我们的需求。 附录类似(?<!pattern)的正则表达式还有(?<=pattern)、(?=pattern)、(?!pattern)、(?:pattern),其含义如下表所示(来自百度百科:正则表达式) 元字符 描述 (?:pattern) 非获取匹配,匹配pattern但不获取匹配结果,不进行存储供以后使用。这在使用或字符“(\\ )”来组合一个模式的各个部分时很有用。例如“industr(?:y\\ ies)”就是一个比“industry\\ industries”更简略的表达式。 (?=pattern) 非获取匹配,正向肯定预查,在任何匹配pattern的字符串开始处匹配查找字符串,该匹配不需要获取供以后使用。例如,“Windows(?=95\\ 98\\ NT\\ 2000)”能匹配“Windows2000”中的“Windows”,但不能匹配“Windows3.1”中的“Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。 (?!pattern) 非获取匹配,正向否定预查,在任何不匹配pattern的字符串开始处匹配查找字符串,该匹配不需要获取供以后使用。例如“Windows(?!95\\ 98\\ NT\\ 2000)”能匹配“Windows3.1”中的“Windows”,但不能匹配“Windows2000”中的“Windows”。 (?<=pattern) 非获取匹配,反向肯定预查,与正向肯定预查类似,只是方向相反。例如,“(?<=95\\ 98\\ NT\\ 2000)Windows”能匹配“2000Windows”中的“Windows”,但不能匹配“3.1Windows”中的“Windows”。*python的正则表达式没有完全按照正则表达式规范实现,所以一些高级特性建议使用其他语言如java、scala等 (?<!pattern) 非获取匹配,反向否定预查,与正向否定预查类似,只是方向相反。例如“(?<!95\\ 98\\ NT\\ 2000)Windows”能匹配“3.1Windows”中的“Windows”,但不能匹配“2000Windows”中的“Windows”。*python的正则表达式没有完全按照正则表达式规范实现,所以一些高级特性建议使用其他语言如java、scala等","categories":[{"name":"Misc","slug":"Misc","permalink":"http://blog.karakarua.com/categories/Misc/"}],"tags":[{"name":"LaTeX","slug":"LaTeX","permalink":"http://blog.karakarua.com/tags/LaTeX/"},{"name":"Regex","slug":"Regex","permalink":"http://blog.karakarua.com/tags/Regex/"}]},{"title":"转载-数据结构与算法知识点思维导图","slug":"转载-数据结构与算法知识点思维导图","date":"2021-03-15T16:00:00.000Z","updated":"2021-03-16T11:04:24.707Z","comments":true,"path":"2021/03/15/fb2f8daaafda/","link":"","permalink":"http://blog.karakarua.com/2021/03/15/fb2f8daaafda/","excerpt":"","text":"转自知乎如何系统地学习数据结构与算法?的数据结构与算法知识点思维导图: 其他:一个算法相关的网站:算法网,网站内也包含一些其他的知识,如后端开发、移动开发、大数据等。","categories":[{"name":"Code","slug":"Code","permalink":"http://blog.karakarua.com/categories/Code/"}],"tags":[{"name":"Code","slug":"Code","permalink":"http://blog.karakarua.com/tags/Code/"}]},{"title":"docker常用命令","slug":"docker常见命令","date":"2021-03-09T16:00:00.000Z","updated":"2021-04-03T04:53:15.101Z","comments":true,"path":"2021/03/09/2d503f39a87f/","link":"","permalink":"http://blog.karakarua.com/2021/03/09/2d503f39a87f/","excerpt":"","text":"镜像操作 docker pull IMAGE-NAME [:TAG] 从仓库拉取镜像,TAG表示镜像的版本,省略TAG表示拉取latest对应的版本的镜像 来自DockerHub的MySQL的截图: docker push IMAGE-NAME [:TAG] 将本地镜像上传到仓库 docker search KEYWORD 根据关键词搜索镜像 docker images [OPTION] [REPOSITORY [:TAG] ] 根据选项、仓库名称、版本,列出本地的镜像 OPTION : -a : 列出全部镜像 -f :显示满足条件的镜像,How to use docker images filter -q : 只显示镜像ID REPOSITORY:TAG : 指定仓库名称和版本时列出本地镜像 docker rmi [OPTION] IMAGE1-NAME[:TAG] IMAGE2-NAME[:TAG] … OPTION : -f : 强制删除镜像(即时存在容器,也强制删除) 可以一次删除多个镜像 docker build [OPTION] PATH 利用Dockerfile自定义构建镜像 OPTION : -f : 指定使用的Dockerfile路径(可以用”.”表示当前路径) -t : 指定镜像名称及版本,格式 IMAGE-NAME[:TAG] Dockerfile文件的名称须是Dockerfile 容器操作 docker run [OPTION] IMAGE-NAME[:TAG] [COMMAND] 以某个镜像创建一个容器并运行容器(可指定初始命令) OPTION : -d 后台运行容器,返回为容器ID -it 以交互模式运行容器(-i:交互模式,-t:伪终端,二者常一起使用) -p 容器内端口映射到宿主机端口,格式为 宿主机端口:容器端口 -P 随机端口映射,容器内端口随机映射到宿主机端口 —name 指定容器名称 -e 指定环境变量(也可以在Dockerfile时通过ENV指定环境变量) —env-file 当环境变量有很多时,可以从指定文件读入环境变量,文件可以任意命名 —link 与其他容器建立单向通信, —link后跟的是容器名称,该选项实际是在本容器的/etc/hosts建立其他容器的解析 -v 挂在文件到容器内容,格式 宿主机路径: 容器内部路径 —dns 指定容器的dns服务器(也可以通过/etc/docker/daemon.json配置dns) —net 指定容器的网络类型,支持 bridge/host/none/container: 四种类型; —expose 开放一个或一组端口(也可以在Dockerfile动过EXPOSE指定开放的端口) —entrypoint 容器启动时的入口命令,该命令会覆盖Dockerfile中的ENTRYPOINT命令。 -w或者—workdir 指定工作目录(Dockerfile的最后一次WORKDIR会成为容器启动时的工作目录) COMMAND :容器启动时的运行命令,如/bin/bash(该指令可以在Dockerfile中由CMD指定,run中指定的COMMAND会覆盖Dockerfile中的CMD) docker start/stop/restart IMAGE-NAME 启动、停止或重启容器 docker exec [OPTION] CONTAINER-NAME 进入容器内部 OPTION : -it 进入容器的交互模式,该选项一般是必须选项 docker crate [OPTION] IMAGE-NAME[:TAG] [COMMAND] 创建容器但不运行容器(OPTION同docker run,有时使用create创建一个共享容器) docker pause/unpause CONTAINER-NAME 暂停/恢复容器(注意pause是暂停容器, stop是停止容器) docker rm [OPTION] CONTAINER-NAME 删除容器 OPTION : -f :强制删除容器(即便容器在运行状态) -v :删除容器挂载的卷(挂载一般是为了使容器与外部连通,除非挂载卷无用才会删除挂载卷) docker ps [OPTION] 列出本地的容器 OPTION: -a 列出所有容器,包括未运行的容器(docker ps只列出运行状态的容器,加上-a是列出所有的容器而不管其是否运行) -l 列出最近创建的容器 -n 列出最近创建的n个容器 -s 显示容器大小(会多一列: SIZE) -q 只显示容器编号,安静模式 docker inspect [OPTION] NAME|ID 查看容器或者镜像的各种属性,如挂载、配置、IP等 OPTION : -s : 标准输出的参数中会显示大小信息(不加-s没有size信息) 可以使用容器或镜像的名称,也可以使用容器或镜像的编号 docker logs [OPTION] CONTAINER-NAME 获取容器内部日志 OPTION : -t 显示日志时间戳(不加-t,默认没有时间戳) —since 显示某个时间之后的日志 —tail 显示末尾N条日志 docker cp CONTAINER-NAME SRC:DST 从宿主机向容器拷贝数据 容器与镜像的备份与迁移 docker save [OPTION] IMAGE-NAME[:TAG] 将本地镜像保存为tar归档 OPTION : -o (必需命令)输出到文件(或者使用 > , “>”的含义是重定向至…) 可以一次写入多个镜像 docker load [OPTION] FILE 将镜像的tar归档文件加载为镜像 OPTION : -i (必需命令)指定输入的文件(或者使用< ,”<”的含义是从…重定向到此) -q 精简输出信息 可以载入多个镜像 docker export [OPTION] CONTAINER-NAME 将容器快照导出为tar文件 OPTION : -o (必需命令)内容输出指向的文件(或使用重定向”>”) CAUTION: 使用export导出的镜像只是容器的快照,会失去历史记录、WORKDIR、ENTRYPOINT等信息 docker import [OPTION] FILE 将容器快照的tar归档文件加载为镜像 注意:恢复的镜像只是容器的快照镜像,没有历史记录及数据信息(WORKDIR、ENTRYPOINT等) docker load和docker import存在区别,准确说是save和export的区别,save会保存该镜像的的所有历史记录,而export只是保存容器的快照 docker commit [OPTION] CONTAINER-NAME [REPOSITORY[:TAG]] 从现有容器制作镜像 OPTION : -a 制作镜像的作者镜像属性中会包含Author信息,可通过inspect命令查看) -m 制作镜像的说明注释(镜像属性中会包含Comment信息,可通过inspect命令查看) commit不同于export,会保存容器的历史变更,即读写层内容 此小节相关链接: docker import和docker load的区别是什么? docker:export/save/commit谁才是你心中那个她 Docker镜像与容器备份迁移(export、import与commit、save、load) docker网络管理:docker network COMMAND docker有四种网络类型,分别是bridge、host、none、container,可以简单理解为桥接模式(或是NAT模式)、主机模式、手动配置网络、共享容器网络,可参考的链接:Docker的网络概念与网络模式、Docker的四种网络模式Bridge模式 docker network ls [OPTION] 列出已有的网络类型 OPTION : -q 安静模式,只列出网络编号 docker network create [OPTION] NETWORK-NAME OPTION : -d, —drive 指定网络类型 —gateway 指定网关 —subnet 使用CIDR格式指定网络IP docker connect NETWORK-NAME CONTAINER-NAME 将容器连接到网络 也可以在容器启动时通过 —network 指定容器连接的网络 容器连接到网络,实际是为容器添加了一块新的网卡并连接到新的网络,可以在容器内部通过ifconfig查看(通过apt-get install net-tools安装ifconfig) docker disconnect [OPTION] NETWORK-NAME CONTAINER-NAME 将容器与网络断开连接 OPTION : -f 强制断开连接 docker inspect [OPTION] NETWORK-NAME 查看网络属性 OPTION : -v, —verbose 详细输出 docker network rm NETWORK-NAME 删除网络 docker network prune [OPTION] 删除无用网络 OPTION : -f : 强制删除,无需确认 借助容器生命周期记忆命令","categories":[{"name":"Docker","slug":"Docker","permalink":"http://blog.karakarua.com/categories/Docker/"}],"tags":[{"name":"Docker","slug":"Docker","permalink":"http://blog.karakarua.com/tags/Docker/"}]},{"title":"docker基础","slug":"docker基础","date":"2021-03-07T16:00:00.000Z","updated":"2021-03-10T11:51:03.198Z","comments":true,"path":"2021/03/07/cce4c2665410/","link":"","permalink":"http://blog.karakarua.com/2021/03/07/cce4c2665410/","excerpt":"","text":"前言 从物理机到容器化: 物理机时代:部署慢、成本高、资源浪费、难迁移、受制于硬件 虚拟机时代:系统级别的隔离,需要安装操作系统 容器化时代:应用层面的隔离,比虚拟机更灵活 (https://sm.ms/image/9oQpOKn17WV3afl) 容器化的应用场景: 标准化的迁移方式 统一的参数配置 自动化部署 应用集群监控 开发与运维沟通的桥梁 docker 安装 在centos上安装docker:Install Docker Engine on CentOS 在Ubuntu上安装docker:Install Docker Engine on Ubuntu docker镜像加速编辑 /etc/docker/daemon.json,加入以下内容(可以添加多个url,以逗号分隔): 123456{ "registry-mirrors":[ "https://docker.mirrors.ustc.edu.cn/", "https://hub-mirror.c.163.com/" ]} 其他加速地址可见链接:Docker 镜像加速 docker的基本概念(不专业的理解) 镜像、一种只读的文件系统,提供容器运行时所需的程序、库、资源、配置等文件。镜像不包含任何动态数据,其内容在构建之后也不会被改变。(不恰当的比喻,类似安装存在系统的镜像文件) 容器: 镜像运行时的实体,彼此之间相互隔离(不恰当的比喻,类似安装成功后的操作系统) 仓库:集中存储镜像的远程服务器。 docker的简单架构docker基于C/S架构,由server提供功能并接受 client 的请求,二者之间通过REST API通信(HTTP) 更复杂的架构细节可见链接:Docker1-架构原理及简单使用(终于把Docker讲明白了)","categories":[{"name":"Docker","slug":"Docker","permalink":"http://blog.karakarua.com/categories/Docker/"}],"tags":[{"name":"Docker","slug":"Docker","permalink":"http://blog.karakarua.com/tags/Docker/"}]},{"title":"快速排序算法","slug":"快速排序苏算法","date":"2021-03-01T20:38:00.000Z","updated":"2021-09-21T04:39:35.072Z","comments":true,"path":"2021/03/01/3971b354f93d/","link":"","permalink":"http://blog.karakarua.com/2021/03/01/3971b354f93d/","excerpt":"","text":"题目链接:排序数组 快速排序的思想在于 partion的目标是让比pivot小的元素分布在其左侧,比pivot大的元素分布在其右侧,在排列过程的最后找到一个合适的pivot位置输出。 partion方法1:遇到左边比pivot大的与右边比pivot小时,二者进行交换,最后low==high时就是pivot的位置,交换pivot与low==high的位置上的数即可。 123456789101112131415161718192021222324252627282930313233343536373839404142434445import java.util.Arrays;class Solution1 { public int partition(int[] nums, int low, int high) { // select one pivot to sort the array partly // and return the position of pivot int pos = high; int pivot = nums[casualPos]; while (low < high) { while (low < high && nums[low] <= pivot) low++; while (low < high && nums[high] >= pivot) high--; swap(nums, low, high); } swap(nums, pos, high); return high; } public void quicksort(int[] nums, int start, int end) { if (start < end) { int pivot = partition(nums, start, end); System.out.println("pivot=" + pivot); Arrays.stream(nums).forEach(System.out::print); System.out.println(); quicksort(nums, start, pivot - 1); quicksort(nums, pivot + 1, end); } } public void swap(int[] nums, int i, int j) { int tmp = nums[i]; nums[i] = nums[j]; nums[j] = tmp; } public static void main(String[] args) { int[] nums = new int[] {5,7,1,6,4,8,3,2 }; Solution1 slt = new Solution1(); slt.quicksort(nums, 0, nums.length - 1); for (int i : nums) { System.out.println(i); } }} partion中有需要注意的地方,如果pivot一开始选择左侧,则要先high后low;如果pivot一开始选择右侧,则要先low后high,这样才能保证low==high时,交换pivot和pos时不会错。 原因:在low==high的上一步,pivot的位置与low、high三者的位置可能有两种关系: pivot的位置在nums[low]和nums[high]的左侧,此时交换nums[low]和pivot就能完成partion过程,只有让high向low移动,然后才可以在low==high时交换pivot和nums[low]; pivot的位置在nums[low]和nums[high]的右侧,此时交换nums[high]和pivot就能完成partion过程,只有让low向high移动,然后才可以在low==high时交换pivot和nums[high]。 Note:如果一开始选择中间元素作为pivot并非不可,只是在low==high的上一步,pivot的位置与low、high三者的位置关系就较难确定,不易编码。选择始选择最左或最右元素作pivot更利于思考。 partion方法2:左侧遇到比pivot大的,与pivot交换,右侧遇到比pivot小的,与pivot交换。每次交换后pivot的索引就会变化,下次使用pivot时要先更新索引。待low==high时,pivot已经被排列到合适的位置,就是low==high的位置。 12345678910111213141516171819202122232425262728293031323334353637383940414243import java.util.Arrays;class Solution1 { public int partition2(int[] nums, int low, int high) { // select one pivot to sort the array partly // and return the position of pivot int pivot = nums[high]; while (low < high) { while (low < high && nums[low] <= pivot) low++; pivot = nums[high]; nums[high] = nums[low]; nums[low] = pivot; while (low < high && nums[high] >= pivot) high--; pivot = nums[low]; nums[low] = nums[high]; nums[high] = pivot; } return high; } public void quicksort(int[] nums, int start, int end) { if (start < end) { int pivot = partition2(nums, start, end); System.out.println("pivot=" + pivot); Arrays.stream(nums).forEach(System.out::print); System.out.println(); quicksort(nums, start, pivot - 1); quicksort(nums, pivot + 1, end); } } public static void main(String[] args) { int[] nums = new int[] { 5, 7, 1, 6, 4, 8, 3, 2 }; Solution1 slt = new Solution1(); slt.quicksort(nums, 0, nums.length - 1); for (int i : nums) { System.out.println(i); } }} 上述代码可以简化,简化后的代码如下。左侧遇到比pivot大的数,该位置的数值赋值到high所在位置,省略对low位置赋值pivot操作(等到最后统一做,此时会会导致low位置的数失效);右侧遇到比pivot小的数,该位置的数值赋值到low所在位置,省略对high位置赋值pivot操作(等到最后统一做,此时会导致high位置的数失效)。失效的位置实际上是潜在的pivot的位置,即将来可能会被赋值pivot。最后一步是把pivot放到low==high的位置上。 12345678910111213141516171819202122232425262728293031323334353637383940import java.util.Arrays;class Solution1 { public int partition2(int[] nums, int low, int high) { // select one pivot to sort the array partly // and return the position of pivot int pivot = nums[high]; while (low < high) { while (low < high && nums[low] <= pivot) low++; nums[high] = nums[low]; while (low < high && nums[high] >= pivot) high--; nums[low] = nums[high]; } nums[high] = pivot; return high; } public void quicksort(int[] nums, int start, int end) { if (start < end) { int pivot = partition2(nums, start, end); System.out.println("pivot=" + pivot); Arrays.stream(nums).forEach(System.out::print); System.out.println(); quicksort(nums, start, pivot - 1); quicksort(nums, pivot + 1, end); } } public static void main(String[] args) { int[] nums = new int[] { 5, 7, 1, 6, 4, 8, 3, 2 }; Solution1 slt = new Solution1(); slt.quicksort(nums, 0, nums.length - 1); for (int i : nums) { System.out.println(i); } }} 简化后的方法其实是一种挖坑法,坑就是被转移走数值的位置上的数,由于坑实际上是未被赋值pivot的位置,所以这个坑可能将来被存放pivot,是pivot潜在的位置。 我感觉方法一易思考和编写,因此将方法一编写在了前面位置,由于没有找到合适的GIF动画讲解方法一,所以给个哔哩哔哩视频的链接,1分11秒开始讲解方法一的partion过程。链接:【理性的设计#01】用10秒,度过30秒时间 - oooooohmygosh 严蔚敏《数据结构》采用的是简化后的方法二,可能是大部分人最先接触的partion方法。","categories":[{"name":"Code","slug":"Code","permalink":"http://blog.karakarua.com/categories/Code/"}],"tags":[{"name":"Code","slug":"Code","permalink":"http://blog.karakarua.com/tags/Code/"}],"author":"Ferrilata"},{"title":"从暴力破解到动态规划解答题目连续子数组的最大和","slug":"从暴力破解到动态规划解答题目连续子数组的最大和","date":"2021-01-12T16:00:00.000Z","updated":"2021-03-10T12:34:44.722Z","comments":true,"path":"2021/01/12/166d8b96ee8b/","link":"","permalink":"http://blog.karakarua.com/2021/01/12/166d8b96ee8b/","excerpt":"","text":"梳理一下自己从暴力破解到动态规划的整个过程,希望可以帮到大家。 解此题,最容易想到的思路就是暴力破解,但是时间复杂度至少会是$O(n^2)$,有两种写法: 1234567891011121314151617// 时间复杂度:O(n^3)class Solution { public int maxSubArray(int[] nums) { int max = Integer.MIN_VALUE; for(int i = 0;i < nums.length;i++){ for(int j = i;j < nums.length;j++){ // 计算sum(i,j) int sum = 0; for(int k = i;k<j;k++) sum+=nums[k]; if(sum > max) max = sum; } } return max; }} 12345678910111213141516// 时间复杂度:O(n^2)class Solution { public int maxSubArray(int[] nums) { int max = Integer.MIN_VALUE; for(int i = 0;i < nums.length;i++){ int sum = 0; for(int j = i;j < nums.length;j++){ //sum(i,j)=sum(i,j-1)+nums[j] sum += nums[j]; if(sum > max) max = sum; } } return max; }} 无论那种暴力破解,过程中需要计算的子数组一定如下所列,其中$sum(i,j)$代表计算从$nums[i]$到$nums[j]$的元素之和,我们要找到最大的 $sum(i,j)$。 sum(0,0) sum(0,1) sum(1,1) sum(0,2) sum(1,2) sum(2,2) sum(0,3) sum(1,3) sum(2,3) sum(3,3) ….. … … …. 假如我们要以$O(n)$的时间复杂度优化算法,就需要进一步压缩计算。 观察上边这个表格,如果我们每次能在最右侧得到该行的最大值,然后再求这么多最大值的最大值,岂不就能在$O(n)$内计算出结果? 行最大值 sum(0,0) dp[0] sum(0,1) sum(1,1) dp[1] sum(0,2) sum(1,2) sum(2,2) dp[2] sum(0,3) sum(1,3) sum(2,3) sum(3,3) dp[3] ….. … … …. dp[j] 表格每一行的子数组都是以某一值结尾,所以我们设$dp[j]$为以$j$ 结尾的子数组的最大值,如上面表格所示。$dp[j]$的最大值就是我们要的结果。 如何计算$dp[j]$呢? 以$sum(0,3) 、 sum(1,3) 、 sum(2,3) 、 sum(3,3)$为例,我们思考一下怎么求四者最大值。 可以看到,四者同时包含$nums[3]$,比较四者哪个更大,其实就是比较$0、nums[2]、nums[1]+nums[2]、nums[0]+nums[1]+nums[2]$四者谁大谁小。 有没有发现规律?$nums[2]、nums[1]+nums[2]、nums[0]+nums[1]+nums[2]$这三者的最大值恰好就是dp[2]。所以,如果dp[2]>0,dp[3]=dp[2]+nums[3],否则,dp[3] = 0 + nums[3]。用公式表示就是: dp[j]=\\begin{cases} dp[j-1]+nums[j], & dp[j-1]>0 \\\\ nums[j], & dp[j-1]\\le 0 \\end{cases} 最后一步,就是对上面所有的$dp[j]$求最大值。所以,动态规划的代码如下: 12345678910111213141516171819class Solution { public int maxSubArray(int[] nums) { int[] dp = new int[nums.length]; dp[0]=nums[0]; for(int j = 1;j<nums.length;j++){ if(dp[j-1]>0){ dp[j] = dp[j-1]+nums[j]; }else{ dp[j] = nums[j]; } } int max = Integer.MIN_VALUE; for(int i = 0;i<dp.length;i++){ if(dp[i]>max) max = dp[i]; } return max; }} 最基础的动态规划做法到这就结束了,关于动态规划的再优化,本文不再赘述。","categories":[{"name":"Code","slug":"Code","permalink":"http://blog.karakarua.com/categories/Code/"}],"tags":[{"name":"Code","slug":"Code","permalink":"http://blog.karakarua.com/tags/Code/"}]},{"title":"Python循环体内实现异步并发","slug":"Python循环体内实现异步并发","date":"2020-12-19T03:19:00.000Z","updated":"2021-09-21T03:22:48.599Z","comments":true,"path":"2020/12/19/1b985f791541/","link":"","permalink":"http://blog.karakarua.com/2020/12/19/1b985f791541/","excerpt":"","text":"问题起源:循环体内无法实现异步。Python使用异步模块Asyncio实现多线程并发,一般方式是:12345678async def func(): # code... async def main(): await(func())if __name__ =='__main__': asyncio.run(main())但实验过程中有个需求,是让循环体的每次循环都作为一个并发线程产生并发。 这种情况下,每次循环使用await调用异步函数,无法实现需求中的并发需求。 asyncio程序如下: 123456789101112131415import timeimport asyncioasync def hello(): await asyncio.sleep(1) print('Hello World:%s' % time.time())async def main(): start = time.time() for i in range(5): await(hello()) print("use time: %f s" % (time.time()-start))if __name__ =='__main__': asyncio.run(main()) 程序结果: 123456Hello World:1608368438.992576Hello World:1608368439.9939594Hello World:1608368440.9950461Hello World:1608368441.9971309Hello World:1608368443.00034use time: 5.008629 s 程序运行时间是5秒,意味着并未达到异步的效果。 原因:整个for循环体是一个协程,协程切换时会挂起整个main协程。 解决办法:使用asyncio.gather()asyncio.gather()需要输入一个任务列表,gather会划分任务,并分组执行,因此可以应对for循环体内的异步。 完善后的代码: 123456789101112131415import timeimport asyncioasync def hello(): await asyncio.sleep(1) print('Hello World:%s' % time.time())async def main(): tasks=[] for i in range(5): tasks.append(hello()) await asyncio.gather(*tasks)if __name__ =='__main__': asyncio.run(main()) 程序运行结果: 123456Hello World:1608368546.8756351Hello World:1608368546.8756351Hello World:1608368546.8756351Hello World:1608368546.8756351Hello World:1608368546.8756351use time: 1.002837 s 程序运行时间是1秒,说明已经达到异步效果。 参考:异步编程 101:asyncio中的 for 循环 python中的asyncio使用详解 Python中的asyncio代码详解","categories":[{"name":"并发","slug":"并发","permalink":"http://blog.karakarua.com/categories/%E5%B9%B6%E5%8F%91/"}],"tags":[{"name":"Python","slug":"Python","permalink":"http://blog.karakarua.com/tags/Python/"},{"name":"Asycnio","slug":"Asycnio","permalink":"http://blog.karakarua.com/tags/Asycnio/"},{"name":"并发","slug":"并发","permalink":"http://blog.karakarua.com/tags/%E5%B9%B6%E5%8F%91/"}],"author":"Ferrilata"},{"title":"多线程基础——CAS","slug":"CAS基础","date":"2020-11-23T16:00:00.000Z","updated":"2021-03-16T05:27:47.925Z","comments":true,"path":"2020/11/23/9ebffbb32803/","link":"","permalink":"http://blog.karakarua.com/2020/11/23/9ebffbb32803/","excerpt":"","text":"CAS概念:CAS全称Compare And Swap,中文是对比并交换,是乐观锁的一种,是说多线程之一在写入修改值前先判定内存位置V中的值是否是预期值,是则修改V中的值,否则获取V中的值重复CAS。 CAS的过程是:(1) 线程读取内存位置V中当前值E到线程内部,然后修改E,写回内存前再次获取位置V的值,假如是N,判断N\\==E?,如果N\\==E,说明没有其他线程修改位置V的值,执行写回操作。CAS流程图如下(来自马士兵老师公开课程) ABA问题ABA是:如果线程1第二次获取位置V处的值依旧是E,但是这个E应不是一开始的E了,它可能被线程2变换了两次,举个例子就是E-> X -> E,但是线程1并不知道,然后修改了内存V的值,这个过程就发生了ABA问题。ABA问题的解决方式可以利用版本号或者时间戳。相关类有 AtomicStampedReference. CAS原理CAS是使用JDK类UnSafe实现的,UnSafe类是使用C++代码实现的,往更底层来讲,是通过汇编代码实现的。 Unsafe类中对应的代码是: 12345678inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { int mp == os::is_MP(); __asm__ volatile (LOCK_IF_MP(%4)) "cmpxchgl %1,(%3)" : "=a" (exchange_value) : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp) : "cc", "memory"); return exchange_value;} 关键在于:(LOCK_IF_MP(%4)) “cmpxchgl %1,(%3)”。 这说明CAS的底层实现还是依靠汇编指令cmpxchg LOCK_IF_MP是说:如果多线程(MP=Multi Processor),则需要在汇编命令cmpxchg前加上lock 因此,关键指令就是lock cmpxchg lock指令可以保证线程执行CAS的过程不受干扰,即加上锁,避免线程写回内存过程位置V处的值发生改变。 lock的硬件原理是:lock指令在执行后面指令的时候锁定一个北桥信号,网上也说是锁内存总线或者cache,这部分去网上翻了翻,大致找到了以下几个资料: 计算机总线结构详解 lock指令 x86系统cache locking的原理 感谢马老师。","categories":[{"name":"Java","slug":"Java","permalink":"http://blog.karakarua.com/categories/Java/"}],"tags":[{"name":"Java","slug":"Java","permalink":"http://blog.karakarua.com/tags/Java/"}]},{"title":"Java 优先队列自排序时机","slug":"Java-优先队列自排序时机","date":"2020-10-25T13:41:48.000Z","updated":"2020-10-25T14:33:28.000Z","comments":true,"path":"2020/10/25/364db06a3c43/","link":"","permalink":"http://blog.karakarua.com/2020/10/25/364db06a3c43/","excerpt":"","text":"今天做算法题,使用到了优先队列数据结构——PriorityQueue,但忘记了它发生自动排序的时机,记录一下。 结论:优先队列只有在元素个数发生变化时才发生自排序,如果只是改变已有元素内容,并不能引发自排序。 实例证明: 优先队列元素类,自定义MC类,包含两个属性,编号No和时间aTime。 12345678910class MC { int No; int aTime; MC(int no, int atime) { No = no; aTime = atime; }} 使用优先队列,自定义优先级规则:先按aTime从小到大排序,再按No从小到大排序 1234567891011121314151617181920212223242526class Solution4 { public static void main(String[] args) { Queue<MC> queue = new PriorityQueue<>(new Comparator<MC>() { @Override public int compare(MC o1, MC o2) { System.out.println("--Sort--"); if (o1.aTime < o2.aTime) return -1; else if (o1.aTime == o2.aTime) { return o1.No - o2.No; } else return 1; } }); for (int j = 1; j <= 3; j++) { System.out.println("insert:" + j); queue.offer(new MC(j, 0)); queue.stream().forEach(e -> System.out.println("<" + e.No + "," + e.aTime + ">")); } System.out.println("change peek"); queue.peek().aTime = 5; queue.stream().forEach(e -> System.out.println("<" + e.No + "," + e.aTime + ">")); }} 结果: 123456789101112131415insert:1<1,0>insert:2--Sort--<1,0><2,0>insert:3--Sort--<1,0><2,0><3,0>change peek<1,5><2,0><3,0> 可以看到,当新元素插入时,会引发优先队列自排序。 但是只修改内部元素对象时,却不会引发优先队列自排序(没有将aTime最大的移动到队尾)。 优先队列添加元素源代码:1234567891011public boolean offer(E e) { if (e == null) throw new NullPointerException(); modCount++; int i = size; if (i >= queue.length) grow(i + 1); siftUp(i, e); //这里就是调整堆排序 size = i + 1; return true;}","categories":[{"name":"Code","slug":"Code","permalink":"http://blog.karakarua.com/categories/Code/"}],"tags":[{"name":"Code","slug":"Code","permalink":"http://blog.karakarua.com/tags/Code/"}]},{"title":"pip更换国内源","slug":"pip更换国内源","date":"2020-10-06T08:20:04.000Z","updated":"2021-03-13T12:15:47.711Z","comments":true,"path":"2020/10/06/2c643ed1b59a/","link":"","permalink":"http://blog.karakarua.com/2020/10/06/2c643ed1b59a/","excerpt":"","text":"感谢博客pip换源一行命令直接搞定 关键命令: 123pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple# 返回结果:# Writing to /home/xxx/.config/pip/pip.conf 其中pip根据你使用的pip版本,Python2对应pip,Python3对应pip3 临时使用源: 1pip install xxx -i https://pypi.tuna.tsinghua.edu.cn/simple 建议全局更换pip源,方便日后使用。 各大知名国内源: 阿里云 http://mirrors.aliyun.com/pypi/simple/ 清华大学 https://pypi.tuna.tsinghua.edu.cn/simple/中国科学技术大学 http://pypi.mirrors.ustc.edu.cn/simple/ 豆瓣(douban) http://pypi.douban.com/simple/","categories":[{"name":"Misc","slug":"Misc","permalink":"http://blog.karakarua.com/categories/Misc/"}],"tags":[{"name":"Python","slug":"Python","permalink":"http://blog.karakarua.com/tags/Python/"}]},{"title":"Deepin安装Mininet","slug":"deepin安装mininet","date":"2020-09-29T16:00:00.000Z","updated":"2021-03-16T05:12:30.636Z","comments":true,"path":"2020/09/29/b79682574a0c/","link":"","permalink":"http://blog.karakarua.com/2020/09/29/b79682574a0c/","excerpt":"","text":"Mininet本身不支持Deepin系统,仅仅支持Ubuntu、Debian、Fedora、RedHatEnterpriseServer、SUSE LINUX。这类系统的教程参考:Mininet使用源码安装 首先确认系统是否有git,有就跳过”安装git“这一步,否则执行下面所有操作。(deepin v20下好像没有git) 安装git:sudo apt install git 安装完git后,运行命令ssh-keygen,一路回车就好。 登录GitHub,添加/home/xxx/.ssh/id_rsa.pub的内容到github的ssh keys中,这一步可以参考博客:GitHub 添加 SSH keys。这一步是为了执行下面的git clone git://….时不会报错。 获取Mininet源码:git clone git://github.com/mininet/mininet 选择Mininet安装版本: 123cd minintgit tag # 所有版本git checkout -b 2.3.0d4 # 版本需要根据git tag结果自己选择 执行安装: 修改install.sh脚本,保证deepin环境可以安装! Mininet确定系统是否支持的逻辑就写在这个脚本中,修改代码如下(就是在DIST中添加了Deepin): 12345DISTS='Deepin|Ubuntu|Debian|Fedora|RedHatEnterpriseServer|SUSE LINUX'if ! echo $DIST | egrep "$DISTS" >/dev/null; then echo "Install.sh currently only supports $DISTS." exit 1fi 只修改这是不行的,我们需要确定DIST从哪来,往上翻,发现这样一片代码: 12345678910111213test -e /etc/debian_version && DIST="Debian"grep Ubuntu /etc/lsb-release &> /dev/null && DIST="Ubuntu"if [ "$DIST" = "Ubuntu" ] || [ "$DIST" = "Debian" ]; then # Truly non-interactive apt-get installation install='sudo DEBIAN_FRONTEND=noninteractive apt-get -y -q install' remove='sudo DEBIAN_FRONTEND=noninteractive apt-get -y -q remove' pkginst='sudo dpkg -i' update='sudo apt-get' # Prereqs for this script if ! which lsb_release &> /dev/null; then $install lsb-release fifi 把这里的代码复制一份,然后修改DIST为Deepin即可,贴一个我改的: 1234567891011121314test -e /etc/debian_version && DIST="Deepin"grep Deepin /etc/lsb-release &> /dev/null && DIST="Deepin"if [ "$DIST" = "Deepin" ] || [ "$DIST" = "Debian" ]; then # Truly non-interactive apt-get installation install='sudo DEBIAN_FRONTEND=noninteractive apt-get -y -q install' remove='sudo DEBIAN_FRONTEND=noninteractive apt-get -y -q remove' pkginst='sudo dpkg -i' update='sudo apt-get' # Prereqs for this script if ! which lsb_release &> /dev/null; then $install lsb-release fifi 执行安装 mininet/util/install.sh -a 最后的参数-a参数可以自己选择,也可以-nvf只安装mininet、OpenFlow、Open vSwitch。具体参数可以参考官网或者Mininet使用源码安装 确认安装成功 执行sudo mn,能够建立最简单的网络拓扑,执行pingall可以全网ping通 为什么Mininet可以在deepin环境下安装成功: 因为Ubuntu和Deepin都是基于Debian构建,没道理Mininet支持Ubuntu,而不支持Deepin,毕竟二者师出同门。","categories":[{"name":"Deepin","slug":"Deepin","permalink":"http://blog.karakarua.com/categories/Deepin/"}],"tags":[{"name":"Deepin","slug":"Deepin","permalink":"http://blog.karakarua.com/tags/Deepin/"},{"name":"Mininet","slug":"Mininet","permalink":"http://blog.karakarua.com/tags/Mininet/"}]},{"title":"大厂笔试题1——小强去买东西的概率","slug":"阿里笔试题1——和为奇数的概率","date":"2020-09-22T12:31:00.000Z","updated":"2020-11-11T14:45:02.000Z","comments":true,"path":"2020/09/22/118d0a70269e/","link":"","permalink":"http://blog.karakarua.com/2020/09/22/118d0a70269e/","excerpt":"","text":"题目记不清楚了,大致题意为: 小强和小丽两人划拳决定谁去买东西。小强从1~N中随机取一个数,小丽从1~M中随机取一个数,如果两个数之和为奇数,则小强去买东西;否则小丽去买东西。问小强去买东西的概率是多少?请用最简分数的形式返回该概率。 示例:N=2, M=3,则小强可以选择的数字范围是1、2, 小丽可以选择的数字范围是1、2、3,所有可能的数字组合有(1,1)、(1,2)、(1,3)、(2,1)、(2,2)、(2,3)。其中和为奇数的组合是(1,2)、(2,1)、(2,3);和为偶数的组合是(1,1)、(1,3)、(2,2)。所以概率是3/6,即1/2。返回结果1/2。 解答: 定义小强的号码牌为 $i$ ,则 $i=1,2…N$ ,定义小丽的号码牌为 $j$ ,则 $j=1,2…M$ 。首先,组合$(i,j)$的数量一共是$M*N$,我们记为$total$ 不妨从小强的角度考虑,对每个 $i$ 值,组合$(i,j)$的和分布在区域$[i+1, M+i]$,则这个范围内包含的奇数的个数是$\\lfloor \\frac{M+i+1}{2} \\rfloor - \\lfloor \\frac{i+1}{2} \\rfloor$ 。(原因:数字范围$[1,X]$内奇数的个数为$\\lfloor \\frac{X+1}{2} \\rfloor$。) 所以,和为奇数的组合个数是$\\sum^{N}_{i=1} \\lfloor \\frac{M+i+1}{2} \\rfloor - \\lfloor \\frac{i+1}{2} \\rfloor$ ,我们记作$odd$ 所求概率$p=\\frac{odd}{total}$ 题目要返回最简分数,所以要求$odd$和$total$的最大公约数,然后分子分母都除以最大公约数,就是结果。 代码如下: 123456789101112131415161718192021222324import java.util.Scanner;public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); while(sc.hasNextInt()){ int N = sc.nextInt(); int M = sc.nextInt(); int odd = 0; for(int i = 1;i<=N;i++){ // java的除会自动向下去整 odd += (M+i+1)/2 - (i+1)/2; } int total=M*N; int factor = gcd(odd, total); System.err.printf("%d/%d\\n", odd/factor,total/factor); } } // 求最大公约数 public static int gcd(int a, int b){ if(b==0) return a; else return gcd(b,a % b); }}","categories":[{"name":"Code","slug":"Code","permalink":"http://blog.karakarua.com/categories/Code/"}],"tags":[{"name":"Code","slug":"Code","permalink":"http://blog.karakarua.com/tags/Code/"}]},{"title":"Hexo渲染LaTeX公式","slug":"Hexo渲染LaTeX公式","date":"2020-09-19T09:19:15.000Z","updated":"2020-09-20T14:53:48.000Z","comments":true,"path":"2020/09/19/50a5bc49a87d/","link":"","permalink":"http://blog.karakarua.com/2020/09/19/50a5bc49a87d/","excerpt":"","text":"感谢博客:在Hexo中渲染MathJax数学公式、Hexo博客中使用Latex、在Hexo中渲染MathJax数学公式 Hexo渲染LaTeX公式关键Hexo渲染主题的两个重要因素:mathjax和kramed,前者是数学公式渲染引擎,后者是Hexo的markdown渲染引擎,hexo默认渲染引擎是marked,但是它不支持mathjax,因此需要替换引擎。 一、Hexo添加mathjax 如果hexo安装有hexo-math,需要先卸载它。卸载命令: 1npm uninstall hexo-math --save 安装mathjax,安装命令: 1npm install hexo-renderer-mathjax --save hexo主题开启mathjax: 进入主题目录,编辑_config.yml,开启mathjax: 1234# MathJax Supportmathjax: enable: true per_page: true hexo博客开启mathjax: 博客文章的开头加入mathjax:true,具体如下: 12345---title: Hexo渲染LaTeX公式关键date: 2020-09-30 22:27:01mathjax: true-- 二、hexo切换kramed引擎 卸载marked引擎 1npm uninstall hexo-renderer-marked --save 安装kramed引擎 1npm install hexo-renderer-kramed --save 修改引擎bug 修改文件/node_modules\\kramed\\lib\\rules\\inline.js中escape和em两行,具体修改如下: 12// escape: /^\\\\([\\\\`*{}\\[\\]()#$+\\-.!_>])/, escape: /^\\\\([`*\\[\\]()#$+\\-.!_>])/, 这一步是在原基础上取消了对\\,{,}的转义(escape)。 同时把第20行的em变量也要做相应的修改。 12// em: /^\\b_((?:__|[\\s\\S])+?)_\\b|^\\*((?:\\*\\*|[\\s\\S])+?)\\*(?!\\*)/, em: /^\\*((?:\\*\\*|[\\s\\S])+?)\\*(?!\\*)/, 重新启动hexo: 1hexo clean && hexo g -d 问题得到解决。","categories":[{"name":"hexo","slug":"hexo","permalink":"http://blog.karakarua.com/categories/hexo/"}],"tags":[{"name":"hexo","slug":"hexo","permalink":"http://blog.karakarua.com/tags/hexo/"}]},{"title":"Leetcode 11 盛最多水的容器","slug":"Leetcode-11-盛最多水的容器","date":"2020-09-05T04:00:22.000Z","updated":"2020-09-22T12:32:34.000Z","comments":true,"path":"2020/09/05/ad3e8ec3f0f6/","link":"","permalink":"http://blog.karakarua.com/2020/09/05/ad3e8ec3f0f6/","excerpt":"","text":"题目链接:11. 盛最多水的容器 解答对任意$(i,j)$组合,面积$area=min(height[i],height[j])*(j-i)$ 方法一:暴力搜索遍历所有$(i,j)$,代码: 12345678910111213class Solution { public int maxArea(int[] height) { int n = height.length; int res=0; for(int i =0;i<n;i++){ for(int j =0;j<i;j++){ int T=Math.min(height[i],height[j])*(i-j); res = Math.max(res,T); } } return res; }} 时间复杂度:$O(N^2)$,空间复杂度:$O(1)$ 方法二:双指针 定义左右指针$i$和$j$,分别代表容器的左右边界。 指定两个指针移动状态: 原则: 两个指针只会向内收缩,即$i$只能增大,$j$只能减小 指针的移动朝着area可能增大的方向移动 移动方法:取二者高度较小的向内收缩。 原因: 高度较大的向内收缩,$area$只会不变或者减小,因为$min(height[i],height[j])$要么不变,要么减小,并且$(j-i)$变小了。 高度较小的向内收缩,$area$可能会变大,因为$min(height[i],height[j])$可能会变大,尽管$(j-i)$变小,但综合$area$存在变大的可能性。(这里说可能变大,也存在变小的可能,就是高度较小的,它更小了) 相比于暴力搜索忽略的$(i,j)$的状态都是比$area$小的,不必重复计算 代码: 123456789101112class Solution { public int maxArea(int[] height) { int i=0,j=height.length-1,res=0; while(i<j){ int area=Math.min(height[i],height[j])*(j-i); res=Math.max(res,area); if(height[i]<height[j]) i++; else j--; } return res; }} 时间复杂度:$O(N)$,空间复杂度:$O(1)$","categories":[{"name":"Code","slug":"Code","permalink":"http://blog.karakarua.com/categories/Code/"}],"tags":[{"name":"Code","slug":"Code","permalink":"http://blog.karakarua.com/tags/Code/"}]},{"title":"Deepin零散记录","slug":"Deepin零散记录","date":"2020-09-04T16:09:48.000Z","updated":"2020-09-04T16:10:43.000Z","comments":true,"path":"2020/09/04/5c2cdfe509d5/","link":"","permalink":"http://blog.karakarua.com/2020/09/04/5c2cdfe509d5/","excerpt":"","text":"有新发现就更新。 本人是deepin v20。deepin的tty1默认是图形界面,而一般Xorg默认在tty7。原因解释见论坛:请问15.5是默认tty1是图形界面么? 官方回答: 目前默认是tty1, 原因是为了同plymouth(启动画面)使用的tty1,进行无缝自然过渡。也就是Xorg和plymouth直接使用同一个tty,避免创建新的tty造成进入X11那一瞬间的黑屏现象。 Deepin深度影院无法打开的问题来自论坛解答 深度影院无法打开,关闭深度影院的硬件加速即可。1gsettings set com.deepin.deepin-movie composited 'DisableComposited' IDEA最近总是莫名其妙卡死的原因:搜狗输入法最新版——“搜狗输入法个人版v2.3.2”需要背锅,详细见论坛贴:[搜狗输入法个人版]goland和IDEA直接卡死,无法正常使用 deepin星火商店有个AppImage转Deb的转换器,转换到*.deb,就能让软件安装到系统,不用再便携式运行了。另外论坛发现帖子:请问AppImage .snap .deb Flatpak 区别","categories":[{"name":"Deepin","slug":"Deepin","permalink":"http://blog.karakarua.com/categories/Deepin/"}],"tags":[{"name":"Deepin","slug":"Deepin","permalink":"http://blog.karakarua.com/tags/Deepin/"}]},{"title":"Deepin删除打开方式的多余项","slug":"deepin删除打开方式的多余项","date":"2020-08-29T16:00:00.000Z","updated":"2021-03-16T05:13:26.781Z","comments":true,"path":"2020/08/29/46fb6e40b2f2/","link":"","permalink":"http://blog.karakarua.com/2020/08/29/46fb6e40b2f2/","excerpt":"","text":"情况复现: deepin打开txt文件时,打开方式支持选择默认程序,如下图,可以选择系统提供好的应用,如文件编辑器,亦可以添加其他程序。 假设我们没有找对路径,错误选择了一个应用,导致如下界面(图中的sublime_text是错误的): 我们想删除第二个sublime_text,怎么办? 解决办法: 上面的操作会在开始菜单中多出一个sublime_text 这个sublime_text的位置一般是:~/.local/share/applications下,找到删除即可。 再次右键查看打开方式,发现已经删除。","categories":[{"name":"Deepin","slug":"Deepin","permalink":"http://blog.karakarua.com/categories/Deepin/"}],"tags":[{"name":"Deepin","slug":"Deepin","permalink":"http://blog.karakarua.com/tags/Deepin/"}]},{"title":"deepin编辑开始菜单项","slug":"deepin编辑菜单项","date":"2020-08-28T16:00:00.000Z","updated":"2021-03-16T05:12:57.158Z","comments":true,"path":"2020/08/28/1d1b99ce15ac/","link":"","permalink":"http://blog.karakarua.com/2020/08/28/1d1b99ce15ac/","excerpt":"","text":"需要的实例图: 情况一:删除开始菜单项 实例:删除菜单项Intellij IDEA和sublime_text 方法: 确定菜单位置 位置1:/usr/share/applications/com.jetbrains.intellij-idea-ultimate.desktop 位置2:~/.local/share/applications/sublime_text 执行删除,注意在/usr/share/applications下删除文件需要权限。 情况二:修改开始菜单项图标、指向的应用 实例:修改Intellij IDEA运行目录、图标 方法: 确定菜单位置:/usr/share/applications/com.jetbrains.intellij-idea-ultimate.desktop 编辑该desktop文件(需要管理员权限): 12345678910111213[Desktop Entry]Version=1.0Type=ApplicationName=IntelliJ IDEA Ultimate EditionIcon=intellij-idea-ultimateExec="/opt/apps/com.jetbrains.intellij-idea-ultimate/files/share/intellij-idea-ultimate/bin/idea.sh" %fComment=Capable and Ergonomic IDE for JVMCategories=Development;IDE;Terminal=falseStartupWMClass=jetbrains-ideaX-Deepin-CreatedBy=com.deepin.dde.daemon.LauncherX-Deepin-AppID=com.jetbrains.intellij-idea-ultimate 这里的Icon代表图标,Exec代表运行命令,按照自己意愿更改即可。","categories":[{"name":"Deepin","slug":"Deepin","permalink":"http://blog.karakarua.com/categories/Deepin/"}],"tags":[{"name":"Deepin","slug":"Deepin","permalink":"http://blog.karakarua.com/tags/Deepin/"}]},{"title":"搭建Hexo博客","slug":"服务器搭建Hexo","date":"2020-08-19T16:00:00.000Z","updated":"2021-03-14T09:03:54.838Z","comments":true,"path":"2020/08/19/5efb209b3054/","link":"","permalink":"http://blog.karakarua.com/2020/08/19/5efb209b3054/","excerpt":"","text":"首先看个服务器搭建Hexo的整体架构图,非常好,有利于理解接下来的步骤。来自博客:如何在服务器上搭建hexo博客 步骤大纲:一、 本地计算机配置Hexo程序二、服务端配置网站根目录、Git裸仓库、Git-hooks、Nginx。三、 本地计算机与服务端建立通道,实现hexo上传。四、本地计算机渲染博客并部署到服务器。 一、本地计算机配置Hexo程序Note: 本地计算机为Deepin系统,理论上适用Ubuntu、Debian 安装及配置git(使用git形式向服务器部署博客时需要安装git) 确保本地计算机中含有git,否则安装git,Ubuntu下安装git命令是 1sudo apt-get install -y git # -y的含义是确定安装git,如果没有-y,系统会提问你是否确认安装git,输入y即可 配置本机git属性 12git config --global user.name "your name" # 引号内填写用户名git config --global user.email "your email" # 引号内填写邮箱 hexo需要node.js的支持,所以需要安装nodejs 打开终端窗口,根据系统输入以下命令之一:其他nodejs版本请参考Node.js Binary Distributionsnpm会随nodejs一起安装成功。 1234567# Using Ubuntucurl -sL https://deb.nodesource.com/setup_lts.x | sudo -E bash -sudo apt-get install -y nodejs# Using Debian, as rootcurl -sL https://deb.nodesource.com/setup_lts.x | bash -apt-get install -y nodejs 验证是否安装成功: 1234# node -v返回 v12.18.3node -v# npm -v返回 6.14.6npm -v 初始化Hexo博客 安装Hexo选择目录存放本地hexo博客内容(假设在主目录/home/$(whoami)/),运行 1npm install -g hexo-cli 初始化hexo:1hexo init blog # blog可以是其他任何你想起的名字 安装hexo其他插件:1234cd blognpm install #这个操作功能是补全依赖环境npm install hexo-deployer-git --save # 自动部署到服务器需要的插件npm install hexo-server # 本地简单的服务器,可以测试hexo是否安装成功 测试hexo是否安装成功。执行如下命令:1hexo g && hexo server 然后访问localhost:4000,如果能看到hexo界面说明本地hexo安装成功。二、服务端配置网站根目录、Git裸仓库、Git-hooks、Git上传用户、Nginx。 配置网站根目录备用 指定一个目录,本文指定主目录/home/$(whoami)/,创建文件夹hexo 12cd /home/$(whoami) #/home/$(whoami)就是主目录~mkdir hexo 服务端配置Git裸仓库 指定一个目录创建git裸仓库,本文选择目录/var/repo/。执行: 1sudo git init --bare blog.git 配置git-hooks 切换到hooks文件夹1cd /var/repo/blog.git/hooks 创建文件post-receive,执行vim post-receive,进入vim在insert模式下输入以下命令,其中$(whoami)就是用户名。12# !/bin/shgit --work-tree=/home/$(whoami)/hexo --git-dir=/var/repo/blog.git checkout -f 然后:wq保存 (非必需)确保git用户具备运行post-receive的权利,否则执行: 12# 正确形式:-rwxr-xr-x 1 git git 72 Aug 20 14:09 post-receive*chmod 755 post-receive 配置Nginx 安装Nginx: 1sudo apt-get install nginx 配置Nginx.conf:Nginx.conf位置是/etc/nginx/nginx.conf(也可能是/user/local/nginx/conf,可以whereis nginx命令查看具体位置)。编辑该文件内容,修改user、server12345678910111213user root;....http { server { listen 80; # 监听端口 server_name "your url or ip"; # 域名或者ip地址 location / { root /home/xxx/hexo; # 服务端网站根目录,xxx是用户名,等于$(whoami) index index.html; } }} 然后保存。三、本地计算机与服务端建立通道,实现hexo上传。 服务端创建并配置git用户,专门用于博客上传。 创建git用户,并更改git仓库的所有者 12sudo adduser git # 创建用户sudo chown -R git:git /var/repo/blog.git # 指定blog.git的所有者为git 禁用 git 用户的 shell 登录权限 出于安全考虑,我们要让 git 用户不能通过 shell 登录。可以编辑 /etc/passwd来实现 将git:x:1001:1001:,,,:/home/git:/bin/bash改成git:x:1001:1001:,,,:/home/git:/usr/bin/git-shell 这样 git 用户可以通过 ssh 正常使用 git,但是无法登录 shell。可以在服务端试一下,su git无法切换到git用户,报错fatal: Interactive git shell is not enabled. 新建git用户的原因出在这里,要使用一个不可以ssh远程登录的用户,而服务器主用户不能完成这样的需求。 本地计算机与服务端建立ssh通道。 本地计算机在/home/$(whoami)/.ssh执行如下命令(若/home/$(whoami)下没有.ssh目录,则创建.ssh目录)。1ssh-keygen 一直回车即可。这一步操作会在/home/$(whoami)/.ssh/下生成两个文件id_rsa和id_rsa.pub。 服务端在/home/git目录下创建文件夹.ssh,并在/home/git/.ssh下创建文件authorized_keys。把id_rsa.pub的内容复制到服务端的authorized_keys中。注意是把本地计算机的/home/$(whoami)/.ssh/id_rsa.pub复制到服务端的/home/git/.ssh/authorized_keys。 本地计算机配置hexo deploy。 编辑/home/xxx/blog/_config.yml,配置deploy: 1234deploy: type: git repo: git@"your domain or ip":/var/repo/blog.git branch: master 保存。 假如服务器端ssh服务的端口非默认端口22号,则本地计算机_config.yml按如下配置 1234deploy: type: git repo: ssh://git@"your domain or ip":"ssh port"/var/repo/blog.git branch: master hexo之处多向部署,即部署到多个服务器,比如同时部署到服务器和github pages 1234567deploy:- type: git repo: git@"your domain or ip":/var/repo/blog.git branch: master- type: git repo: git@github.com:"name"/"name".github.io.git branch: master 四、本地计算机渲染博客并部署到服务器。 编写博客 方式一:创建博客使用的markdown文件,然后书写内容 1hexo new "xxxx" 创建成功后,该markdown文件在source/_posts目录下。继续编辑xxx.md即可。 方式二:直接把写好了的md文件丢进source/_posts目录下。 渲染博客: 1hexo gernerate 或者 1hexo g 部署博客到服务器 1hexo deploy 或者 1hexo d 第二步和第三步可以一起使用 1hexo g && hexo d 或者 1hexo g -d 或者 1hexo d -g 操作完成后,服务端目录/home/deepin/hexo下会有刚刚提交的博客,但/var/repo/blog.git/branches不会有博客文件,因为它是裸仓库。注意:hexo d可能会报错Error: EACCES: permission denied, unlink ...,说什么权限拒绝。如果使用sudo hexo d,就会报另外一种错误:git@github.com: Permission denied (publickey). fatal: 无法读取远程仓库。。解决办法:一劳永逸,直接chmod -R 777 blog/。参考博客:使用Hexo+Github搭建博客的各种问题 完工。整个流程请照文章开头的架构图理解一下,比较难理解的是第二章节和第三章节。 其他发现:hexo支持Git以外的其他部署方式,详见官网文档:一键部署。","categories":[{"name":"hexo","slug":"hexo","permalink":"http://blog.karakarua.com/categories/hexo/"}],"tags":[{"name":"hexo","slug":"hexo","permalink":"http://blog.karakarua.com/tags/hexo/"}]},{"title":"利用SQL函数批量插入测试数据","slug":"利用SQL函数批量插入测试数据","date":"2020-08-18T16:00:00.000Z","updated":"2021-03-16T05:18:15.076Z","comments":true,"path":"2020/08/18/c7e2073b252b/","link":"","permalink":"http://blog.karakarua.com/2020/08/18/c7e2073b252b/","excerpt":"","text":"学习过程中需要创建一张含有批量数据的测试表,可以使用高级语言插入,但感觉比较麻烦,搜了搜可以采用SQL函数,直接在命令行页面批量插入。 数据库名为studytest,表名为student:12345678910CREATE DATABASE studytest;use studytest;CREATE TABLE `student` ( `s_id` int(11) NOT NULL AUTO_INCREMENT, `s_name` varchar(100) DEFAULT NULL, `s_age` int(11) DEFAULT NULL, `s_phone` varchar(30) DEFAULT NULL, PRIMARY KEY (`s_id`), KEY `s_name` (`s_name`)) ENGINE=InnoDB, CHARSET=utf8; SQL函数:1234567891011121314-- 定义分界符DELIMITER $$CREATE PROCEDURE insert_student(IN START INT(10),IN max_num INT(10))BEGIN DECLARE i INT DEFAULT START; -- 关闭自动提交 SET autocommit=0; REPEAT SET i=i+1; INSERT INTO student (s_id,s_name,s_age,s_phone) VALUES (i,CONCAT('name_',i),MOD(i,70),CONCAT('phone_',i)); UNTIL i=max_num END REPEAT; COMMIT;END $$ 调用该函数:123-- 先把分隔符换回来,方便操作。不换回来也行,就是以后的;都要写成 $$DELIMITER ; $$CALL insert_student(0,100000); 恢复自动提交:MySQL默认开启自动提交。除非显式地开始一个事务,否则每个查询都被当做一个单独的事务自动执行。 1SET autocommit=1; 查看autocommit状态:命令:show variables like ‘autocommit’;结果: PS:也可以直接利用SQL工具,如Navicat创建函数,完成批量插入。详见:mysql使用函数批量插入数据 完成后的student表: 参考博客:mysql使用存储过程&函数实现批量插入MySQL用存储过程与函数批量插入数据MySQL事务autocommit自动提交","categories":[{"name":"SQL","slug":"SQL","permalink":"http://blog.karakarua.com/categories/SQL/"}],"tags":[{"name":"SQL","slug":"SQL","permalink":"http://blog.karakarua.com/tags/SQL/"}]},{"title":"小记——根据用户动态加载菜单","slug":"小记——根据用户动态加载菜单","date":"2020-08-18T16:00:00.000Z","updated":"2021-03-16T05:19:43.278Z","comments":true,"path":"2020/08/18/d5007a69eb69/","link":"","permalink":"http://blog.karakarua.com/2020/08/18/d5007a69eb69/","excerpt":"","text":"引用博客:Vue + Spring Boot 项目实战(十五):动态加载后台菜单 不同用户登录后菜单显示不同的实现,需要同时结合前端和后端。后端主要实现:1. 数据库设计用户可以访问的菜单列表。2. 接收url请求,查询数据库返回允许的菜单列表。前端主要实现:1. Vuex Store添加菜单数组,保存允许访问的菜单项。2. 配置路由,包括Router入口、利用前置守卫添加菜单项路由。 3. 编写前端界面。 后端: 数据库设计用户可以访问的菜单列表:访问控制采用RBAC,数据库涉及的表包括用户表user、角色表role、菜单表menu、用户-角色映射表user_role、角色-菜单映射表role_menu。sql文件链接是:blog.sql这5个表的表属性及内容截图: 后端接收url请求,查询数据库返回允许的菜单列表。需要设计menuService以及menuController,当接收"api/menu"请求后,从数据库中查询当前用户可以访问菜单列表,并返回给前端。注意:本文使用的ORM框架为Mybatis-Plus,相关CRUD请访问官网:CRUD 接口、条件构造器 IMenuService:123public interface IMenuService extends IService<Menu> { public List<Menu> getMenuByCurrentUser();}menuServiceImpl:代码逻辑是:先根据当前用户查询对应的角色,再根据角色查询允许的菜单项。123456789101112131415161718192021222324252627282930313233343536373839404142@Transactional@Servicepublic class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements IMenuService { @Autowired IUserService userService; @Autowired IUserRoleService userRoleService; @Autowired IRoleMenuService roleMenuService; @Override public List<Menu> getMenuByCurrentUser() { // 获取当前用户 String username = SecurityUtils.getSubject().getPrincipal().toString(); User user = userService.getUser(username); System.out.println("CurrentUser:" + username); // 查询UserRole表, 找到用户对应的role id列表 LambdaQueryWrapper<UserRole> query = Wrappers.<UserRole>lambdaQuery().eq(UserRole::getUid, user.getId()); List<Integer> rids = userRoleService.list(query).stream().map(UserRole::getRid).collect(Collectors.toList()); // 找出这些角色对应的菜单项 LambdaQueryWrapper<RoleMenu> query2 = Wrappers.<RoleMenu>lambdaQuery().in(RoleMenu::getRid, rids); List<Integer> menuIds = roleMenuService.list(query2).stream().map(RoleMenu::getMid).collect(Collectors.toList()); List<Menu> menus = listByIds(menuIds).stream().distinct().collect(Collectors.toList()); //处理菜单项的结构 handleMenus(menus); return menus; } public void handleMenus(List<Menu> menus) { menus.forEach(menu -> {// LambdaQueryWrapper<Menu> query = Wrappers.<Menu>lambdaQuery().eq(Menu::getParentId, menu.getId());// List<Menu> children = list(query); List<Menu> children = menus.stream().filter(m -> m.getParentId() == menu.getId()).collect(Collectors.toList()); menu.setChildren(children); }); // 只是移除显示上的层次关系,但内部多级层次关系并没有删除 menus.removeIf(m -> m.getParentId() != 0); }}menuController:1234567891011@RestControllerpublic class MenuController { @Autowired IMenuService menuService; @GetMapping("/api/menu") public List<Menu> menu() { return menuService.getMenuByCurrentUser(); }} 前端: Vuex Store添加菜单数组,保存允许访问的菜单项。1234567891011121314151617181920212223 export default new Vuex.Store({ state: { user: { username: window.localStorage.getItem('user' || '[]') == null ? '' : JSON.parse(window.localStorage.getItem('user' || '[]')).username }, // 新增的用来保存可访问菜单项的数组 adminMenus: [] }, mutations: { login (state, user) { state.user = {username: user.username} window.localStorage.setItem('user', JSON.stringify(user)) }, logout (state) { state.user = [] window.localStorage.removeItem('user') }, // 新增的菜单数组驱动 initMenu (state, menus) { state.adminMenus = menus } }}) 配置路由,包括Router入口、利用前置守卫添加菜单项路由 首先配置router下的index.js,新增'/admin'路由,作为展示菜单界面的入口。router/index.js:12345678{ path: '/admin', name: 'Admin', component: AdminIndex, meta: { requireAuth: true } } 利用Vue-Router前置守卫,在真正发出url请求之前初始话菜单,包括1. 将后端返回的菜单项path添加到路由,2. 更新store的adminMenus。这部分代码是在main.js中书写。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475router.beforeEach((to, from, next) => { if (store.state.user.username && to.path.startsWith('/admin')) { // console.log('initMenu') initMenu(router, store) } // 已登录状态下访问login直接跳转到后台首页 if (store.state.user.username && to.path.startsWith('/login')) { next({ path: 'admin/dashboard' }) }// 登录部分 if (to.meta.requireAuth) { // console.log(store.state.user.username) if (store.state.user) { axios.get('/authentication') .then(resp => { if (resp.data) next() // resp.data为空代表后端拦截器判断是未认证、未RememberMe,但这时候依然有resp else { next({ path: 'login', // path后缀, 以path?xxx=yyy附加拼接, redirect代表拼接字符xxx, 可以自定义 // 该URL=login?redirect=%2Findex query: {redirect: to.fullPath} }) } }) } else { next({ path: 'login', query: {redirect: to.fullPath} }) } } else { next() }})//初始化菜单const initMenu = (router, store) => { if (store.state.adminMenus.length > 0) { return } axios.get('/menu') .then(resp => { if (resp && resp.status === 200) { // 把后端返回的菜单列表进行拼接 var fmtRoutes = formatRoutes(resp.data) // 并添加到Router router.addRoutes(fmtRoutes) store.commit('initMenu', fmtRoutes) } })}//拼接菜单项路由const formatRoutes = (routes) => { let fmtRoutes = [] routes.forEach(route => { if (route.children) { route.children = formatRoutes(route.children) } let fmtRoute = { path: route.path, component: resolve => { require(['./components/administration/' + route.component + '.vue'], resolve) }, name: route.name, nameZh: route.nameZh, iconCls: route.iconCls, children: route.children } fmtRoutes.push(fmtRoute) }) return fmtRoutes} 编写前端界面。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253<template> <div> <el-menu :default-active="currentPath" class="el-menu-admin" router mode="vertical" background-color="#545c64" text-color="#fff" active-text-color="#ffd04b" :collapse="isCollapse"> <div style="height: 80px;"></div> <!--index 没有用但是必需字段且为 string --> <el-submenu v-for="(item,i) in adminMenus" :key="i" :index="(i).toString()" style="text-align: left"> <span slot="title" style="font-size: 17px;"> <i :class="item.iconCls"></i> {{ item.nameZh }} </span> <el-menu-item v-for="child in item.children" :key="child.path" :index="child.path"> <i :class="child.icon"></i> {{ child.nameZh }} </el-menu-item> </el-submenu> </el-menu> </div></template><script>export default { name: 'AdminMenu', data () { return { isCollapse: false } }, computed: { adminMenus () { return this.$store.state.adminMenus }, currentPath () { return this.$route.path } }}</script><style scoped> .el-menu-admin { border-radius: 5px; height: 100%; }</style> 总结步骤:1. 后端设计数据库。2. 后端设计service和controller,接收请求返回当前用户允许的菜单列表。3. 前端设计Vuex.store,新添菜单数组。4. 前端设计Router,包括router/index.js入口路由、main.js中动态添加菜单项路由。 5. 编写Vue组件AdminMenu.vue。","categories":[{"name":"Java Web","slug":"Java-Web","permalink":"http://blog.karakarua.com/categories/Java-Web/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://blog.karakarua.com/tags/Spring-Boot/"},{"name":"Java Web","slug":"Java-Web","permalink":"http://blog.karakarua.com/tags/Java-Web/"},{"name":"Vue","slug":"Vue","permalink":"http://blog.karakarua.com/tags/Vue/"}]},{"title":"Mybatis-Mybatis-Plus忽略实体类某字段","slug":"Mybatis-Mybatis-Plus忽略实体类某字段","date":"2020-08-17T16:00:00.000Z","updated":"2021-03-16T05:28:37.721Z","comments":true,"path":"2020/08/17/8b6f786f28b5/","link":"","permalink":"http://blog.karakarua.com/2020/08/17/8b6f786f28b5/","excerpt":"","text":"情况: 1. 数据库表有字段id、cola、colb,映射到Java实体类会有属性id,colA,colB。现在我们需要再在实体类中新添加一个自定义属性colC。 2. 但是,colC并非数据库字段,如果不做处理,mybatis执行sql过程中会报错,`Cause: java.sql.SQLSyntaxErrorException: Unknown column 'children' in 'field list'` 解决:对colC属性使用注解@TableField(exist = false)参考博客:MyBatisPlus 如何忽略数据库和实体类之间的映射字段MyBatis实体非字段的属性注解(ps:该博客另外提到:hibernate实现该目的是使用注解@Transient )","categories":[{"name":"Java Web","slug":"Java-Web","permalink":"http://blog.karakarua.com/categories/Java-Web/"}],"tags":[{"name":"Java Web","slug":"Java-Web","permalink":"http://blog.karakarua.com/tags/Java-Web/"},{"name":"Mybatis-Plus","slug":"Mybatis-Plus","permalink":"http://blog.karakarua.com/tags/Mybatis-Plus/"}]},{"title":"小记——cookie,session,token,跨域","slug":"小记cookie,session,token,跨域","date":"2020-08-17T16:00:00.000Z","updated":"2021-03-16T05:18:56.249Z","comments":true,"path":"2020/08/17/0375e0510ae9/","link":"","permalink":"http://blog.karakarua.com/2020/08/17/0375e0510ae9/","excerpt":"","text":"转自:彻底理解cookie,session,token - 墨颜丶 - 博客园token保存在客户端,服务端通过验证数字签名的方式保证用户合法性。 Web项目中的跨域设置:参考博客:CORS 跨域 Cookie 的设置与获取基于Filter实现CORS、Spring注解@CrossOrigin。另外可以也使用拦截器实现CORS。 有关跨域Option请求: 跨域请求过程中,出现一个请求发送两次的现象,第一次是以OPTION方法发送的,第二次才是真正的请求。正式跨域的请求前,浏览器会根据需要,发起一个“PreFlight”(也就是Option请求),用来让服务端返回允许的方法(如get、post),被跨域访问的Origin(来源,或者域),还有是否需要Credentials(认证信息)参考 跨域中option请求详解 preflighted request 在发送真正的请求前, 会先发送一个方法为OPTIONS的预请求(preflight request), 用于试探服务端是否能接受真正的请求,如果options获得的回应是拒绝性质的,比如404\\403\\500等http状态,就会停止post、put等请求的发出。 参考HTTP跨域请求OPTION解析 axios 都是复杂请求,ajax 可以是简单请求。参考简单请求和复杂请求的区别","categories":[{"name":"Java Web","slug":"Java-Web","permalink":"http://blog.karakarua.com/categories/Java-Web/"}],"tags":[{"name":"Java Web","slug":"Java-Web","permalink":"http://blog.karakarua.com/tags/Java-Web/"}]},{"title":"Vue嵌套路由","slug":"Vue嵌套路由","date":"2020-08-11T16:00:00.000Z","updated":"2021-03-16T05:28:52.770Z","comments":true,"path":"2020/08/11/5475832df48c/","link":"","permalink":"http://blog.karakarua.com/2020/08/11/5475832df48c/","excerpt":"","text":"使用Vue嵌套路由的情况多半是要在某组件中嵌套其他组件渲染,比如要在主页添加其他内容。简单说说步骤,详情:Vue 嵌套路由,以及其他两篇参考:Vue-Router路由嵌套理解、Vue嵌套路由(vue-cli项目写法)组件嵌套举例:1234567+------------------+| Home || +--------------+ | | | index | || | | || +--------------+ |+------------------+ 父组件中要配置 <router-view />12345<template> <div> <router-view /> </div></template> 要配置路由嵌套1234567891011121314151617181920212223242526routes: [ { path: '/home', name: 'Home', component: Home, children: [ { /* 注意path的写法, 要注意, 以 / 开头的嵌套路径会被当作根路径。这让你充分的使用嵌套组件而无须设置嵌套的路径。 'index' -> 访问路径 = /home/index '/index' -> 访问路径 = /index '/home/index' -> 访问路径 = /home/index */ path: '/index', name: 'AppIndex', component: AppIndex, meta: { requireAuth: true } }, { path: '/other', name: 'Other', component: OtherIndex, } ] }, 注意<router-view />不能遗漏,以及父子路由的写法。","categories":[{"name":"Java Web","slug":"Java-Web","permalink":"http://blog.karakarua.com/categories/Java-Web/"}],"tags":[{"name":"Java Web","slug":"Java-Web","permalink":"http://blog.karakarua.com/tags/Java-Web/"},{"name":"Vue","slug":"Vue","permalink":"http://blog.karakarua.com/tags/Vue/"}]},{"title":"小记——Shiro","slug":"小记shiro","date":"2020-08-11T16:00:00.000Z","updated":"2021-03-16T05:19:20.197Z","comments":true,"path":"2020/08/11/8ea9ec3637ed/","link":"","permalink":"http://blog.karakarua.com/2020/08/11/8ea9ec3637ed/","excerpt":"","text":"shiro架构参考# Apache Shiro系列四,概述 —— Shiro的架构 shiro相关教程:Shiro教程安排","categories":[{"name":"Java Web","slug":"Java-Web","permalink":"http://blog.karakarua.com/categories/Java-Web/"}],"tags":[{"name":"Shiro","slug":"Shiro","permalink":"http://blog.karakarua.com/tags/Shiro/"}]},{"title":"小记——Vuex实现前端拦截","slug":"Vuex实现前端拦截小记","date":"2020-08-10T16:00:00.000Z","updated":"2021-03-16T05:16:44.017Z","comments":true,"path":"2020/08/10/fa5865d99101/","link":"","permalink":"http://blog.karakarua.com/2020/08/10/fa5865d99101/","excerpt":"","text":"之前讲过后端拦截的实现,后端拦截与前端拦截的区别:发生拦截的位置不同,一个在后端,一个在前端。后端拦截时请求肯定是已经到达了服务端,后端已经要做处理了。后端拦截的逻辑是对session判断,是否包含用户信息,没有就执行重定向请求,方法是response.sendRedirect(“/xxx”),这个过程一直是后端在操纵,最后返回给浏览器。前端拦截是前端框架分别利用状态管理和路由,判断登录状态和重定向请求,比如Vue.js利用Vuex保存一个浏览器用户状态,当用户状态是未登录时,使用vue-router重定向到"/login"。这个过程是发生在前端的,只有必须要请求后端时才向服务端发送请求("/login")。前端拦截的实现代码来自博客Vue + Spring Boot 项目实战(六):前端路由与登录拦截器 仅仅靠下面的前端拦截实现是不可行的,存在绕过风险,这里暂不讨论。详细请参考博客Vue + Spring Boot 项目实战(十四):用户认证方案与完善的访问拦截的第二部分:完善的访问拦截。 引入Vuex:123456789101112131415161718import Vue from 'vue'import Vuex from 'vuex'Vue.use(Vuex)export default new Vuex.Store({ state: { user: { username: window.localStorage.getItem('user' || '[]') == null ? '' : JSON.parse(window.localStorage.getItem('user' || '[]')).username } }, mutations: { login (state, user) { state.user = user window.localStorage.setItem('user', JSON.stringify(user)) } }})配置vue-router,修改router/index.js12345678910111213141516171819202122232425import Vue from 'vue'import Router from 'vue-router'import AppIndex from '@/components/home/AppIndex'import Login from '@/components/Login'Vue.use(Router)export default new Router({ mode: 'history', routes: [ { path: '/login', name: 'Login', component: Login }, { path: '/index', name: 'AppIndex', component: AppIndex, meta: { requireAuth: true } } ]})配置main.js123456789101112131415161718192021222324252627282930313233343536373839import Vue from 'vue'import App from './App'import router from './router'import store from './store'import ElementUI from 'element-ui'import 'element-ui/lib/theme-chalk/index.css'var axios = require('axios')axios.defaults.baseURL = 'http://localhost:8443/api'Vue.prototype.$axios = axiosVue.config.productionTip = falseVue.use(ElementUI)router.beforeEach((to, from, next) => { if (to.meta.requireAuth) { if (store.state.user.username) { next() } else { next({ path: 'login', query: {redirect: to.fullPath} }) } } else { next() } })/* eslint-disable no-new */new Vue({ el: '#app', render: h => h(App), router, store, components: { App }, template: '<App/>'})配置Login.vue(已删除UI部分,只保留了js脚本)123456789101112131415161718192021222324252627282930313233export default { name: 'Login', data () { return { loginForm: { username: 'admin', password: '123' }, responseResult: [] } }, methods: { login () { var _this = this console.log(this.$store.state) this.$axios .post('/login', { username: this.loginForm.username, password: this.loginForm.password }) .then(successResponse => { if (successResponse.data.code === 200) { // var data = this.loginForm _this.$store.commit('login', _this.loginForm) var path = this.$route.query.redirect this.$router.replace({path: path === '/' || path === undefined ? '/index' : path}) } }) .catch(failResponse => { }) } }}后端拦截可参考本人另外的博客:后端登录拦截/过滤这两篇博客均是学习Vue + Spring Boot 项目实战(六):前端路由与登录拦截器的解决心得。","categories":[{"name":"Java Web","slug":"Java-Web","permalink":"http://blog.karakarua.com/categories/Java-Web/"}],"tags":[{"name":"Java Web","slug":"Java-Web","permalink":"http://blog.karakarua.com/tags/Java-Web/"},{"name":"Vue","slug":"Vue","permalink":"http://blog.karakarua.com/tags/Vue/"}]},{"title":"后端登录拦截/过滤","slug":"后端登录拦截-过滤","date":"2020-07-28T16:00:00.000Z","updated":"2021-03-16T05:17:53.459Z","comments":true,"path":"2020/07/28/e4da9ef1c798/","link":"","permalink":"http://blog.karakarua.com/2020/07/28/e4da9ef1c798/","excerpt":"","text":"记录项目学习过程中的一个知识点——后端登录拦截。并思考用一种方式——Servlet Filter实现该功能。 注:拦截器@Override的prehandle逻辑,在后期学习过程修改为了shiro验证,本文代码不再修改,详情见Vue + Spring Boot 项目实战(十四):用户认证方案与完善的访问拦截 项目还是那个项目:Vue + Spring Boot 项目实战(一):项目简介还是第6节Vue + Spring Boot 项目实战(六):前端路由与登录拦截器。 文中有讲到一个需求——后端登录拦截,意思就是没有登录的用户,必须要求其登录之后才可以访localhost:8843/blog/index页面。文中实现的方式使用拦截器,拦截器是什么?拦截器是SpringMVC的一种机制,类似于Servlet的过滤器Filter,主要实现对某个处理方法进行拦截,执行某些操作。有个链接可参考: Spring MVC拦截器(Interceptor )详解 如何使用拦截器实现登录拦截? (原文:Vue + Spring Boot 项目实战(六):前端路由与登录拦截器的实现方式) 首先自定义一个类实现HandlerInterceptor,比如public class LoginInterceptor implements HandlerInterceptor,然后重写回调函数preHandle。这里的逻辑就是:判断request.getServletPath()是否是否是"/index",如果是,就判断是否登录(session.getAttribute("user") 是否为空),没有登录就重定向到"/login"(response.sendRedirect(request.getContextPath() + "/login"); 然后自定义类实现WebMvcConfigurer ,重写addInterceptors,把上面的LoginInterceptor注册到拦截器中。贴一个我自己写的代码,可以对比着原文代码看看(注意不要忘了注解):123456789101112131415public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); String servletPath = request.getServletPath(); if (servletPath.startsWith("/index")) { if (session.getAttribute("user") == null) { System.out.println("Not Login"); response.sendRedirect(request.getContextPath() + "/login"); return false; } } return true; }} 12345678910111213@SpringBootConfigurationpublic class MyWebConfigurer implements WebMvcConfigurer { @Bean public LoginInterceptor getLoginInterceptor() { return new LoginInterceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(getLoginInterceptor()).addPathPatterns("/**").excludePathPatterns("/index.html"); }} 下面是原博客中的LoginInterceptor的实现:123456789101112131415161718192021222324252627282930313233343536public class LoginInterceptor implements HandlerInterceptor{ @Override public boolean preHandle (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception { HttpSession session = httpServletRequest.getSession(); String contextPath=session.getServletContext().getContextPath(); String[] requireAuthPages = new String[]{ "index", }; String uri = httpServletRequest.getRequestURI(); uri = StringUtils.remove(uri, contextPath+"/"); String page = uri; if(begingWith(page, requireAuthPages)){ User user = (User) session.getAttribute("user"); if(user==null) { httpServletResponse.sendRedirect("login"); return false; } } return true; } private boolean begingWith(String page, String[] requiredAuthPages) { boolean result = false; for (String requiredAuthPage : requiredAuthPages) { if(StringUtils.startsWith(page, requiredAuthPage)) { result = true; break; } } return result; }} 既然一些博客在介绍SpringMVC的拦截器时说类似Servlet的Filter,那能否用Filter实现呢?我做了做实验,应该是可以的。但因为博主还是小白,因此如果写的不对,还请读者指教。逻辑和拦截器的代码差不多,就是对所有url先拦截,在判断ServletPath的前缀是不是"/index",是的话就判断用户是否登录,没有登录就重定向到"/login",已经登录就放行。直接上代码吧,同样注意不要忘了注解。1234567891011121314151617181920212223242526272829303132333435@Configuration@WebFilter(filterName = "LoginFilter", urlPatterns = {"/*"})@Order(value = 1)public class LoginFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; HttpSession session = request.getSession(); System.out.println("Servlet Path=" + request.getServletPath()); if (request.getServletPath().startsWith("/index")) { User user = (User) session.getAttribute("user"); if (user == null) { System.out.println("Not Login"); response.sendRedirect(request.getContextPath() + "/login"); } else { //放行 filterChain.doFilter(request, response); } } else { filterChain.doFilter(request, response); } } @Override public void destroy() { }} 请仔细理解这两种方式的代码。如果您感觉我写的不对,请批评指正。我能写出来也是“麦芒掉进针眼里——凑巧了”。如果您也在学习Vue + Spring Boot 项目实战(六):前端路由与登录拦截器,并且您实践了我说的这两种方式(前提我写对了,嘿嘿),请您继续往下看。我说一个小细节吧。使用过滤器Filter可以对"/index"和"/index.html"都进行过滤,要求登录以后才能访问,但是使用拦截器的方式并不能对"/index.html"进行拦截,即不要求登录就能访问(注:这个index.html是Vue.js单页面开发,直接访问是空白页面,由于不懂前端,我并不知道是否存在危害,或许可以直接访问并不存在危害,只是我感觉不太好)。因为MyWebConfigurer·中的·addInterceptor()方法有一句excludePathPatterns("/index.html")。可是为什么要加这一句呢?原博客也有说明原因,是因为未登录状态下,"/index"会重定向到"/login"界面,然后又因为"/login"使用了ErrorConfig(这个请看原博客,属于项目内容),将HTTP 404重定向到了"/index.html","/index.html"的前缀同样是"/index",未登录情况下再次重定向到"/login",然后这就造成了死循环,即重定向过多导致页面访问失败了。那为什么Filter不会重定向过多呢?我认为和拦截器、过滤器的机制不同有关,拦截器是属于SpringMVC规范,但过滤器是属于Servlet规范。博客(蛮不错的)《spring boot 过滤器、拦截器的区别与使用》给了几个图,并指出“过滤器和拦截器触发时机不一样,过滤器是在请求进入容器后,但请求进入servlet之前进行预处理的。请求结束返回也是,是在servlet处理完后,返回给前端之前。”这说明,过滤器Filter是在进入容器之后就起作用了,但是拦截器Interceptor是在进入了LoginController之前才被调用,并且这个过程应该是在ErrorConfig的后面(我感觉ErrorConfig的重定向应该是属于dispatcher)。这还有个利用System.out.println()输出Spring组件加载顺序的图,因此,如果MyWebConfigurer中的addIntercepto不写excludePathPatterns("/index.html"),就会造成这样的重定向死循环顺序:"/index"(未登录) --> "/login" --> "/index.html" --> "/login" --> "/index.html" --> ...,要把"/index.html"排除在外。截了个图可以看一下:当然,加了excludePathPatterns("/index.html"),也就如前面所说,意味着"/index.html"不需要登录了,可以直接访问。 好了,说完了。欢迎批评指正。","categories":[{"name":"Java Web","slug":"Java-Web","permalink":"http://blog.karakarua.com/categories/Java-Web/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://blog.karakarua.com/tags/Spring-Boot/"},{"name":"Java Web","slug":"Java-Web","permalink":"http://blog.karakarua.com/tags/Java-Web/"}]},{"title":"前端Vue整合后端部署页面一片空白","slug":"前端Vue整合后端部署页面一片空白","date":"2020-07-27T16:00:00.000Z","updated":"2021-03-16T05:18:37.628Z","comments":true,"path":"2020/07/27/8ca54e056d31/","link":"","permalink":"http://blog.karakarua.com/2020/07/27/8ca54e056d31/","excerpt":"","text":"首先说明不懂前端,仅仅是在学习前后端分离时遇到了问题,说说怎么解决的。 情况是这样的,学习了一个Vue+Springboot前后端分离博客:Vue + Spring Boot 项目实战(一):项目简介学习到第6节Vue + Spring Boot 项目实战(六):前端路由与登录拦截器,把前端和后端整合时,出现了问题,访问localhost:8443/blog/login(Note: 我设置了项目路径,即server.servlet.context-path:/blog/),界面一篇空白。但可以保证,检查敲的代码一点错误没有,操作步骤完全正确。这就尴尬了,几乎不懂前端,全要靠自己排查了。 排查步骤: 首先注意到原博客有一句话:“虽然页面是空白的,但确实是获取到了这个页面”。然后对比我的index.html的F12 Network调试页面,发现刺眼的错误:脚本和样式的请求路径是错误的,缺少了/blog/路径。 ) 问题出现:URL请求缺少项目路径。那怎么才能能配置路径,让URL请求再加上一个项目路径”/blog/“呢? 解决办法: 参考博客#为什么vue+webpack需要用到node,如何部署项目到服务器?、知乎vuejs怎么在服务器部署?(主要参考papersnake的回答)、官方配置文档配置参考、API 参考。(顺嘴说一句,在什么不懂的前提下,与其乱百度,倒不如直接看官网,这更像是正确的学习方式)。 首先配置index.html中的js、css等资源路径,这个相关的配置是前端项目config/index.js中,build下的assetsPublicPath。需要改成assetsPublicPath: './'或者assetsPublicPath: '/blog/'。下图是官方配置参考文档(我的vue是Version 2.x,官方提供的好像是Version 3.x的文档,虽然属性名字有差别,但感觉指的是一个东西,如有不对,请指正哈!)。 然后,配置前端项目router/index.js的Router,添加base:/blog/,这个的目的是告知router项目路径,即请求前缀。没有这一个,前端不能正确识别请求,即/blog/login会无法识别。配个官方配置API 参考截图:这个一定要配置,如果不配置,则访问/blog/login还是空白,后端并没有把404转到index.html资源。 注意:第一步中有个黄色的特别说明,当使用history路由方式时,尽量避免想对路径,即避免assetsPublicPath: './',即应该配置为:assetsPublicPath: '/blog/'。如果疏忽了,在history模式下使用./相对路径的方式,会有什么后果呢?(因为粗心大意,没有按官方要求设置,出现了错误,却因祸得福让我注意到了这一点。)给大家截个图:(前提:上面两个配置均操作完成。)可以发现history模式下使用想对publicPath的后果是,部分资源请求路径发送错误。 到这一步,/blog/index.html页面虽然还是空白,但是该拿到的资源就都拿到了。 配置完这两项后,再访问localhost:8443/blog/login,就能看到页面了呦! 总结:关键两步:1. 配置是前端项目config/index.js中,build下的assetsPublicPath;2. 配置前端项目router/index.js的Router,添加base:/blog/,blog是你的项目名称","categories":[{"name":"Java Web","slug":"Java-Web","permalink":"http://blog.karakarua.com/categories/Java-Web/"}],"tags":[{"name":"Vue","slug":"Vue","permalink":"http://blog.karakarua.com/tags/Vue/"}]},{"title":"Intellij-IDEA利用spring-initializr添加maven依赖包","slug":"Intellij-IDEA利用spring-initializr添加maven依赖包","date":"2020-07-25T16:00:00.000Z","updated":"2021-03-16T05:28:00.642Z","comments":true,"path":"2020/07/25/3bb75a65f0b6/","link":"","permalink":"http://blog.karakarua.com/2020/07/25/3bb75a65f0b6/","excerpt":"","text":"利用spring-initializr创建spring-boot项目时,可以直接选择依赖,创建成功后,这些依赖就会自动存在pom.xml中。但在项目编写过程中如果还想利用spring-initializer引入部分依赖,怎么办? 这就需要一个IDEA插件,叫EditStarters,可以直接利用spring-initiizer像创建项目时添加依赖包,很方便。 具体参考链接:IntelliJ IDEA中如何再次调出springboot的依赖窗口,随时可以根据喜好导入和移除插件 贴一个添加成功的依赖和插件主页面主页面:","categories":[{"name":"Misc","slug":"Misc","permalink":"http://blog.karakarua.com/categories/Misc/"}],"tags":[{"name":"Java","slug":"Java","permalink":"http://blog.karakarua.com/tags/Java/"},{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://blog.karakarua.com/tags/Spring-Boot/"}]},{"title":"shell编程:判断数组是否包含某个元素","slug":"shell编程:判断数组是否包含某个元素","date":"2020-07-24T16:00:00.000Z","updated":"2021-04-01T08:10:46.175Z","comments":true,"path":"2020/07/24/7c033a78b2a7/","link":"","permalink":"http://blog.karakarua.com/2020/07/24/7c033a78b2a7/","excerpt":"","text":"简单描述: 如数组$arr=(1\\ 2\\ 3\\ 4), target_1=4, target_2=5$。期望编写函数$IsContains()$,分别接受两个参数$arr$和$targe_i$,返回——0(True)或1(False)12345678910111213IsContains(){ # 把原来的arr新赋值给一个新数组 narr=($1) # 逻辑是判断删除\\$2后的数组是否与原数组相同,不同则标明包含元素,相同(没有\\$2可删除)则标明不包含元素 [[ ${narr[@]/$2/} != ${narr[@]} ]];echo $?}arr=(1 2 3 4)target1=4target2=5# ${arr[*]}!, not ${arr[@]}! TestResult: ${arr[@]}=> narr=(1), ${arr[*]=> narr=(1 2 3 4)# but i don't know whyecho `IsContains "${arr[*]}" $target1`echo `IsContains "${arr[*]}" $target2`代码中有个疑问,当调用的主参使用$arr[@]时,IsContains函数中$1=(1),当调用的主参使用$arr[*]时,IsContains函数中$1=(1 2 3 4)。不知道为什么,可能我对shell编程还是一知半解吧。望能看到这篇博客的大佬给予解答,谢谢。 另外关于shell判断逻辑——if [[]]、(())、[] 的区别,可以参考博客:[shell if [[ ]]和 ]区别 || &&该文章有参考博客:Bash数组-判断某个元素是否在数组内的几种方法 Shell数组与字符串:shell脚本编程进阶之数组、字符串切片","categories":[{"name":"Linux","slug":"Linux","permalink":"http://blog.karakarua.com/categories/Linux/"}],"tags":[{"name":"Linux","slug":"Linux","permalink":"http://blog.karakarua.com/tags/Linux/"},{"name":"Shell","slug":"Shell","permalink":"http://blog.karakarua.com/tags/Shell/"}]},{"title":"Tomcat零碎收集","slug":"关于Tomcat的零碎收集","date":"2020-07-24T16:00:00.000Z","updated":"2021-03-16T05:17:34.496Z","comments":true,"path":"2020/07/24/5c0475ca64d0/","link":"","permalink":"http://blog.karakarua.com/2020/07/24/5c0475ca64d0/","excerpt":"","text":"问题: 1. **Tomcat是什么?** 2. **Tomcat、Apache、Nginx都是啥关系?** 3. **Tomcat目录及说明** 4. **Tomcat基本框架** 5. **tomcat文件解释:server.xml、web.xml** 简单解答: 1. **Tomcat是什么?** Tomcat是一种轻量级应用服务器,是一个独立的Servlet、JSP的容器,能够处理动态资源,也提供一些web服务器的功能,能够处理静态资源请求。经常搭配Apache/Nginx做动态资源/静态资源分开处理,搭配Nginx做负载均衡。 2. **Tomcat、Apache、Nginx都是啥关系?** Apache是纯粹的web/http服务器,只能处理静态资源,Nginx也可以作为web/http服务器,但除此之外也提供负载均衡功能。参考1:WEB服务器、应用程序服务器、HTTP服务器区别。参考2:apache、node.js、nginx、tomcat谁能帮我捋一捋关系? 3. **Tomcat目录及说明** /bin - 启动,关闭和其他脚本。 /conf - 配置文件和相关的 DTD。这里最重要的文件是 server.xml。它是容器的主要配置文件。 /lib - 用来存放 tomcat 运行需要加载的 jar 包。 /logs - 日志文件目录。 /webapps - 存放 Web 应用程序的目录。 /work - 部署 Web 应用程序的临时工作目录。 /temp - 运行时产生的临时文件。 Tomcat基本框架 如上图,Tomcat可以按功能划分许多不同的组件,这些组件都可以通过/conf/server.xml文件中可定义和配置,包括Server, Service, Connector, Engine, Cluster, Host, Alias, Context, Realm, Valve, Manager, Listener, Resources, ResourceEnvRef, WatchedResource, Store, Transaction, Channel, Membership, Transport, Member, ClusterListener等,一般可分为以下四类:1、顶级组件:位于配置层次的顶级,并且彼此间有着严格的对应关系,有Server组件、Service组件; 2、连接器:连接客户端(可以是浏览器或Web服务器)请求至Servlet容器,只有Connector组件, 3、容器:表示其功能是处理传入请求的组件,并创建相应的响应。如Engine处理对一个Service的所有请求,Host处理对特定虚拟主机的所有请求,并且Context处理对特定web应用的所有请求; 4、被嵌套的组件:位于一个容器当中,但不能包含其它组件;一些组件可以嵌套在任何Container中,而另一些只能嵌套在Context中。 参考:Tomcat(一) Tomcat是什么:Tomcat与Java技术 Tomcat与Web应用 以及 Tomcat基本框架及相关配置 tomcat文件解释:server.xml、web.xml server.xml: Tomcat核心配置文件,包含Service, Connector, Engine, Realm, Valve, Hosts主组件的相关配置信息。如server.xml默认配置(删除注释内容)如下: web.xml:为部署与Tomcat实例上的所有web应用程序提供部署描述符,通常用于为webapp提供默认的servlet定义和基本的MUIME映射表。 tomcat的web.xml写入的都是web的默认配置,自定义配置需要在web项目的web.xml中设置。参考链接:tomcat下的web.xml和项目中的web.xml","categories":[{"name":"Java Web","slug":"Java-Web","permalink":"http://blog.karakarua.com/categories/Java-Web/"}],"tags":[{"name":"Java Web","slug":"Java-Web","permalink":"http://blog.karakarua.com/tags/Java-Web/"},{"name":"Tomcat","slug":"Tomcat","permalink":"http://blog.karakarua.com/tags/Tomcat/"}]},{"title":"(Windows-Linux)Spring Boot启动异常:Sprint-Boot-Tomcat--An-incompatible-version-[1-x-x]-o","slug":"(Windows-Linux)--Sprint-Boot-Tomcat--An-incompatible-version-[1-x-x]-o","date":"2020-07-20T16:00:00.000Z","updated":"2021-03-16T05:36:49.326Z","comments":true,"path":"2020/07/20/93b1076d0048/","link":"","permalink":"http://blog.karakarua.com/2020/07/20/93b1076d0048/","excerpt":"","text":"情景:启动spring-boot项目,报错 An incompatible version [1.2.12] of the Apache Tomcat Native library is installed, while Tomcat requires [1.2.14]。原因:spring-boot内置tomcat要求的native库版本和本地native库版本不一致,需要升级。解决办法: Windows: 方法一:下载安装对应版本native库,文件一般是tcnative-1.dll,把它安装到C:\\Windows\\System32目录下覆盖原来的tcnative-1.dll。一般百度这个错误搜到的教程都是针对Windows平台的,比较好处理。这个不再赘述,给个教程链接:An incompatible version [1.2.12] of the APR based Apache Tomcat Native library is installed, while Tomcat requires version [1.2.14] 方法二:源码编译安装该native库。首先下载所需要的native库源码文件。链接地址为:http://archive.apache.org/dist/tomcat/tomcat-connectors/native/1.2.14/source/。下载/tomcat-native-1.2.14-win32-src.zip。然后解压后按照官方给定的教程编译安装。官方教程地址:http://archive.apache.org/dist/tomcat/tomcat-connectors/native/1.2.14/source/ Linux: 暂时没有找到Linux下比较方便安装native库的方式,因此只能以源码方式编译安装该native库,不过步骤也不麻烦。 参考教程:Linux下Springboot解决APR based Apache Tomcat Native library提示 ;官方教程 首先下载对应版本native库源码,下载source/tomcat-native-1.2.14-src.tar.gz,解压。 执行如下命令: sudo apt-get autoremove libtcnative-1 #删除安装的库cd native./configure —with-apr=/usr/bin/apr-1-configsudo makesudo make install 重新运行spring-boot项目一般就不再报错误了。完毕。","categories":[{"name":"Java Web","slug":"Java-Web","permalink":"http://blog.karakarua.com/categories/Java-Web/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://blog.karakarua.com/tags/Spring-Boot/"}]},{"title":"LeetCode-146--LRU缓存机制","slug":"LeetCode-146--LRU缓存机制","date":"2020-07-11T16:00:00.000Z","updated":"2021-03-16T05:27:35.625Z","comments":true,"path":"2020/07/11/5991101609ed/","link":"","permalink":"http://blog.karakarua.com/2020/07/11/5991101609ed/","excerpt":"","text":"题目描述:运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。 获取数据 get(key) - 如果关键字 (key) 存在于缓存中,则获取关键字的值(总是正数),否则返回 -1。写入数据 put(key, value) - 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字/值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。请在 O(1) 时间复杂度内完成这两种操作。 来源:力扣(LeetCode)链接:https://leetcode-cn.com/problems/lru-cache著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。 解答:方法一:(来自LeetCode官方题解)使用自带的LinkedHashMap,可以直接模拟LRU。LinkedHashMap的数据结构是在HashMap的基础上,为每个HashMap节点都增加了两个属性before、after,并用双链表把每个节点连接起来,before、after分别代表某元素在双链表的前一个、后一个元素。详细可参考Java集合详解5:深入理解LinkedHashMap和LRU缓存 1234567891011121314151617181920212223242526272829303132333435//LinkedHashMap的双向链表实现LRU,最后节点为常访问的,头部的节点是最近最久未使用的import java.util.LinkedHashMap;class LRUCache extends LinkedHashMap { int capacity; LRUCache(int capacity) { // 需要使用LinkedHashMap中accessOrder=true的构造函数 // LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) // 顺便说个知识点,HashMap初始化并不会分配内存,分配是在第一次put元素后 super(capacity, 0.75F, true); this.capacity = capacity; } public int get(int key) { // 这个函数是getkey,如果取不到,则返回defaulValue,即第二个参数 return super.getOrDefault(key, -1); } public void put(int key, int value) { // 直接继承HashMap的put方法,如果accessOrder=true,则会把刚访问的节点或者新添加的节点安排到双链表末尾 super.put(key, value); } @Override protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) { // LinkedHashMap继承HashMap,二者的removeEldestEntry均return false,表示不删除最近最久未使用节点 // 需要重写,这里的逻辑是当元素个数大于初始容量时 // // HashMap的threshold修饰级别是default,除了HashMap的实例外,HashMap所在package的其他类也可以访问, // 但是除此之外其他都不能访问。因此,这里发生删除元素时,size>16,实际的capacity>24(16/0.75=24) return size() > capacity; }} 方法二:哈希表+双向链表方式构造LRU。HashMap用来定位某元素在节点中的位置,映射关系是,(HashMap).get(key)直接拿到节点。双向链表模拟LRU缓存,头部位置代表最近访问过的,尾部表示最久未使用的。原因:访问过的都往头部转移,那些不常访问的元素就会越来越往后移动。 这个哪一端是刚刚访问的,随你心情定义,你也可以定义尾部就是刚刚访问的,而头部是最久未使用的。get(int key)的逻辑:寻找某元素时,找不到就返回-1,找得到就返回value,并把该元素移动到头部,代表最近访问了,久而久之,尾部就会使最久未使用的元素。put(int key, int value)的逻辑是,如果不是新元素,就更新该元素value(同时意味着该元素要移动到开头,可以利用get(key)判断是否是新元素,若元素已存在就会移动到开头,然后更新开头元素value);如果是新元素,容量足够就加到头部,代表刚刚访问;容量不够,就得移除尾部节点,再在头部节点添加元素。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.Stack;class ListNode { int key; int value; ListNode before; ListNode after; ListNode(int key, int value) { this.key = key; this.value = value; }}class LRUCache { int capacity; // head和tail操作来自LinkedHashMap思想,好处是可以避免好多节点判空操作,比较方便。 // 牺牲两个节点空间换来操作方便,可以作为编程技巧。 ListNode head; ListNode tail; Map<Integer, ListNode> map; LRUCache(int capacity) { this.capacity = capacity; head = new ListNode(-1, -1); tail = new ListNode(-1, -1); head.after = tail; tail.before = head; map = new HashMap<>(); } // get的逻辑是:找不到返回-1,找得到就返回value,并把该元素移动到头部,代表最近访问了,久而久之,尾部就会使最久未使用的元素 public synchronized int get(int key) { if (capacity == 0) return -1; ListNode vn = map.get(key); // 找不到元素 if (vn == null) return -1; // 找到元素,无论它在哪,肯定有前后节点,最不济是head和tail,所以前后节点一定不为空 // 如果vn是头一个节点,不必操作,直接返回 if (vn.before == head) return vn.value; // 否则,在vn原来的地方移除vn removeNode(vn); // 移动到开头 addToHead(vn); return vn.value; } // put的逻辑是,如果不是新元素,就更新该元素value(同时意味着该元素要移动到开头,可以利用get判断是否是新元素,若元素已存在会移动到开头); // 如果是新元素,容量够就加到头部,代表刚刚访问;容量不够,就得移除尾部节点,再在头部节点添加元素 public synchronized void put(int key, int value) { if (capacity == 0) return; // 增添就意味着可能要移除元素 int v = get(key); // 并非新元素,更新 if (v != -1) { head.after.value = value; return; } ListNode newNode = new ListNode(key, value); /* 这个注释掉的内容是添加新节点的另一个方式 // 超容量就删除尾部节点 if (map.size() + 1 > capacity) { map.remove(tail.before.key); removeNode(tail.before); } // 添加节点到头部 map.put(key, newNode); addToHead(newNode); */ // 多一个元素不超容量,可以直接压入头部 if (map.size() + 1 <= capacity) { addToHead(newNode); map.put(key, newNode); return; } // 超容量,要去除最后一个最久未使用,并在最前面加入新元素 // tail.before=head是为了防止容量为0,当然可以直接在开头预防 // if (tail.before = head) // return; // 对应map要删除重添加 map.remove(tail.before.key); map.put(key, newNode); removeNode(tail.before); addToHead(newNode); } private synchronized void removeNode(ListNode node) { node.before.after = node.after; node.after.before = node.before; } private synchronized void addToHead(ListNode vn) { ListNode first = head.after; head.after = vn; vn.before = head; vn.after = first; first.before = vn; }}完毕。LRU缓存这道题常见于字节跳动的面试题,这里同时分享一个字节跳动面试常考算法题,来自LeetCode.LeetCode探索之字节跳动","categories":[{"name":"Code","slug":"Code","permalink":"http://blog.karakarua.com/categories/Code/"}],"tags":[{"name":"Code","slug":"Code","permalink":"http://blog.karakarua.com/tags/Code/"}]},{"title":"Linux-VSCode-Code-Runner编译不生成临时文件(Java)","slug":"Linux-VSCode-Code-Runner编译不生成临时文件(Java)","date":"2020-07-10T16:00:00.000Z","updated":"2021-03-16T05:28:16.927Z","comments":true,"path":"2020/07/10/09321f9ef153/","link":"","permalink":"http://blog.karakarua.com/2020/07/10/09321f9ef153/","excerpt":"","text":"VSCode Code Runner插件编译Java文件,默认会在源码所在文件生成*.class临时文件,而造成源码文件夹内容混乱,希望自定义位置保存类文件。 解决措施: Setting -> 搜索Code Runner -> 点击左侧”Run Code configuration” -> 右侧找到code-runner.executorMap,点击Edit in settings.json 修改code-runner.executorMap配置,如下:123"code-runner.executorMap": { "java":"cd $dir && javac $fileName && java $fileNameWithoutExt && mv $dir/*.class /tmp/vscodesws_*/jdt_ws/jdt.ls-java-project/bin"}, 参数解释:$dir是Java源码文件所在路径;$fileName是文件名,如test2.java;$fileNameWithoutExt是没有后缀的文件名,即test2(Java一般是主类和文件名相同);最后/tmp/..是自定义的class文件保存路径。 保存再运行,即可在指定路径生成*.class文件,不再一股脑保存在源码文件。 其他编程语言,可以bing搜索”coderunner 不生成临时文件”查看其他教程。或者参考https://github.com/formulahendry/vscode-code-runner/issues/338","categories":[{"name":"Misc","slug":"Misc","permalink":"http://blog.karakarua.com/categories/Misc/"}],"tags":[{"name":"Linux","slug":"Linux","permalink":"http://blog.karakarua.com/tags/Linux/"}]}],"categories":[{"name":"大数据","slug":"大数据","permalink":"http://blog.karakarua.com/categories/%E5%A4%A7%E6%95%B0%E6%8D%AE/"},{"name":"Java","slug":"Java","permalink":"http://blog.karakarua.com/categories/Java/"},{"name":"LaTeX","slug":"LaTeX","permalink":"http://blog.karakarua.com/categories/LaTeX/"},{"name":"Git","slug":"Git","permalink":"http://blog.karakarua.com/categories/Git/"},{"name":"Linux","slug":"Linux","permalink":"http://blog.karakarua.com/categories/Linux/"},{"name":"Nginx","slug":"Nginx","permalink":"http://blog.karakarua.com/categories/Nginx/"},{"name":"Docker","slug":"Docker","permalink":"http://blog.karakarua.com/categories/Docker/"},{"name":"HedgeDoc","slug":"HedgeDoc","permalink":"http://blog.karakarua.com/categories/HedgeDoc/"},{"name":"SQL","slug":"SQL","permalink":"http://blog.karakarua.com/categories/SQL/"},{"name":"Misc","slug":"Misc","permalink":"http://blog.karakarua.com/categories/Misc/"},{"name":"Code","slug":"Code","permalink":"http://blog.karakarua.com/categories/Code/"},{"name":"并发","slug":"并发","permalink":"http://blog.karakarua.com/categories/%E5%B9%B6%E5%8F%91/"},{"name":"Deepin","slug":"Deepin","permalink":"http://blog.karakarua.com/categories/Deepin/"},{"name":"hexo","slug":"hexo","permalink":"http://blog.karakarua.com/categories/hexo/"},{"name":"Java Web","slug":"Java-Web","permalink":"http://blog.karakarua.com/categories/Java-Web/"}],"tags":[{"name":"大数据","slug":"大数据","permalink":"http://blog.karakarua.com/tags/%E5%A4%A7%E6%95%B0%E6%8D%AE/"},{"name":"Flink","slug":"Flink","permalink":"http://blog.karakarua.com/tags/Flink/"},{"name":"Design Pattern","slug":"Design-Pattern","permalink":"http://blog.karakarua.com/tags/Design-Pattern/"},{"name":"Java","slug":"Java","permalink":"http://blog.karakarua.com/tags/Java/"},{"name":"LaTeX","slug":"LaTeX","permalink":"http://blog.karakarua.com/tags/LaTeX/"},{"name":"Git","slug":"Git","permalink":"http://blog.karakarua.com/tags/Git/"},{"name":"Gerrit","slug":"Gerrit","permalink":"http://blog.karakarua.com/tags/Gerrit/"},{"name":"BibTeX","slug":"BibTeX","permalink":"http://blog.karakarua.com/tags/BibTeX/"},{"name":"Linux","slug":"Linux","permalink":"http://blog.karakarua.com/tags/Linux/"},{"name":"CentOS","slug":"CentOS","permalink":"http://blog.karakarua.com/tags/CentOS/"},{"name":"Docker","slug":"Docker","permalink":"http://blog.karakarua.com/tags/Docker/"},{"name":"Nginx","slug":"Nginx","permalink":"http://blog.karakarua.com/tags/Nginx/"},{"name":"HedgeDoc","slug":"HedgeDoc","permalink":"http://blog.karakarua.com/tags/HedgeDoc/"},{"name":"CodiMD","slug":"CodiMD","permalink":"http://blog.karakarua.com/tags/CodiMD/"},{"name":"HackMD","slug":"HackMD","permalink":"http://blog.karakarua.com/tags/HackMD/"},{"name":"SQL","slug":"SQL","permalink":"http://blog.karakarua.com/tags/SQL/"},{"name":"Regex","slug":"Regex","permalink":"http://blog.karakarua.com/tags/Regex/"},{"name":"Code","slug":"Code","permalink":"http://blog.karakarua.com/tags/Code/"},{"name":"Python","slug":"Python","permalink":"http://blog.karakarua.com/tags/Python/"},{"name":"Asycnio","slug":"Asycnio","permalink":"http://blog.karakarua.com/tags/Asycnio/"},{"name":"并发","slug":"并发","permalink":"http://blog.karakarua.com/tags/%E5%B9%B6%E5%8F%91/"},{"name":"Deepin","slug":"Deepin","permalink":"http://blog.karakarua.com/tags/Deepin/"},{"name":"Mininet","slug":"Mininet","permalink":"http://blog.karakarua.com/tags/Mininet/"},{"name":"hexo","slug":"hexo","permalink":"http://blog.karakarua.com/tags/hexo/"},{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://blog.karakarua.com/tags/Spring-Boot/"},{"name":"Java Web","slug":"Java-Web","permalink":"http://blog.karakarua.com/tags/Java-Web/"},{"name":"Vue","slug":"Vue","permalink":"http://blog.karakarua.com/tags/Vue/"},{"name":"Mybatis-Plus","slug":"Mybatis-Plus","permalink":"http://blog.karakarua.com/tags/Mybatis-Plus/"},{"name":"Shiro","slug":"Shiro","permalink":"http://blog.karakarua.com/tags/Shiro/"},{"name":"Shell","slug":"Shell","permalink":"http://blog.karakarua.com/tags/Shell/"},{"name":"Tomcat","slug":"Tomcat","permalink":"http://blog.karakarua.com/tags/Tomcat/"}]}