Leetao's Blog

Talk is cheap, show me the code

0%

换博客

我竟然又想折腾博客了,我发现每过一段时间我都会想重新折腾一下博客,尽管我知道博客最重要的是内容,但是还是控制不住自己折腾的心。

现在的博客系统是自己基于 Flask 开发的,功能是基本满足日常使用的,原来是从 hexo 迁移过来的,可能料到自己会再次折腾,这个系统可以把自己所有的文章都导出成符合 hexo 格式的 markdown 文件,可能切回 hexo 是最方便的方案?

hexo ?

周六的上午起来就一直在浏览 hexo 的相关主题,比较中意的是 Next 主题,发现随着自己的年龄的增长,对简洁的东西更加中意了,但是出了简洁还希望能有点个性。

wp?

自己现在的博客是部署在服务器上的,如果用上了 hexo,就没有用服务器了,完全可以托管到 github 上,至于为放到服务器上,总觉得把静态博客放到服务器上的行为是对服务器的浪费。

想换 wp,不过不知道自己的小水管能不能撑住 wp,除此之外,网上有不少 hexo 迁移到 wp 的方案但是似乎都有点问题,不是特别想折腾。

end

思前想后,最后还是决定不换了,抽个时间再对自己的博客改造一番。服务器续费了 5 年,至少也让这个博客再运行五年。

读书

最近在看《秋园》,似乎是豆瓣 20 年排名比较靠前的一本书,才读了一点点,提到那个时期,女人还有裹脚布的习惯,“三寸金莲”,从书里对那个场景的描述,就能想象到被裹脚的痛楚,更是难以想象这种恶俗竟然持续了几千年。

《秋园》是自己开始看的第四本书,第三本书是《夜晚的潜水艇》,这篇读书笔记还没有写。。。

综艺

最近几年没事的时候偶尔会看一点综艺,这段时间再看 【奇葩说】,感觉奇葩说里的有的时候讨论的议题还是蛮有意思的,比如昨天再看的 20 期讨论 “父母该不该教孩子让着弟弟妹妹”,正反方都有各自的理由和观点,但是有一句我觉得蛮有道理的,“你是大的,应该让着小的”,说出这句话的时候多半是父母一种偷懒的表现。

BTW,辩论赛真是有意思,当初大学应该去参加的,真是可惜,哈哈

前言

最近使用 flutter 构建 App,涉及到网络请求部分,使用了 dart 自带的 HttpClient 库发现了一个有趣的问题,dart 默认情况下不使用代理,即使电脑开着代理。

解决方案

HttpClient 有个 findProxy 方法,复写这个方法就是设置代理,因此只需要在请求之前设置代理就可以了。

findProxy

1
2
3
4
HttpClient client = HttpClient();
client.findProxy = (uri) {
return "PROXY localhost:3128;";
};

但是显然在开发过程中我们并不清楚代理具体的 ip 和端口,所以为了提高可用性,需要一个方法帮助我们发现系统代理。

发现系统代理

flutter 有现成的三方 package-system_proxy 可以帮助获取代理。

安装 system-proxy

在 pubspec.yaml 中添加依赖

1
system_proxy: ^0.0.1

使用

1
2
3
4
Map<String, String> proxy = await SystemProxy.getProxySettings();
if (proxy != null) {
print('proxy $proxy');
}

优化代码

成功安装完需要的 package 之后,优化一下代码,使得在请求之前使用系统代理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
extension HttpClientExtension on HttpClient {
Future<HttpClient> autoProxy() async {
Map<String, String> sysProxy = await SystemProxy.getProxySettings();
var proxy = "DIRECT";
if (sysProxy != null) {
proxy = "PROXY ${sysProxy['host']}:${sysProxy['port']}; DIRECT";
print("find proxy $proxy");
}
this.findProxy = (uri) {
return proxy;
};
return this;
}
}

/// 使用方法
var httpClient = await new HttpClient().autoProxy();
/// 逻辑

参考链接

Network calls don’t show in Charles Proxy Debugger

Support proxy configuration in HttpClient

学会描述符之后,不仅有更多的工具集可用,还会对 Python 的运作
方式有更深入的理解,并由衷赞叹 Python 设计的优雅。——Raymond Hettinger
Python 核心开发者和专家

什么是描述符

描述符 (Descriptor) 是 Python 中一个非常重要的特性,在实际应用中我们经常使用到它,但是也最容易被忽略,property、classmethod、staticmethod。那么究竟什么叫描述符呢?看一下官方的定义:

In general, a descriptor is an attribute value that has one of the methods in the descriptor protocol. Those methods are __get__(), __set__(), and __delete__(). If any of those methods are defined for an attribute, it is said to be a descriptor.

一般而言,描述器是一个包含了描述器协议中的方法的属性值。 这些方法有 __get__(), __set__() __delete__()。 如果为某个属性定义了这些方法中的任意一个,它就可以被称为 descriptor

如何使用描述符

除了上面提到的三个内置属性,其实在不少 Python 库中都有关于描述符的应用,比如各种 ORM。

ORM 示例

这里以官方的 ORM 示例做个简单的演示 – 通过描述符来实现简单的 object relational mapping 框架。
其核心思路是将数据存储在外部数据库中,Python 实例仅持有数据库表中对应的的键。描述器负责对值进行查找或更新:

1
2
3
4
5
6
7
8
9
10
11
12
class Field:

def __set_name__(self, owner, name):
self.fetch = f'SELECT {name} FROM {owner.table} WHERE {owner.key}=?;'
self.store = f'UPDATE {owner.table} SET {name}=? WHERE {owner.key}=?;'

def __get__(self, obj, objtype=None):
return conn.execute(self.fetch, [obj.key]).fetchone()[0]

def __set__(self, obj, value):
conn.execute(self.store, [value, obj.key])
conn.commit()

可以用 Field 类来定义描述了数据库中每张表的模式的 models

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Movie:
table = 'Movies' # Table name
key = 'title' # Primary key
director = Field()
year = Field()

def __init__(self, key):
self.key = key

class Song:
table = 'Music'
key = 'title'
artist = Field()
year = Field()
genre = Field()

def __init__(self, key):
self.key = key

然后连接数据库验证一下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> import sqlite3
>>> conn = sqlite3.connect('entertainment.db')
>>> Movie('Star Wars').director
'George Lucas'
>>> jaws = Movie('Jaws')
>>> f'Released in {jaws.year} by {jaws.director}'
'Released in 1975 by Steven Spielberg'

>>> Song('Country Roads').artist
'John Denver'

>>> Movie('Star Wars').director = 'J.J. Abrams'
>>> Movie('Star Wars').director
'J.J. Abrams

自定义验证器

官方还提供了一个验证器的例子,同样值得一看。验证器是一个用于托管属性访问的描述器。在存储任何数据之前,它会验证新值是否满足各种类型和范围限制。如果不满足这些限制,它将引发异常,从源头上防止数据损坏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from abc import ABC, abstractmethod

class Validator(ABC):

def __set_name__(self, owner, name):
self.private_name = '_' + name

def __get__(self, obj, objtype=None):
return getattr(obj, self.private_name)

def __set__(self, obj, value):
self.validate(value)
setattr(obj, self.private_name, value)

@abstractmethod
def validate(self, value):
pass

自定义验证器需要从 Validator 继承,并且必须提供 validate() 方法以根据需要测试各种约束。

1
2
3
4
5
6
7
8
class OneOf(Validator):

def __init__(self, *options):
self.options = set(options)

def validate(self, value):
if value not in self.options:
raise ValueError(f'Expected {value!r} to be one of {self.options!r}')

验证结果

1
2
3
4
5
6
class Component:
kind = OneOf('wood', 'metal', 'plastic')
#...
def __init__(self, name, kind, quantity):
self.kind = kind
#...

描述器会阻止无效实例的创建

1
2
3
4
>>> Component('WIDGET', 'metle', 5)      # Blocked: 'metle' is misspelled
Traceback (most recent call last):
...
ValueError: Expected 'metle' to be one of {'metal', 'plastic', 'wood'}

参考

描述器使用指南

前言

通过 Docker 安装 Zabbix Web 界面默认的语言是英文,当切换成中文的时候,有些图标显示的标签就会乱码,不能正常显示,出现这个问题的原因是在于 zabbix 默认的字体对中文支持有问题,所以解决办法就是替换默认的字体。

解决问题

首先找到自己的喜欢的字体,这里我用的是 simkai.ttf。

将字体复制到容器

1
docker cp simkai.ttf zabbix-web-nginx-mysql:/usr/share/zabbix/assets/fonts/

注意:复制到 zabbix-web 的容器而不是 zabbix-server 容器

替换默认字体文件

进入到容器中然后替换默认的字体文件

1
2
3
4
docker exec -it  zabbix-web-nginx-mysql bash
cd /usr/share/zabbix/assets/fonts/
mv DejaVuSans.ttf DejaVuSans.ttf.bak # 不放心的话,可以先备份一下原来的字体文件
mv simkai.ttf DejaVuSans.ttf

然后刷新页面,乱码的问题就解决了

前言

前几天将 IntelliJ IDEA 升级到 2020.3 版本,然后在编译项目的时候发现原本正常运行的项目无法编译成功了,提示下面的错误:

java: You aren’t using a compiler supported by lombok, so lombok will not work and has been disabled. 

在网上检索寻找问题解决办法,发现在 github 上已经有人反馈了这个问题 [BUG] Lombok Does not work with IntelliJ EAP 2020.3 Build 203.4203.26 并给出了解决办法。

解决办法

总共发现两种解决办法,两种办法均有效。

方法一

设置编译器(compiler),打开设置(File -> Settings)

然后选择编译器(compiler),在 Shared build process VM options 填入: -Djps.track.ap.dependencies=false

**点击 apply,这个时候就可以重新编译成功了。

方法二

第二种方法就比较简单了,将你的 Lombok 升级就可以解决了,升级到 1.18.14

1
2
3
4
5
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.14</version>
</dependency>

结尾

本次出现这个问题,IDEA 方拒绝背锅,说是 Lombok 的问题。完整内容见:Build project with lombok - IntelliJ Ultimate 2020.3 EAP

前言

在实际过程开发中,有时候会有在启动 Web 服务的同时,希望启动一个后台任务去运行其他的任务。显然后台的任务,都是借由线程去完成的,所以在什么地方运行线程是需要我们解决的问题。

解决方案

请求时启动任务

这种方式显然时最简单的,配合 flask 自带的 api 我们可以在第一次请求时,甚至在每次请求启动线程。**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import threading
import time
from flask import Flask
app = Flask(__name__)

# app.before_request etc
@app.before_first_request
def activate_job():
def run_job():
while True:
print("执行后台任务...")
time.sleep(3)

thread = threading.Thread(target=run_job)
thread.start()

@app.route("/")
def hello():
return "Hello World!"


if __name__ == "__main__":
app.run()

运行一下代码看一下结果:

1
2
3
4
5
6
7
8
9
10
11
12
(venv) D:\workspace\python\test>python test_flask.py
* Serving Flask app "test_flask" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
执行后台任务...
127.0.0.1 - - [24/Nov/2020 16:02:22] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [24/Nov/2020 16:02:22] "GET /favicon.ico HTTP/1.1" 404 -
执行后台任务...
执行后台任务...

显然这个有一点局限性,在没有任何请求的时候线程不会执行。我们可以对代码进行优化,让线程这部分独立于 Flask 应用。

和 Flask 应用一起启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests
import threading
import time
from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
return "Hello World!"


def start_runner():
def start_loop():
while True:
print("执行后台任务...")
time.sleep(3)

thread = threading.Thread(target=start_loop)
thread.start()

if __name__ == "__main__":
start_runner()
app.run()

同样的,运行一下代码看一下结果:

1
2
3
4
5
6
7
8
9
10
(venv) D:\workspace\python\test>python test_flask.py
执行后台任务...
* Serving Flask app "test_flask" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
执行后台任务...
执行后台任务...

这个时候,后台任务和 Flask 应用相互独立,功能算是基本实现。但是后台任务和 Flask 应用完全分离,实现方式不够优雅,我们对代码进行重构一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import requests
import threading
import time
from flask import Flask
import os

class FlaskApp(Flask):
def __init__(self, *args, **kwargs):
super(FlaskApp, self).__init__(*args, **kwargs)
self._activate_background_job()

def _activate_background_job(self):
def run_job():
while True:
print('执行后台任务')
time.sleep(3)

t1 = threading.Thread(target=run_job)
t1.start()


app = FlaskApp(__name__)

@app.route("/")
def hello():
return "Hello World!"

if __name__ == "__main__":
app.run()

拓展思维

到这里程序算是基本上完成了,但是如果在运行过程中,停止应用的话,显然会让后台任务直接粗暴的退出,那么有没有一种优雅的方式,可以让应用程序在退出时,让后台任务收拾完成之后,优雅的退出呢?

前言

一直都有订阅 Rss 的习惯,前段时间配合钉钉的自定义机器人,建了一个在钉钉定时推送消息的群,当时是服务是跑在自己的小水管上的,并且是在自己博客的基础上的,依赖了 Flask。后续想着开源,考虑到单独一个 Rss 推送机器人依赖一个 Web 框架就显得比较笨重了,就着手将这部分服务独立出来。

钉钉 RSS 推送机器人

钉钉 RSS 推送机器人,主要内容分为两部分,第一部分是钉钉机器人;第二部分是订阅推送。

钉钉机器人

因为是自定义内容,钉钉现有的机器人不支持,所以我们需要借助钉钉的自定义机器人。

步骤一

【电脑钉钉 】-【群聊】-【群设置】-【智能群助手】-【添加更多】-【添加机器人】-【自定义】-【添加】,编辑机器人名称和选择添加的群组。完成必要的安全设置(至少选择一种),勾选 我已阅读并同意《自定义机器人服务及免责条款》,点击“完成”即可。

步骤二

复制出机器人的Webhook地址(格式如下截图),可用于向这个群发送消息。并点击【完成】,自定义机器人就添加成功了。

为了安全考虑,这里我们在创建机器人的时候,勾选“加签”。这样子会获得 webhook 和 secret 两个参数

1
2
webhook = 'https://oapi.dingtalk.com/robot/send?access_token=这里填写自己钉钉群自定义机器人的token'
secret = 'SEC11b9...这里填写自己的加密设置密钥'

消息推送

完成机器人的创建,就需要完成通过 api 将消息推送到群里,使用 DingtalkChatbot

安装

1
pip install DingtalkChatbot

例子

1
2
3
4
5
6
7
8
9
10
from dingtalkchatbot.chatbot import DingtalkChatbot
# WebHook地址
webhook = 'https://oapi.dingtalk.com/robot/send?access_token=这里填写自己钉钉群自定义机器人的token'
secret = 'SEC11b9...这里填写自己的加密设置密钥' # 可选:创建机器人勾选“加签”选项时使用
# 初始化机器人小丁
xiaoding = DingtalkChatbot(webhook) # 方式一:通常初始化方式
xiaoding = DingtalkChatbot(webhook, secret=secret) # 方式二:勾选“加签”选项时使用(v1.5以上新功能)
xiaoding = DingtalkChatbot(webhook, pc_slide=True) # 方式三:设置消息链接在PC端侧边栏打开(v1.5以上新功能)
# Text消息@所有人
xiaoding.send_text(msg='我就是小丁,小丁就是我!', is_at_all=True)

RSS 解析

rss 解析,这里使用 feedparser 进行解析。

安装

1
pip install DingtalkChatbot

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
>>> import feedparser
>>> leetao_feed = feedparser.parse('https://www.leetao94.cn/feeds') # parse() 方法,解析对应的 url
>>> leetao_feed.feed # feed 对象
{'title': "Peach Blog's Feeds", 'title_detail': {'type': 'text/plain', 'language': None,
'base': 'https://www.leetao94.cn/feeds', 'value': "Peach Blog's Feeds"},
'id': 'https://www.leetao94.cn/feeds', 'guidislink': True,
'link': 'https://www.leetao94.cn/', 'updated': '2020-09-25T19:53:35Z',
'updated_parsed': time.struct_time(tm_year=2020, tm_mon=9, tm_mday=25, tm_hour=19, tm_min=53, tm_sec=35, tm_wday=4, tm_yday=269, tm_isdst=0),
'links': [{'href': 'https://www.leetao94.cn/', 'rel': 'alternate', 'type': 'text/html'},
{'href': 'https://www.leetao94.cn/feeds', 'rel': 'self', 'type': 'application/atom+xml'}],
'generator_detail': {'name': 'Werkzeug'}, 'generator': 'Werkzeug'}
>>> type(leetao_feed.entries) # 解析出来的文章列表对象
<class 'list'>
>>> len(leetao_feed.entries)
10
>>> leetao_feed.entries[0] # 单个文章的内容
{'title': 'Mybatis中特殊符号#{}和${}',
'title_detail': {'type': 'text/plain',
'language': None, 'base': 'https://www.leetao94.cn/feeds',
'value': 'Mybatis中特殊符号#{}和${}'},
'id': 'https://www.leetao94.cn/post/Mybatis中特殊符号#{}和${}',
'guidislink': True, 'link': 'https://www.leetao94.cn/post/Mybatis中特殊符号#{}和${}',
'updated': '2020-09-25T19:53:35Z',
'updated_parsed': time.struct_time(tm_year=2020, tm_mon=9, tm_mday=25, tm_hour=19, tm_min=53, tm_sec=35, tm_wday=4, tm_yday=269, tm_isdst=0),
'published': '2020-09-25T19:53:35Z',
'published_parsed': time.struct_time(tm_year=2020, tm_mon=9, tm_mday=25, tm_hour=19, tm_min=53, tm_sec=35, tm_wday=4, tm_yday=269, tm_isdst=0),
'links': [{'href': 'https://www.leetao94.cn/post/Mybatis中特殊符号#{}和${}',
'rel': 'alternate', 'type': 'text/html'}], 'authors': [{'name': 'Leetao'}],
'author_detail': {'name': 'Leetao'}, 'author': 'Leetao',
'content': [{'type': 'text/html', 'language': None, 'base': 'https://www.leetao94.cn/feeds',
'value': '...'}

具体用法可以参考官方文档:https://pythonhosted.org/feedparser/

RSS 钉钉机器人

准备工作完成之后,就是将机器人和 Rss 解析内容整合到一起,为了避免重复推送内容,很显然需要借助数据库,考虑简便性,采用 python 原生支持的 sqlite 数据库免安装;为了操作数据库方便,这里就使用轻量级的 ORM – peewee

模型定义

首先完成数据模型的定义,两个模型,一个是 RSS 源模型,一个是 History 历史记录模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# models.py
# -*- encoding: utf-8 -*-
from peewee import *
import datetime

db = SqliteDatabase('rss.db')


class BaseModel(Model):
class Meta:
database = db


class Rss(BaseModel):
feed = CharField(unique=True) # rss 源地址
cover = CharField(max_length=255) # 封面
title = CharField(max_length=20) # rss 的标题
url = CharField(max_length=255) # 官网地址 可选


class History(BaseModel):
url = CharField(max_length=255)
publish_at = DateField(default=datetime.datetime.now)


def create_tables():
with db:
db.create_tables([Rss, History])

模型定义完成,接下来就是代码的整合了。

整合代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# -*- encoding: utf-8 -*-

"""
rss.py
钉钉 Rss 机器人

"""
from datetime import datetime
import feedparser
from dingtalkchatbot.chatbot import DingtalkChatbot, CardItem, ActionCard
import dateparser
from models import db, Rss, History
import os

class RssRobot:
def __init__(self):
self.robot = DingtalkChatbot(
os.environ.get("DD_WEBHOOK"),
pc_slide=True, secret=os.environ.get("DD_SECRET")) # 两个环境变量

def parse_rss(self):
rss_list = Rss.select()
rss_card_dict = {}
post_url_list = [rss_history.url for rss_history in
History.select().where(History.publish_at == datetime.today().strftime("%Y-%m-%d"))] # 查询当天的历史记录
for rss in rss_list:
rss_history_list = []
card_list = [
CardItem(title=rss.title, url=rss.url, pic_url=rss.cover)]
feed = feedparser.parse(rss.feed)
for entry in feed.entries:
if entry.link not in post_url_list and self.is_today(entry): # 判断链接是否是当天的并且没有推送过的
card_list.append(CardItem(title=f'{entry.title}', url=entry.link,
pic_url='https://ftp.bmp.ovh/imgs/2020/07/6cdb9f606677c9e3.jpg'))
rss_history_list.append(History(url=entry.link))

if len(card_list) > 1:
rss_card_dict[rss.title] = card_list
with db.atomic():
History.bulk_create(rss_history_list, batch_size=10)

return rss_card_dict

def is_today(self, entry):
return dateparser.parse(entry['updated']).date() == datetime.today().date()

def send_rss(self):
rss_card_dict = self.parse_rss()
for key in rss_card_dict:
self.robot.send_feed_card(rss_card_dict[key])


def send_rss():
rss_bot = RssRobot()
rss_bot.send_rss()

if __name__ == '__main__':
send_rss()

到这里,算是完成了 Rss 钉钉机器人的大部分功能了,还差唯一的定时推送,原本考虑是用 Python 的常见的定时任务库去完成的,但是考虑的后续部署,还需要一台服务器,奈何自己的阿里云服务器最低配不一定能支持住了,就想着结合 Github Actions 把服务器也省了。

Github Actions

Github ActionsGithub推出的一个新的功能,可以为我们的项目自动化地构建工作流,例如代码检查,自动化打包,测试,发布版本等等。入口在项目pull request的旁边。


这里就不再详细介绍 Github Actions 了,贴一下我写的 workflows 文件

workflows

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 钉钉 rss 推送机器人
name: DingDingRssBot

on:
schedule: # 设置定时任务
- cron: '0 0/1 * * *'

jobs:
build:

runs-on: ubuntu-latest
steps:
- name: 'Checkout codes'
uses: actions/checkout@v2 # 拉取最新的代码
- name: Set up Python 3.7.6
uses: actions/setup-python@v2 # 设置 python 版本
with:
python-version: 3.7.6
- name: Install dependencies # 安装依赖
run: |
cd robot
pip install -r requirements.txt
- name: send rss
env:
DD_WEBHOOK: ${{ secrets.webhook }}
DD_SECRET: ${{ secrets.secret }}
run: |
cd robot
python rss.py
- name: Commit
run: |
git config --global user.email leetao94cn@gmail.com
git config --global user.name lt94
git add .
git commit -m "update" -a
- name: Push changes
uses: ad-m/github-push-action@master
with:
branch: main
github_token: ${{ secrets.TOKEN }}

其中三个环境变量需要我们自行配置,在项目的 Settings 中的 Secrets 中设置

其中 TOKEN 是 Person access tokens,需要在个人设置中申请,申请时需要勾选 admin:repo_hook,repo,workflow 三个选项。

Person access tokens 主要是用来将修改推送到仓库的,借助 Github Actions 运行完成之后会把代码删除,会导致产生的修改丢失,这样子的话历史记录就无法保存,因此我们需要在每次推送完成之后,将对数据库的修改推送到仓库中。

总结

到此所有配置完成,Github Actions 会定时执行 rss 推送任务。

有兴趣的可以用钉钉搜索群号:35731271,或者扫描二维码

前言

一直在 Springboot 中使用 Mybatis,基本上使用到的都是 #{}, 由于需求,需要有一个根据视图名可以直接调用视图的函数,一开始 SQL 是这样子:

1
SELECT * FROM #{viewName}

实际运行过程中传入一个视图名称 viewName 发现实际运行的 SQL 语句是这样子的:

1
select * from "viewName";

很明显, SQL 语句存在语法问题,无法执行。最后将 SQL 语句改成:

1
SELECT * FROM ${viewName}

成功运行,那么 #{} 和 ${} 区别是什么?

#{} 和 ${}

从上面的例子不难发现俩者最终的在 SQL 语句的表现形式不一样

  • #{} 会使得传入的参数在 SQL 中通常被 “” 包裹;
  • ${} 则是参数保持原样。

其实使用 #{} 时, Mybatis 会对其传入的参数进行转义,以达到防止 SQL 注入攻击 的目的,而 ${} 则会保持原样,这就意味着我们需要尽可能的使用 #{} 而不是 ${}。${} 通常会用在传递表名或者字段名的情况下,不过在这种情况下需要自定义校验规则,以防止 SQL 注入攻击.

前言

前段时间在服务器上安装 RabbitMQ,安装完成之后启动程序发现一个很奇怪的问题,会出现下面错误导致启动失败:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'bssInteractiveController': Unsatisfied dependency expressed through field 'bssInteractiveRabbitMQProducer'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'bssInteractiveRabbitMQProducer': Invocation of init method failed; nested exception is org.springframework.amqp.AmqpTimeoutException: java.util.concurrent.TimeoutException
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:588)
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:366)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1264)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:553)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483)
at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:761)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:867)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:543)
at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:122)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:693)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:360)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:303)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1118)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1107)
at com.dimpt.netres.RestUiApplication.main(RestUiApplication.java:30)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:48)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:87)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:50)
at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:51)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'bssInteractiveRabbitMQProducer': Invocation of init method failed; nested exception is org.springframework.amqp.AmqpTimeoutException: java.util.concurrent.TimeoutException
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:137)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:409)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1620)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483)
at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:208)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1138)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1066)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:585)
... 27 common frames omitted
Caused by: org.springframework.amqp.AmqpTimeoutException: java.util.concurrent.TimeoutException
at org.springframework.amqp.rabbit.support.RabbitExceptionTranslator.convertRabbitAccessException(RabbitExceptionTranslator.java:74)
at org.springframework.amqp.rabbit.connection.AbstractConnectionFactory.createBareConnection(AbstractConnectionFactory.java:371)
at org.springframework.amqp.rabbit.connection.CachingConnectionFactory.createConnection(CachingConnectionFactory.java:573)
at org.springframework.amqp.rabbit.core.RabbitTemplate.doExecute(RabbitTemplate.java:1430)
at org.springframework.amqp.rabbit.core.RabbitTemplate.execute(RabbitTemplate.java:1411)
at org.springframework.amqp.rabbit.core.RabbitTemplate.execute(RabbitTemplate.java:1387)
at org.springframework.amqp.rabbit.core.RabbitAdmin.declareQueue(RabbitAdmin.java:207)
at com.dimpt.message.service.AbstractRabbitMQBase.init(AbstractRabbitMQBase.java:33)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleElement.invoke(InitDestroyAnnotationBeanPostProcessor.java:366)
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:311)
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:134)
... 39 common frames omitted
Caused by: java.util.concurrent.TimeoutException: null
at com.rabbitmq.utility.BlockingCell.get(BlockingCell.java:77)
at com.rabbitmq.utility.BlockingCell.uninterruptibleGet(BlockingCell.java:120)
at com.rabbitmq.utility.BlockingValueOrException.uninterruptibleGetValue(BlockingValueOrException.java:36)
at com.rabbitmq.client.impl.AMQChannel$BlockingRpcContinuation.getReply(AMQChannel.java:372)
at com.rabbitmq.client.impl.AMQConnection.start(AMQConnection.java:297)
at com.rabbitmq.client.impl.recovery.RecoveryAwareAMQConnectionFactory.newConnection(RecoveryAwareAMQConnectionFactory.java:63)
at com.rabbitmq.client.impl.recovery.AutorecoveringConnection.init(AutorecoveringConnection.java:99)
at com.rabbitmq.client.ConnectionFactory.newConnection(ConnectionFactory.java:900)
at com.rabbitmq.client.ConnectionFactory.newConnection(ConnectionFactory.java:859)
at com.rabbitmq.client.ConnectionFactory.newConnection(ConnectionFactory.java:1013)
at org.springframework.amqp.rabbit.connection.AbstractConnectionFactory.createBareConnection(AbstractConnectionFactory.java:356)
... 52 common frames omitted


从错误信息上很容易看出是连接超时的问题。

问题定位

上网检索答案,大部分出现超时的原因只有一个,他们错把 RabbitMQ 的管理端口 **15672 **当作连接端口,导致超时。但是不幸的是,我并不是这个原因。开始排查日志,日志的信息很少

1
2
3
4
5
6
=INFO REPORT==== 27-Aug-2020::16:23:52 ===
accepting AMQP connection <0.31279.2> (127.0.0.1:45728 -> 127.0.0.1:5672)

=WARNING REPORT==== 27-Aug-2020::16:24:07 ===
closing AMQP connection <0.30726.2> (127.0.0.1:45562 -> 127.0.0.1:5672):
connection_closed_abruptly

一时间陷入瓶颈当中,然后我通过命令行看了一眼 rabbitmq 的状态,有一行信息让我灵光一闪,我觉得问题可能就在这

1
2
3
[root@host-xx-xx-xxx workspace]# rabbitmqctl status
Status of node 'rabbit@host-xx-xx-xxx' ...
...

这个hostname似乎有点问题,然后我立马搜了一下 rabbitmq hostname 的相关信息,发现网上有提到修改 hostname 导致信息丢失等等,还提到要把修改后的 hostname 写到 hosts 文件中。我立马对比了一下我在本地 docker 启动的 rabbitmq 服务的 hosts 文件和服务器上的 hosts 文件,发现通过 docker 启动的,hosts 文件中已经有对应的 hostname 和 127.0.0.1 的映射了,而服务器上的并没有。

docker 中的 hosts 文件

1
2
3
4
5
6
7
8
root@myRabbit:/# cat /etc/hosts 
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.2 myRabbit # myRabit 就是 docker 中启动服务的 hostname

服务器上的 hosts 文件

1
2
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6

问题解决

将服务器上的 hostname 添加到 hosts 文件中,再次重启程序,问题解决。很少有看到关于 RabbitMQ 安装的时候提及 hostname 的问题,可能默认情况下 hostname 是在 hosts 中的。

后记

很少有看到关于 RabbitMQ 安装的时候提及 hostname 的问题,可能默认情况下 hostname 是在 hosts 中的。用 docker 安装省了不少事情,某些不能使用 docker 的场景下,难免会踩坑。

前言

Flutter 开发 App 的时候有一些比较方便特性,比如这次的主题:跳转页面后 AppBar 会自带返回按钮。当然想要出现返回按钮是需要前提的。

使用方法

主页面通常会使用如下代码:

1
2
3
4
5
6
7
8
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage());
}
}

如果想跳转到其他页面,其他页面出现返回按钮的话,在其他页面就不需要使用 MaterialApp 了,直接使用 Scaffold

1
2
3
4
5
6
7
8
class OtherPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ConfigPageStatefulWidget(),
);
}
}