BlueXIII's Blog

热爱技术,持续学习

0%

spring-boot中使用slf4j+log4j

背景

spring-boot默认是使用slf4j+logback做日志输出的, 本文主要演示如何切换为slf4j+log4j。
正常情况下,建议直接使用logback。

在某SpringBoot项目中,因为要使用ELK做日志采集,局方要求使用log4j并给出了一组日志格式规范:

  1. 日志分为xxx-info和xxx-error两个文件,分开打印
  2. 日志格式要求为
    1
    %d{yyyy-MM-dd HH:mm:ss} [%-5p](%-30c{1}) [TxId : %X{PtxId} , SpanId : %X{PspanId}] [ET:%X{ENV_TYPE},AN:%X{APP_NAME},SN:%X{SERVICE_NAME},CN:%X{CONTAINER_NAME},CI:%X{CONTAINER_IP}] %m%n
    所以只能有两种方案可供选择:
  3. 配置logback,并保证与之前的log4j的行为一致
  4. 将logback切换为log4j

由于面对日志格式中的一大堆%比较头大,于是选择了不怎么优雅的方案2,切换为log4j。

配置Maven依赖

首先要显式的引入spring-boot-starter,并将其中的spring-boot-starter-logging排除掉
然后新增一个spring-boot-starter-log4j的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j</artifactId>
<version>1.2.5.RELEASE</version>
</dependency>

配置log4j

新建src/resource/log4j.properties,按正常方式配置即可
下面是一个双日志输出的样例:

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
log4j.rootCategory=info,file_info,file_error,stdout
log4j.logger.org.springframework.web.filter.CommonsRequestLoggingFilter=debug,file_info,stdout
log4j.additivity.org.springframework.web.filter.CommonsRequestLoggingFilter=false

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n

log4j.appender.file_info=org.apache.log4j.RollingFileAppender
log4j.appender.file_info.layout=org.apache.log4j.PatternLayout
log4j.appender.file_info.MaxFileSize=100MB
log4j.appender.file_info.MaxBackupIndex=10
log4j.appender.file_info.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%-5p](%-30c{1}) [TxId : %X{PtxId} , SpanId : %X{PspanId}] [ET:%X{ENV_TYPE},AN:%X{APP_NAME},SN:%X{SERVICE_NAME},CN:%X{CONTAINER_NAME},CI:%X{CONTAINER_IP}] %m%n
log4j.appender.file_info.Threshold=DEBUG
log4j.appender.file_info.append=true
log4j.appender.file_info.File=/opt/logs/busi/crm_sdtools-info.log

log4j.appender.file_error=org.apache.log4j.RollingFileAppender
log4j.appender.file_error.layout=org.apache.log4j.PatternLayout
log4j.appender.file_error.MaxFileSize=100MB
log4j.appender.file_error.MaxBackupIndex=10
log4j.appender.file_error.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%-5p](%-30c{1}) [TxId : %X{PtxId} , SpanId : %X{PspanId}] [ET:%X{ENV_TYPE},AN:%X{APP_NAME},SN:%X{SERVICE_NAME},CN:%X{CONTAINER_NAME},CI:%X{CONTAINER_IP}] %m%n
log4j.appender.file_error.Threshold=ERROR
log4j.appender.file_error.append=true
log4j.appender.file_error.File=/opt/logs/busi/crm_sdtools-error.log

简介

Spring3.0以下的遗留工程,由于无法使用RestTemplate,所以调用REST接口时会比较繁琐。
通过使用第三方REST户端框架Resty,可以大幅简化调用过程。
Resty的优势是使用简单,不需要引入过多的依赖。但相比于resttemplate/jersey/resteasy等,Resty还是过于小众,而且已有2年时间没有维护,异常处理也不是很完善。所以只建议在老旧项目中使用。
本文演示了Resty各种用法,包括代理设置、Header设置等。

添加依赖

对于较老的项目,直接下载并添加resty-0.3.2.jar

1
wget http://repo1.maven.org/maven2/us/monoid/web/resty/0.3.2/resty-0.3.2.jar

如果项目使用了maven或gradle,直接添加依赖即可

1
2
3
4
5
<dependency>
<groupId>us.monoid.web</groupId>
<artifactId>resty</artifactId>
<version>0.3.2</version>
</dependency>
1
compile 'us.monoid.web:resty:0.3.2'

示例

初始化

1
2
String baseUrl = "http://127.0.0.1:8080/foobar";
Resty resty = new Resty();

设置代理

1
resty.setProxy("127.0.0.1", 8888);

设置token

1
2
3
4
5
6
7
8
resty.setOptions(new Resty.Option() {
@Override
public void apply(URLConnection aConnection) {
aConnection.setRequestProperty("Authorization", "Bearer foobarboobarfoobar");
super.apply(aConnection);
}
});

GET 查询

1
2
3
String restUrl = baseUrl + "/users/4038488549360733";
JSONObject jsonObject = resty.json(restUrl).object();
String serialNumber = jsonObject.getString("serialNumber");

PUT 修改

1
2
3
String restUrl = baseUrl + "/users/4038488549360733";
String jsonStr = "{\"brandCode\":\"3G02\"}";
resty.json(restUrl, Resty.put(new Content("application/json", jsonStr.getBytes())));

POST 新增

1
2
3
4
String restUrl = baseUrl + "/users";
String jsonStr = "{\"brandCode\":\"3G03\"}";
resty.json(restUrl, new Content("application/json", jsonStr.getBytes()));

DELETE 删除

1
2
String restUrl = baseUrl + "/users/4038488549360733";
resty.json(restUrl, Resty.delete());

综合示例

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
String serialNumber = "12345678";
String restUrl;
JSONObject jsonObject;
JSONArray jsonArray;
String itemId = "";

try {
// 查询用户信息,通过serialNumber取到userId
restUrl = baseUrl + "/users?serialNumber=" + serialNumber;
jsonObject = resty.json(restUrl).object();
System.out.println(jsonObject);
String userId = jsonObject.getString("userId");

// 查询资费信息,通过userId取到所有资费
restUrl = baseUrl + "/discnts?userId=" + userId;
jsonArray = resty.json(restUrl).array();
for (int i = 0; i < jsonArray.length(); i++) {
jsonObject = jsonArray.getJSONObject(i);
System.out.println(jsonObject);
// 随机取一个itemId
itemId = jsonObject.getString("itemId");
}

// 更改资费信息,通过itemId修改资费信息
restUrl = baseUrl + "/discnts/" + itemId;
String jsonStr = "{\"startDate\":\"2001-01-01 20:01:02\", \"endDate\":\"2018-05-08 20:01:02\"}";
resty.json(restUrl, Resty.put(new Content("application/json", jsonStr.getBytes())));
} catch (Exception e) {
e.printStackTrace();
}

GitHub

https://github.com/beders/Resty

引言

cx_Oracle是Python环境下的一个(好像也是唯一的一个)用于操作Oracle的第三方模块。
最近在写某个对帐程序时,不得已要连接一台Oracle库,于是用到了cx_Oracle,总的来说还是比较顺利的,期间遇到几个有意思的小坑写出来分享一下。

RHEL 6.4下安装cx_Oracle

RHEL6.4和cx_Oracle比较搭,安装时应该不会遇到什么挫折,使用RPM安装好instantclient后,直接用pip3安装cx_Oracle即可。

需要注意地方:

  1. 尽量使用RPM方式安装instantclient,安装更方便,而且比zip更好管理。
  2. instantclient的版本选择的是11.2.0.4.0,没有选择12。对于Oracle/WebLogic这类闭源的东西,还是选择次最新版本的比较稳妥。

安装步骤:

  1. 下载Linux版本的instantclient
    这里是 Oracle官网 instantclient下载页面 ,下载以下3个RPM包:
    oracle-instantclient11.2-basic-11.2.0.4.0-1.x86_64.rpm
    oracle-instantclient11.2-devel-11.2.0.4.0-1.x86_64.rpm
    oracle-instantclient11.2-sqlplus-11.2.0.4.0-1.x86_64.rpm

  2. 安装instantclient并设置环境变量

    1
    2
    3
    4
    yum -y install libaio bc flex
    rpm -ivh oracle-instantclient11.2-basic-11.2.0.4.0-1.x86_64.rpm
    rpm -ivh oracle-instantclient11.2-devel-11.2.0.4.0-1.x86_64.rpm
    rpm -ivh oracle-instantclient11.2-sqlplus-11.2.0.4.0-1.x86_64.rpm
  3. 设置环境变量

    1
    2
    3
    4
    5
    echo 'export ORACLE_VERSION="11.2"' >> $HOME/.bashrc
    echo 'export ORACLE_HOME="/usr/lib/oracle/$ORACLE_VERSION/client64/"' >> $HOME/.bashrc
    echo 'export PATH=$PATH:"$ORACLE_HOME/bin"' >> $HOME/.bashrc
    echo 'export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:"$ORACLE_HOME/lib"' >> $HOME/.bashrc
    source $HOME/.bashrc
  4. 使用pip安装cx_Oracle

    1
    pip3 install cx_Oracle

    macOS 12下安装cx_Oracle

    macOS下,安装cx_Oracle后的编译过程有可能会报错,一般是因为instantclient安装有误造成的。

需要注意的地方:

  1. mac下的instantclient只有zip包一种安装方式,要注意手工建两个软链接。
  2. pip安装前,注意导入LD_LIBRARY_PATH与DYLD_LIBRARY_PATH两个环境变量。

安装过程:

  1. 下载Mac版本的instantclient
    下载以下3个zip包,并unzip解压至同一目录:
    instantclient-basic-macos.x64-11.2.0.4.0.zip
    instantclient-sdk-macos.x64-11.2.0.4.0.zip
    instantclient-sqlplus-macos.x64-11.2.0.4.0.zip

  2. 建立软链接

    1
    2
    3
    cd /path/to/instant
    ln -s libclntsh.dylib.11.2 libclntsh.dylib
    ln -s libocci.dylib.11.2 libocci.dylib
  3. 设置环境变量

    1
    2
    3
    export ORACLE_VERSION="11.2"
    export ORACLE_HOME="/path/to/instantclient_11_2"
    export PATH=$PATH:"$ORACLE_HOME"
  4. 使用pip安装cx_Oracle,注意提前导入DYLD_LIBRARY_PATH与LD_LIBRARY_PATH

    1
    2
    3
    export DYLD_LIBRARY_PATH="$ORACLE_HOME"
    export LD_LIBRARY_PATH="$ORACLE_HOME"
    pip3 install cx_Oracle

    中文乱码问题

    Oracle中文乱码问题存在已久,使用cx_Oracle时也不例外,解决方法还是设置NLS_LANG环境变量。
    有两种方式,一是在系统中设置永久环境变量,二是直接在代码中使用os.environ设置环境变量,这里推荐后者。

  5. 方法一:在Shell中设置环境变量

    1
    export NLS_LANG="SIMPLIFIED CHINESE_CHINA.UTF8"
  6. 方法二:直接在代码中加入:

    1
    2
    import os
    os.environ['NLS_LANG'] = 'SIMPLIFIED CHINESE_CHINA.UTF8'

PyCharm下不识别cx_Oracle问题

安装安成后,在python的console中已经可以import cx_Oracle了,但在PyCharm中却提示找不到cx_Oracle。这是一个比较大的坑,可以详细讲一下处理过程。

首先初步定位到原因,是由于PyCharm中没有定义LD_LIBRARY_PATH与DYLD_LIBRARY_PATH两个环境变量造成的。
PyCharm会自动读取系统中的环境变量设置并导入,但唯独这两个没有导进来。不深究原因,先尝试手工在PyCharm中配置这两个环境变量,总共有两处可以配置:

第一处:
CMD+,打开Preference,找到 Build,Execution,Deployment -> Console -> Python Console ->Enviroment Variables

此处的配置会修复PyCharm中的Console。

第二处:
右上角 Run -> Edit Configurations,添加两条环境变量

此处的配置会修复PyCharm中Ctrl+Shift+R运行代码时的报错。

在以上两处手工添加环境变量:

1
2
DYLD_LIBRARY_PATH=/path/to/instantclient
LD_LIBRARY_PATH/path/to/instantclient

两处的环境变量配置完成后,虽然console中可以正常使用cx_Oracle了,代码也可以正常运行了,但编辑界面中的inspection还是有问题的,提示有Error,并且不能使用自动完成功能。

于是回到之前的问题,为什么明明已经定义了,但PyCharm却没有找到LD_LIBRARY_PATH与DYLD_LIBRARY_PATH?
同时偶然发现,在执行env命令查看已定义的环境变量时,也是找不到LD_LIBRARY_PATH与DYLD_LIBRARY_PATH的。
这貌似已经不是PyCharm自身的问题了,需要从macOS系统来着手了。

于是在stackoverflow上找到了这么一段话:

El Capitan added system integrity protection (SIP), and one side effect of that is that exporting DYLD_LIBRARY_PATH doesn’t work. That could affect running SQL*Plus from a shell script, for example. There are workarounds for the 11g instant client. The installation notes at the bottom of the download page have changed since I last did this, and it now says to hard link the library files to the user’s ~/lib directory to avoid that issue. Fortunately it looks like you don’t need to worry about that with the 12c client - they’ve fixed the way it’s built.

看来根本原因是OSX 10.11之后加入的这个SIP引发的了。

首先想到的最简单的方法,将所有的.dylib.h都拷贝到系统默认的目录就可以了。
但很杯具,/usr/lib/user/include这两个目录也被SIP保护了。因为不想强制关闭SIP,继续再想别的办法。
接着发现/usr/local/lib/user/local/include目录还是可以操作的,于是拷到这两个目录:

1
2
3
cd /path/to/instantclient
cp *.dylib /usr/local/lib
cp ./sdk/include/*.h /usr/local/include

再将cx_Oracle卸载后重新编译安装:

1
2
sudo pip3 uninstall cx_Oracle
sudo pip3 install cx_Oracle

重启PyCharm后,问题解决。

cx_Oracle的简单使用

cx_Oracle的使用上没有什么问题,一切按套路来写就可以了。贴一个简单的示例:

1
2
3
4
5
6
7
8
9
import cx_Oracle
conn = cx_Oracle.connect('username', 'password','host:port/sid')
cursor = conn.cursor()
cursor.execute('select column from table')
result = cursor.fetchall()
for row in result:
print(row)
cursor.close()
conn.close()

更加详细的内容请参考官方文档:
https://cx-oracle.readthedocs.io/en/latest/
https://oracle.github.io/python-cx_Oracle/

参考文档

https://gist.github.com/thom-nic/6011715
http://stackoverflow.com/questions/37711482/how-to-install-oracle-instant-client-on-a-mac

引言

继续接着上次的话题,上周整理出来的flask-rest脚手架,用于快速构建Restful API。虽然后端的代码量很少,但前端会变得很重,所以总体开发速度并不算太快,不太适合小型项目。
于是又整理了一套flask-adminlte脚手架,目标是用较传统的方式,减少整体的代码量,加快开发速度,以应对一些比较极端的需求。大体估算,开发时间可以做到SpringBoot全家桶的30%左右。

源码

http://git.si-tech.com.cn/guolei/flask-adminlte-handler
https://github.com/xiiiblue/flask-rest-sample

简介

flask-adminlte-handler是一个Python环境下的WEB后台管理系统脚手架,目标是用极少量的代码,快速构建小型WEB应用。请勿在大中型项目中进行尝试。

  1. 使用较传统的重后端+轻前端的方式,降低总体代码量
  2. Web框架使用Flask,默认Jinja模版
  3. ORM框架使用Peewee
  4. 前端套用基于BootStrap的AdminLTE模板

系统截图

  • 登录页

  • 主页

  • 编辑界面

  • 查询界面

第三方依赖

  • peewee
  • pymysql
  • flask
  • flask-script
  • flask-wtf
  • flask-login

环境配置

venv虚拟环境安装配置

1
2
3
sudo pip3 install virtualenv
virtualenv venv
. venv/bin/activate

第三方依赖安装

1
2
pip3 install -r requirements.txt

系统参数配置

  1. 编辑config.py, 修改SECRET_KEY及MySQL数据库相关参数

    1
    2
    3
    4
    5
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secret'
    DB_HOST = '127.0.0.1'
    DB_USER = 'foobar'
    DB_PASSWD = 'foobar'
    DB_DATABASE = 'foobar'
  2. 编辑log-app.conf,修改日志路径

    1
    args=('/path/to/log/flask-rest-sample.log','a','utf8')

    数据库初始化

  3. 自动建表
    直接运行python3 models.py

  4. 插入管理员用户(默认admin/admin)

    1
    2
    3
    INSERT INTO `user` (`id`, `username`, `password`, `fullname`, `email`, `phone`, `status`)
    VALUES
    (1, 'admin', 'pbkdf2:sha1:1000$Km1vdx3W$9aa07d3b79ab88aae53e45d26d0d4d4e097a6cd3', '管理员', 'admin@admin.com', '18612341234', 1);

    启动应用

    1
    2
    3
    nohup ./manage.py runserver 2>&1 &

    ./run_app_dev.py (仅限测试)

项目目录结构

  • /app/auth 用户认证相关代码
  • /app/main 主要功能点相关代码
  • /app/static JS、CSS等静态文件
  • /app/template 页面模版
  • /app/models.py Peewee模型
  • /app/utils.py 工具模块
  • /conf 系统参数及日志配置

相关学习文档

引言

最近在某个小型项目上进行了一些尝试,目标是用极快的速度构建一套简洁且优雅的小型WEB系统。当然这次尝试可以说是失败的,稍后会提到。

架构上主要是前后端分离,后端使用Python实现RESTFul API,前端直接套用AdminLTE模版。

得益于Python动态语言的特性,后端可以做到非常少量的代码实现增删改查等基本功能,大约是可以使用100多行代码写了25个REST服务吧,非常惊人。虽然比较简陋,但比起之前使用Spring Boot全家桶来开发REST,要快得太多。

但比起后端,前端的代码量,初版写出来之后,是后端的几十倍,使用gulp构建,include等插件后,做到了一定简化,接下来还要做SPA化,引入vue.js、webpack等等,最终会变成 为了目的不择手段,为了手段而忘记目的 。一个很重要的前提是,要做一个 小型WEB系统 ,整体成本要小。所以前后端分离虽然好处多多,但前端太重了,并不适合小型系统。

最终还是使用了Flask+Jinja,传统的重后端、轻前端的方式来实现,将总体代码量控制到了极少。
但这次的REST API部分,不失为一次有益的尝试,单独拿出来做成脚手架,提供给有需要的人吧。

源码

http://git.si-tech.com.cn/guolei/flask-rest-sample
https://github.com/xiiiblue/flask-rest-sample

简介

flask-rest-sample 是一个使用Python编写的REST API,可以对多个资源提供增删改查服务,用于快速构建REST应用。

  1. Web框架使用Flask
  2. ORM框架使用peewee
  3. 安全方面使用了flask-jwt插件进行JWT(Json Web Token)认证
  4. 由于flask-rest/flask-restplus侵略性较强,本次没有使用。
  5. 可以考虑自行使用flasgger添加SwaggerUI文档。

第三方依赖

  • peewee
  • pymysql
  • Flask
  • Flask-JWT
  • flask-script

主要代码示例

  1. 建模时尽量避免使用外键等约束条件,保证模型结构上一致,可以做到共用一个公共服务。

  2. 可使用@rest.route添加多个资源

    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
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    # 基本API
    @rest.route('/api/reports', methods=['GET', 'POST'])
    @rest.route('/api/reports/<id>', methods=['GET', 'PUT', 'DELETE'])
    @rest.route('/api/res2', methods=['GET', 'POST'])
    @rest.route('/api/res2/<id>', methods=['GET', 'PUT', 'DELETE']) #只要模型结构一致,可以添加多个资源
    @jwt_required()
    def common_rest_api(id=None):
    model_name = request.path.split('/')[2]
    pee_wee_model = utils.get_model_by_name(model_name) #从URL中确定模型
    if not pee_wee_model:
    return utils.jsonresp(status=400, errinfo='model_name不正确')

    if id:
    # 查询
    if request.method == 'GET':
    try:
    model = pee_wee_model.get(pee_wee_model.id == id)
    except:
    return utils.jsonresp(status=404, errinfo='查询不到资料')
    return utils.jsonresp(jsonobj=utils.obj_to_dict(model))
    # 修改
    elif request.method == 'PUT':
    json_dict = request.get_json(silent=True, force=True)
    if not json_dict: return utils.jsonresp(status=400, errinfo='参数格式不正确')
    try:
    model = pee_wee_model.get(pee_wee_model.id == id)
    except:
    return utils.jsonresp(status=404, errinfo='查询不到资料')
    utils.dict_to_obj(dict=json_dict, obj=model, exclude=('id',)) # 去掉ID字段
    model.save()
    return utils.jsonresp(status=201)
    # 删除
    elif request.method == 'DELETE':
    try:
    pee_wee_model.get(pee_wee_model.id == id).delete_instance()
    except:
    return utils.jsonresp(status=404, errinfo='查询不到资料')
    return utils.jsonresp(status=204)
    else:
    return utils.jsonresp(status=405, errinfo='不支持的HTTP方法')
    else:
    # 全量查询(支持分页、排序、搜索)
    if request.method == 'GET':
    # 处理查询参数
    logger.debug(request.args)
    try:
    # 当前页码
    page = request.args.get('page', '')
    if page: page = int(page) + 1
    # 每页展示数量
    length = request.args.get('length', '')
    if length:
    length = int(length)
    else:
    length = cfg.ITEMS_PER_PAGE
    # 排序
    sort = request.args.get('sort', '')
    if sort:
    sort_column = sort.split(',')[0]
    sort_direction = sort.split(',')[1]
    except:
    return utils.jsonresp(status=400, errinfo='参数格式不正确')

    # 查询
    query = pee_wee_model.select()
    total_count = query.count()

    # 排序
    if sort:
    if sort_column in pee_wee_model._meta.fields:
    field = getattr(pee_wee_model, sort_column)
    if sort_direction != 'asc':
    field = field.desc()
    query = query.order_by(field)
    # 分页
    if page:
    query = query.paginate(page, length)

    dict = {'content': utils.query_to_list(query), 'totalElements': total_count}
    return utils.jsonresp(jsonobj=dict)
    # 新增
    elif request.method == 'POST':
    json_dict = request.get_json(silent=True, force=True)
    if not json_dict: return utils.jsonresp(status=400, errinfo='参数格式不正确')
    user = utils.dict_to_obj(dict=json_dict, obj=pee_wee_model(), exclude=['id']) # 去掉ID字段
    user.save()
    return utils.jsonresp(status=201)
    else:
    return utils.jsonresp(status=405, errinfo='不支持的HTTP方法')

    环境配置

    venv虚拟环境安装配置

    1
    2
    3
    sudo pip3 install virtualenv
    virtualenv venv
    . venv/bin/activate

    第三方依赖安装

    1
    2
    pip3 install -r requirements.txt

    系统参数配置

  3. 编辑config.py, 修改SECRET_KEY及MySQL数据库相关参数

    1
    2
    3
    4
    5
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secret'
    DB_HOST = '127.0.0.1'
    DB_USER = 'foobar'
    DB_PASSWD = 'foobar'
    DB_DATABASE = 'foobar'
  4. 编辑log-app.conf,修改日志路径

    1
    args=('/path/to/log/flask-rest-sample.log','a','utf8')

    数据库初始化

  5. 自动建表
    直接运行python3 models.py

  6. 插入管理员用户(默认admin/admin)

    1
    2
    3
    INSERT INTO `user` (`id`, `username`, `password`, `fullname`, `email`, `phone`, `status`)
    VALUES
    (1, 'admin', 'pbkdf2:sha1:1000$Km1vdx3W$9aa07d3b79ab88aae53e45d26d0d4d4e097a6cd3', '管理员', 'admin@admin.com', '18612341234', 1);

    启动应用

    1
    2
    3
    nohup ./manage.py runserver 2>&1 &

    ./run_app_dev.py (仅限测试)

REST接口说明

以reports资源为例:

  • GET /api/reports/ 查询
    200 成功
  • PUT /api/reports/ 修改
    201 成功

  • DELETE /api/reports/ 删除
    204 成功

  • POST /api/reports 新增
    200 成功

  • GET /api/reports 全量查询
    200 成功

    支持分页(URL参数:page、length)及排序(URL参数:sort)
    参数示例:?page=1&length=5&sort=serial_number,desc

JWT认证简单说明

  1. 向/api/auth发起用户认证请求
    1
    2
    3
    4
    5
    6
    POST http://127.0.0.1:5000/api/auth
    Content-Type: application/json
    {
    "username": "admin",
    "password": "admin"
    }
  2. 获取响应,取得access_token
    1
    2
    3
    {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0OTE2MzUyNTQsIm5iZiI6MTQ5MTYzNTI1NCwiaWRlbnRpdHkiOjEsImV4cCI6MTQ5MTYzNTU1NH0.wq-uer9LbRP5hEYGT4WfD5O4jf7k7du2Q1K6musKzvU"
    }
  3. 在接下来的HTTP请求头中,加入Authorization,值为JWT+空格+access_token
    以获取当前用户信息为例
    请求:
    1
    2
    GET http://127.0.0.1:5000/api/identity
    Authorization: JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0OTE2MzUyNTQsIm5iZiI6MTQ5MTYzNTI1NCwiaWRlbnRpdHkiOjEsImV4cCI6MTQ5MTYzNTU1NH0.wq-uer9LbRP5hEYGT4WfD5O4jf7k7du2Q1K6musKzvU
    响应:
    1
    2
    3
    4
    5
    6
    {
    "email": "admin@admin.com",
    "fullname": "管理员",
    "username": "admin",
    "phone": "18612341234"
    }

简介

Gradle是一种类似Maven的项目构建工具,它没有使用繁琐的XML,而是使用Groovy语言进行配置。
作为后起之秀,Gradle继承了Maven的一些思想,并且 配置简洁 ,有更强的 灵活性

Android Studio从一定程度上也加快了Gradle的流行,目前有非常多的开源项目已经迁移到了Gradle。
但现阶段Gradle还不能完全替代Maven,从目前GitHub上的趋势看来,二者可能要并存一段时间了。

从去年开始在一些中小型项目上尝试引入Gradle,仅做简单的项目构建,没有太过深入研究,整体使用下来的体验还是很愉快的。
本文主要是作一些科普,抛砖引玉,并贴出两个可以直接拿来使用的示例,帮助大家快速上手。

与Maven简单对比

  1. 简洁的配置。现在有很多人都对XML深恶痛绝,在Maven中,添加一个依赖需要编写以下5行配置:

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>2.5.0</version>
    </dependency>

    而在Gradle中,只需要一行:

    1
    compile 'com.zaxxer:HikariCP:2.5.0'

    所以Gradle配置文件的整体长度大约是Maven的1/4到1/5左右,并且更加易读。

  2. 灵活性。例如要执行一条shell命令,只需要3行。当然客观来说,灵活往往是复杂的同义词:

    1
    2
    3
    task dropDB(type: Exec) {
    commandLine ‘curl’,’-s’,’s’,’-x’,’DELETE’,"http://${db.server}:{db.port}/db_name"
    }
  3. 约定优于配置。Gradle的Java Plugin,定义了与Maven完全一致的项目布局:

    1
    2
    3
    4
    src/main/java
    src/main/resources
    src/test/java
    src/test/resources

    更多的比较,可以参考 https://gradle.org/maven-vs-gradle

安装

  • macOS下安装:
    1
    brew install gradle
  • Ubuntu下安装:
    1
    sudo apt install gradle
  • Windows下安装:
    1
    2
    3
    Step1: 在[https://gradle.org/releases](https://gradle.org/releases) 下载binary-only的zip包  
    Step2: 解压至某一目录,如C:/bin/gradle
    Step3: 在系统属性-高级-环境变量中,新增GRADLE_HOME环境变量来指向安装路径,并在PATH环境变量的最后追加上GRADLE_HOME/bin

    学习资源

    关于Gradle的学习,不再赘述,有大量的资源可供查阅
  • Gradle 官网
  • Gradle User Guide
  • Gradle User Guide 中文版

单模块项目构建示例

下面是一个单模块Spring Boot项目的示例,适合快速搭建小型项目

/build.gradle

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
buildscript {
repositories {
flatDir {
dirs 'libs'
}
mavenLocal()
maven { url "http://xxx.xxx.xxx.xxx:8081/nexus/content/groups/public" }
// // mavenCentral() //jcenter()
}

dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.4.0.RELEASE")
}
}

apply plugin: 'java'
apply plugin: "spring-boot"

jar {
baseName = 'project'
version = '1.0-SNAPSHOT'
}

sourceCompatibility = 1.7
targetCompatibility = 1.7

repositories {
mavenCentral()
}

dependencies {
compile 'org.springframework.boot:spring-boot-starter'
compile 'org.apache.commons:commons-email:1.4'
compile files('libs/jxl-2.6.12.jar')
runtime 'mysql:mysql-connector-java:5.1.36'
testCompile 'org.springframework.boot:spring-boot-starter-test'
}

test {
exclude 'com/foo/**'
}

task listJars(description: 'Display all compile jars.') << {
configurations.compile.each { File file -> println file.name }
}

多模块项目构建示例

下面是一个基于Spring Boot的多模块项目示例,可以裁剪后直接拿来做为脚手架使用,适合中型项目

/settings.gradle

1
2
3
include 'common'
include 'repository'
include 'restapi'

/build.gradle

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
buildscript {
repositories {
mavenLocal()
maven { url "http://xxx.xxx.xxx.xxx:8081/nexus/content/groups/public" }
//mavenCentral()
//jcenter()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.4.0.RELEASE")
}
}

allprojects {
apply plugin: 'idea'
apply plugin: 'eclipse'

group = 'com.foo.bar'
version = '1.0-SNAPSHOT'
//archivesBaseName = 'project'

repositories {
mavenLocal()
maven { url "http://xxx.xxx.xxx.xxx:8081/nexus/content/groups/public" }
//mavenCentral()
//jcenter()
}
}

subprojects {
apply plugin: 'java'
apply plugin: "spring-boot"

sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
compile 'org.springframework.boot:spring-boot-starter'
compile 'org.springframework.boot:spring-boot-configuration-processor'
compile 'org.springframework.boot:spring-boot-devtools'
testCompile 'org.springframework.boot:spring-boot-starter-test'
}

test {
exclude 'com/foo/bar/**'
}

task listJars(description: 'Display all compile jars.') << {
configurations.compile.each { File file -> println file.name }
}
}

task wrapper(type: Wrapper) {
gradleVersion = '3.1'
}

/common/build.gradle

1
2
3
4
5
6
7
8
9
10
bootRepackage.enabled = false
jar.baseName 'project-common'

dependencies {
compile project(':repository')
compile 'org.springframework.boot:spring-boot-starter-web'
compile 'commons-lang:commons-lang:2.6'
compile 'org.apache.httpcomponents:httpclient:4.5.2'
compile 'org.modelmapper:modelmapper:0.7.5'
}

/repository/build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bootRepackage.enabled = false
jar.baseName 'project-repository'

dependencies {
//spring boot
compile ('org.springframework.boot:spring-boot-starter-data-jpa')
compile 'org.springframework.boot:spring-boot-starter-jdbc'
//jackson
compile 'com.fasterxml.jackson.core:jackson-annotations:2.8.1'
//hibernate validator
compile 'javax.validation:validation-api:1.1.0.Final'
compile 'org.hibernate:hibernate-validator:5.2.4.Final'
compile 'org.hibernate:hibernate-validator-cdi:5.2.4.Final'
compile 'javax.el:javax.el-api:2.2.4'
compile 'org.glassfish.web:javax.el:2.2.4'
//swagger
compile 'io.springfox:springfox-swagger2:2.6.0'
compile 'io.springfox:springfox-swagger-ui:2.6.0'
//jdbc driver
runtime 'mysql:mysql-connector-java:5.1.36'
runtime 'com.h2database:h2:1.4.192'
//test
testCompile 'org.springframework.security:spring-security-core:4.1.1.RELEASE'
}

/restapi/build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
jar.baseName 'project-rest-api'

dependencies {
compile project(':repository')
compile project(':common')
//spring boot
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
compile 'org.springframework.boot:spring-boot-starter-cache'
compile 'org.springframework.boot:spring-boot-starter-security'
compile 'org.springframework.boot:spring-boot-starter-actuator'
//oauth2
compile 'org.springframework.security.oauth:spring-security-oauth2:2.0.11.RELEASE'
//cache
compile 'net.sf.ehcache:ehcache:2.10.2.2.21'
//pool
compile 'com.alibaba:druid:1.0.23'
compile 'com.zaxxer:HikariCP:2.5.0'
//swagger-staticdocs
testCompile 'io.springfox:springfox-staticdocs:2.6.0'
}

现象

macOS版SoapUI启动后,窗口卡死,只能Cmd-Option-Esc强制退出。

系统版本: macOS Sierra 10.12.4
SoapUI版本: 5.3.0/5.2.1

解决方法

  1. 修改vmoptions.txt

    1
    vi /Applications/SoapUI-5.3.0.app/Contents/vmoptions.txt

    最后下新增一行-Dsoapui.browser.disabled=true

  2. 修改soapui.sh

    1
    vi /Applications/SoapUI-5.3.0.app/Contents/java/app/bin/soapui.sh

    JAVA_OPTS="$JAVA_OPTS -Dsoapui.browser.disabled=true"一行的注释去掉。

简介

某同事要用Socket连接外围系统,帮忙写了个简单的Demo。
包含一个客户端和一个服务端的,仅是示例,比较简陋,服务端也没有用多线程。
直接贴代码了。

服务端

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
76
77
import java.io.*;
import java.net.*;

/**
* Socket服务端示例
*/
public class SocketServer {
ServerSocket serverSocket;
Socket connection = null;
ObjectOutputStream out;
ObjectInputStream in;
String message;

void runServer() {
try {
// 创建服务端监听
serverSocket = new ServerSocket(31313, 10);

// 等待客户端连接
System.out.println("等待客户端连接...");
connection = serverSocket.accept();
System.out.println("收到客户端连接: " + connection.getInetAddress().getHostName());

// 获取输入输出流
out = new ObjectOutputStream(connection.getOutputStream());
out.flush();
in = new ObjectInputStream(connection.getInputStream());

// 连接成功后,首先向客户端发成功消息
sendMsg("连接成功");

// 发送接收消息
do {
try {
message = (String) in.readObject();
System.out.println("client>" + message);

// 发送退出消息
if (message.equals("bye")) {
sendMsg("bye");
}
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
System.err.println("数据格式无效");
}
} while (!message.equals("bye")); //当对方消息为bye时退出循环
} catch (IOException ex) {
ex.printStackTrace();
} finally {
// 关闭Socket连接
try {
in.close();
out.close();
serverSocket.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}

void sendMsg(String msg) {
try {
out.writeObject(msg);
out.flush();
System.out.println("server>" + msg);
} catch (IOException ex) {
ex.printStackTrace();
}
}

public static void main(String args[]) {
SocketServer server = new SocketServer();
while (true) {
server.runServer();
}
}
}

客户端

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
import java.io.*;
import java.net.*;

/**
* Socket客户端示例
*/
public class SocketClient {
Socket clientSocket;
ObjectOutputStream out;
ObjectInputStream in;
String message;

void runClient() {
try {
// 连接到服务端
clientSocket = new Socket("localhost", 31313);
System.out.println("已连接到服务端");

// 获取输入输出流
out = new ObjectOutputStream(clientSocket.getOutputStream());
out.flush();
in = new ObjectInputStream(clientSocket.getInputStream());

// 发送接收消息
do {
try {
// 接收消息
message = (String) in.readObject();
System.out.println("server>" + message);

// 发送消息
sendMsg("hello");
sendMsg("hello again");

// 发送退出消息
message = "bye";
sendMsg(message);
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
System.err.println("数据格式无效");
}
} while (!message.equals("bye")); //当对方消息为bye时退出循环
} catch (UnknownHostException ex) {
ex.printStackTrace();
System.err.println("无法连接到服务端");
} catch (IOException ex) {
ex.printStackTrace();
} finally {
// 关闭Socket连接
try {
in.close();
out.close();
clientSocket.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}

void sendMsg(String msg) {
try {
out.writeObject(msg);
out.flush();
System.out.println("client>" + msg);
} catch (IOException ex) {
ex.printStackTrace();
}
}

public static void main(String args[]) {
SocketClient client = new SocketClient();
client.runClient();
}
}

问题

最初安装Ubuntu时,/boot分区只给了200M空间(使用CentOS延续下来的习惯),结果每次生级内核时都会报错/boot空间不足。
下次全新安装Ubuntu,一定记得分出2G空间来给/boot。

临时解决方法

  1. uname -a 查看目前在用的内核版本
  2. dpkg --get-selections|grep linux-image 显示所有的内核版本
  3. sudo apt remove linux-image-XXXX-generic 卸载旧内核,注意只保留最新的 两个 版本
  4. sudo apt autoremove 清理无用的依赖

什么是个人知识管理

个人知识管理,又名PKM(Personal Knowledge Management), 是一种个人收集,验证,存储,搜索,提取,分享知识的过程。 –WIKI
PKM其实是一个很大的命题,包含了很多方面的内容。但我们每个人都在或多或少的使用它,都有自已的一些工具、心得。
以下仅讨论一下具体的笔记、摘录等文档的 编写保存

什么是MarkDown

Markdown是一种轻量级标记语言,它允许人们“使用易读易写的纯文本格式编写文档,然后转换成有效的XHTML(或者HTML)文档”。这种语言吸收了很多在电子邮件中已有的纯文本标记的特性。
MarkDown第一眼看上去这个样子的:

左侧是”源码”,右侧是渲染好的”结果”

MarkDown的优势

MarkDown有很多先天的优势,决定了它非常适合用来编写文档、记录笔记、撰写文章:

  • 首先,MarkDown是 纯文本 ,这意味着我们可以使用任何自已喜欢的文本编辑器来进行写作,例如vi/emacs/SublimeText/Atom/VsCode/记事本/EditPlus/UltraEdit等等。
  • 然后,MarkDown在纯文本的基础上,加入某些极其简单标记,例如用#表示一级标题,##表示二级标题等。相比起HTML,要简单的多,不会对文章内容造成过多的入侵。
  • 虽然没有官方机构,但MarkDown目前已经是事实上的标准,而且已经非常流行,在GitHub等网站中有很好的应用。
  • 市面上目前有许多优秀的MarkDown编辑器(或插件),可以提供更优雅、更舒适的写作体验。
  • MarkDown很容易导出为HTML或PDF格式,而且大多数工具都可以提供自定义的样式。

MarkDown语法速成

MarkDown语法非常简单,下面列出一些基本的标记:

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
标题:  
# 一级标题
## 二级标题
### 三级标题
#### 四级标题
##### 五级标题
###### 六级标题

文字样式:
**用两个星号标记起来,表示加粗**
*一个星号,表示斜体*
~~这样子表示删除线~~

插入链接:
[链接名称](http://url.com/)

邮箱链接:
<myname@example.com>

插入图片:
![图片的信息,可为空](这里是图片的url,可以是本地路径,也可以是远程URL)

无序列表:
- Red
- Green
- Blue

有序列表:
1. Bird
2. McHale
3. Parish

内容引用:
> 你
> 一会看我
> 一会看云
> 我觉得
> 你看我时很远
> 你看云时很近

分割线:
`-`加上空格组成,三个以上
- - - - - -

代码高亮:
使用3个连续的`,将代码内容包起来
` ` `
s = 'hello world'
` ` `

选择适合自已的编辑器

MarkDown不需要专门工具就可以编写,任何纯文本编辑器都可以胜任。但选择一款适合自已的工具可以提供代码高亮,实时预览,自定义主题,PDF导出等高级功能,极大的提升我们的写作体验。
以下列出一些比较热门的工具:

全平台:
Atom 不用过多介绍了,GitHub出品,被称为是新时代的文本编辑器,Emacs的精神继承者。通过非富的插件,很好的支持MarkDown,并提供了无限的可能性。
Sublime Text 3 更不用介绍了,同样是利用插件支持。
MarkEditor 商业软件,使用Electron写的跨平台应用。功能强大,缺点是启动速度较慢。

Mac平台:
Mweb 商业软件,作者是国人。
Mou 商业软件,Mac下老牌的编辑器了。
MacDown 功能与Mou基本相同,免费使用。
Typora 所见即所得的编辑器,输入MarkDown标记后会立刻渲染出结果。

Win平台:
MarkdownPad Win下的工具比较少,这是个不错的。

目前笔者常用的是Atom,跨平台,可以很好在工作在macOS和Ubuntu上。另外因为是通用型的编辑器,且插件非富,不仅限于写MarkDown,其它工作也可以胜任。
上面提到的工具各有特色,可以都试用一下再选一款适合自已的。

使用七牛云保存图片

因为MarkDown是纯文本格式,所以决定了它不可能像Microsoft Word那样内嵌图片。
虽然在平时使用MarkDown做个人知识记录的过程中,是极少使用图片的。但如果要撰写给别人看的博客、教程一类的文档,有一些图片总是好的。
MarkDown可以使用本地图片,也可以使用URL远程图片。

不推荐使用本地图片,虽然图片保存在本地比扔在图床上更”安全”,丢失的风险更小,但本地散乱图片会增加我们文件夹的容量,增加文档管理的难度。而且包含本地图片的文章要发布到博客上时,通常需要一张张手工上传图片,费时费力。
所以选择一个靠谱的图床,将图片上传后,以URL的方式嵌入MarkDown是最佳选择。鉴于国内的网络环境,以及前一段时间各大云存储厂商的所做所为,国内靠谱的图床确实不多,这里只推荐 七牛云
七牛云的CEO是许式伟,曾是金山WPS2005的首席架构师,大神级的人物,所以七牛云的技术方面我们不用过多担心。重度用户可以适当付费支持一下,让七牛云良性的发展下去。
七牛云不仅仅是图片存储这么简单,具体的特性不一一介绍了,注册后可以慢慢在官网上看文档。

使用iPic快速上传图片

解决了图床的问题,再来看一下如何使用工具快速将图片上传,并获取生成的URL。
因为每插入一张图片,都要找开网站,点击上传按钮,找到存在本地磁盘上的图片,点击确定,成功后然后再把URL复制下来,实在是很繁琐。
这里推荐一个Mac下的小工具 iPic ,下面是一个演示动画:
https://ww2.sinaimg.cn/large/006tNc79gw1fah02zweq2g30j60as7wh.gif
除了拖拽,还有更快捷的操作方式,只要选中本地图片或者复制网页中的图片,然后按下Command+U快捷键就可自动上传并获取URL。

使用坚果云保存文档并同步

MarkDown文档写好了,保存在本地。一切看上去都很美好,但接下来,我们又有了更多的需求:

  1. 重要的文档,特别是我们辛辛苦苦多年积累下来的知识,只保存在本地是不安全的。不要把鸡蛋放在同一个篮子里,要把它备份到云端。
  2. 要实现多设备间的同步,这样在手机上、iPad上、家里的电脑上可以随时查阅、修改。

坚果云 是国内一款非常类似于DropBox的云存储应用,它提供了 全平台 的非常 快速稳定增量同步 功能。
坚果云可以说是国内硕果仅存的一家了,它的增量同步在国内做的是最好的,与百度云等不同,它专注于”同步”,而不是电影等大文件的存储。
坚果云的客户端做的也是非常用心,Linux下可以全功能完美使用。手机和iPad上的APP做的也不错。希望坚果云好好存活下去。
坚果云的使用非常简单,这里有一个视频教程:
视频教程链接

为什么不直接使用云笔记

目前市面上有非常多的跨平台的云笔记工具可以做到上面的事情,而且可能做的更好。 例如为知笔记、有道云笔记、EverNote、EssentialPIM等,另外还有一些后起之秀。
为什么还要使用 MarkDown + 坚果云 这样看上去略显繁琐的组合?

  • 我需要用纯文的方式来管理知识
    MarkDown相对来说比较”单纯”,而且又有表达”样式”的能力。举个例子,就像我们写代码,当然是要保存”源码”,而不是编译好的”二进制文件”。
    我们对纯文本有绝对的控制能力,纯文本方便检索,更重要的是方便日后的加工,可以导出PDF、HTML,可以分享发布到博客和专栏。
  • 我需要一个能随时替换掉的云服务
    云服务并不可靠,目前大部分国外的服务已经被封锁,国内的因为没有良好的盈利模式,也纷纷关闭,所以不要重度依赖云。既然MarkDown文档首先保存在本地,掌握在自已手中,这也就意味着我可以使用任何云同步服务,不仅仅是坚果云,还可以是OneDrive、DropBox、iCloud,甚至是自已在VPS上搭建的私有Git。
  • 我为什么抛弃了云笔记软件
    之前使用过的工具中,为知笔记是我最喜爱的(个人认为比EverNote做的要更好)。但它也是有一些缺点的,例如虽然支持MarkDown编写,但本质上还是会保存成HTML富文本。另外它的Mac版做的不如Win版优秀,Linux版也不友好。
    另外,使用第三方的工具,所有数据保存在云端,在本地没有原始档的备份,会让人有一种深深的”不安感”。再加上很多工具不提供”导出”功能,这意味着我们一旦使用某个工具,就要一直用下去,谁也无法保证5年后这个厂家还能不能存活下来,继续为我们提供服务。
  • 个人知识管理,不是简单的将文档编写并保存
    接下来,我们会对它进行搜索,加工,导出,分享,而这正是MarkDown的优势所在。

基于以上几点,我最终选择了使用 MarkDown + 坚果云 这样的组合进行个人知识管理,并准备长期使用下去。