普通视图

Received before yesterday

如此有才的秀恩爱

作者崔话记
2025年11月15日 15:50

  秀恩爱见的多,如此有才的秀恩爱却少见,看看我朋友夫妻俩在文学刊物上发文给对方致信及回信。

  彼岸和马哥是夫妻,一对十分有才情的夫妻,在我小小的朋友圈里算是才气值最高的了。忘记了是几个月以前,得知他俩作品发表到刊物了便向其索书,彼岸说等期刊出来之后寄我两本,我就等着了,期间还问过一两次书出来没,最近终于出来了,我如愿收到。她还收集了几片美丽的银杏叶赠我,刻意找纯黄无瑕疵的叶子,用精致的小相框夹好了。真是有心!

神泉&银杏

  马哥与我并未谋面,只见于彼岸的叙述里。马哥是一位职业画家,作画是个辛苦活,还经常要去外地好多天,我见过他的工笔花鸟画,虽然我不懂但也能感到十分惊艳。彼岸跟我是快二十年的老朋友了,当时在厦门认识的,也是我的湖北老乡,比我小好几岁。那时的她就已经展露她的才气了,圈子里颇有名气。我们有一些共同的朋友,她在家“大宴宾客”的时候,我也蹭过饭。这么些年了,还能保持联系,属实不容易的,除了珍视友谊,也还需要一些幸运的。

  收到书的第一天就仔细读了马哥的长文《写给妻子的一封信》,又搁了几天读了彼岸的回信《雨天回马先生家书》,还有马哥的几首小诗。十分赞叹他俩文笔,也十分钦佩他俩的十多年的感情,还有他俩对人生的态度。文字版的全文发在《房县文艺》公众号,本文末尾有链接。

  丈夫马哥《写给妻子的一封信》的开头和结尾两段拿出来感受一下。

当我就着窗外的月光将这些承载着心意的文字轻轻叠合,忽然发现每一次倾诉里,都藏着我们灵魂共振的频率。原来爱从不是单向的奔赴,而是两颗心在时光里互为镜像,温柔生长。
……
……
往后的日子,愿你、我,还有我们的小小马继续在时光里种满“一起”的故事,用理解作帆,信任为锚,让每一个现在都成为未来回忆里的糖。那时的我们或许不知道,这缕月光会一路照亮小院的蔷薇、房县的街巷、郊外的蓝天碧草山山水水,以及此刻“一起”生长的时光。你看,春天在发芽,夏天在开花,而我们,正在彼此的生命里,长成最美好的模样。

  妻子彼岸的回信《雨天回马先生家书》也摘录两段与诸君共赏。

你说“云与天空的诀别”,这般意象美得令人心头发紧——原来每一场雨都是云朵写给苍穹的绝笔诗啊。而人间所有未尽的言语,大抵也都化作氤氲水汽升腾至九霄,只待某日倾盆而下,淋湿某个猝不及防的归人。
……
……
“月有阴晴圆缺”恰是道不尽的东方智慧。你看此时窗外那被雨水洗过的月亮,残缺时清辉更甚圆满。若把人生看作长卷,彼此分隔两地的时光不过是留白处的题跋,而所有未完成的句点,终将在回忆里长出新的枝桠。恰如你笔下的云,纵使消散成雨,亦会以朝露的姿态重返花瓣,以薄雾的形貌再吻青山。

《写给妻子的一封信》1

《写给妻子的一封信》2

《雨天回马先生家书》

  十多年的夫妻,仍然有这样的感情浓度,甜不甜,醇不醇,羡慕不羡慕?最好的感情,不是520的红包,不是节日礼物的仪式感,不是18万的彩礼,而是真正感恩对方的付出,心里总是装着对方。他们生活在小县城,经济条件并不是十分优越,家里也无法助力,凭借两个人的努力工作和对生活的热爱,虽然辛苦,也过得幸福美满。

  多年夫妻,过得仍然相安无事的就算凤毛麟角了,多数是一地鸡毛,就如我自己也是痛苦拉扯分手收场的反面典型。上乘的婚姻不需要经营,因为双方契合有格局,各自按照自己的方式自然而然的生活,就能过得很幸福。中等的婚姻需要经营,需要至少一方有能力和智慧,克服人性的弱点,扬长避短获取生活的幸福,然而大多数人都是不具备这项能力的。

  婚姻这么重要而且需要艺术的事情,我们往往是未经任何学习和研究就茫然入场了。起初只看到了爱情的光芒,以为自己也能以爱情为矛,刺破所有挡路的盾,待到跌跌撞撞伤痕累累才知自己曾经多么傲慢无知。当然也有许多人并不知自我反思反而归咎于外,等闲变却故人心,却道故人心易变。也有许多人再婚的时候,并没有「吃一堑长一智」,反而更加傲慢无知。当两人有爱情的时候,算计得失尚且会伤感情,而爱情不足的两个人算计更多市侩得失,结果就可想而知了。

  应当相信爱情,同时也应当相信人性。认清生活的真相后,依然热爱生活。「取法乎上,仅得其中。取法乎中,仅得其下。取法乎下,无所得矣。」以十分的热情对待爱情和生活,未必能够收获十分的回报,但以五分的热情对待爱情和生活,也许就只能得到三分回报七分失望。

  有兴趣看上述两篇文章的完整文字版的,可以打开下面的链接。

《写给妻子的一封信》

《雨天回马先生家书》

来自故乡湖北的桔子

作者崔话记
2025年11月13日 22:26

  前些日,收到湖北老乡朋友寄来的一箱桔子,非常感激。写这篇文字除了表达一下感激之外,也是想借此机会抒发一点其它的感想。

  这桔子是生长于风景秀美的丹江口水库之滨,今年夏季湖北大旱之后又大涝,非正常的气候其实不太利于桔子成长,朋友说可能不那么好吃,不过我吃第一个的时候,感觉非常好吃,清甜多汁而且无籽无渣,后续偶有吃到微酸的但口感仍然不错。这些桔子是朋友自家地里种的,也是朋友自己采摘的,因为担心快递损伤还逐个挑选比较结实的果子,果然到深圳打开的时候一个都没有伤了或坏了的。这些桔子并不值许多钱,但这情谊无价,能吃到的人自然也是幸福感满满。

  这棵桔的冬天、秋天、果实。
桔

  风景秀美的丹江口水库。
丹江

  我想到,母亲数年前在家里也种了几棵橘子树,几棵树经历种种意外,今年还剩一棵活着,而且这一棵在今年被收割机在收割其它作物时打掉了半边,剩下的一半今年也还结了桔子。可惜我没有吃到过自己家种的桔子,因为这些年我都没有在这个季节回过老家,而让父母寄快递也是有很现实的困难不必多叙。前阵子和母亲通电话她还说起家里的橘子树和柚子树,而我只能凭空想象。什么时候退休啊,我要去吃自家的桔子。

  幼时家中菜园种过几棵橘子树,是姨妈家送的果树苗,然而种下不久就被邻里偷走移栽到他们家地里去了,但是我们家没亲眼看到没亲手抓到就没用,毕竟在村里的环境,武力值我们家比不过他们,只能认栽,这种事太常见了。我对家乡有各种惦记,但对这些邻里并没有太多好感,大概这也是重要原因之一吧。

  之前还收到过湖北的同学给我寄的伦晚脐橙,也很美味。湖北盛产柑橘,以前没什么了解,离开湖北这么多年,反而真切感受了。湖北不仅柑橘长得好,人也重情谊。

实现MinIO数据的每日备份

2025年10月20日 00:00

1.概述

MinIO是一个对象存储解决方案,常作为中间件用于后端系统保存和管理文件附件,附件和关系型数据库的库表数据一样是系统的核心用户数据,因此系统运行过程中,需要对附件数据进行每天备份。

在常年累月运行中,系统产生的附件量是巨大的,有时单独一个附件就很大,如果每天进行全量备份,那备份的文件就会像滚雪球一样越来越大,因此这里采用增量备份的形式,每天只备份当天的数据。

2.后端代码适配

首先,MinIO的文件层次就需要按天分开,在后端调用S3接口进行上传的代码进行控制

path = FileUtils.generatePath(content, name);int year = LocalDate.now().getYear();int month = LocalDate.now().getMonthValue();int day = LocalDate.now().getDayOfMonth();path = year+"/"+month+"/"+day+"/"+path;

这样,在前端调用上传接口上传附件后,返回的附件路径应该是这样的

{    "code": 0,    "data": "2025/10/20/62ca4c572522f9708199a4f96e0816f879669785347483232a8fcfd085267dc5.PNG",    "msg": "",    "total": null}

文件在MinIO中会按照年月日分级存储

3.备份Shell脚本

编写以下Shell脚本,调用MinIO客户端命令mc拷贝文件,并定时调用脚本实现每天进行备份

#!/bin/bash# MinIO 备份脚本 YEAR=$(date +%Y)MONTH=$(date +%m)DAY=$(date +%d)# 配置变量MINIO_ALIAS="myminio"BUCKET_NAME="u******ia"BACKUP_BASE_DIR="/opt/backup"LOG_DIR="/var/log/minio_backup"DATE_SUFFIX=$(date +%Y-%m-%d)-backBACKUP_PATH="${BACKUP_BASE_DIR}/${DATE_SUFFIX}"# 创建必要的目录mkdir -p "${BACKUP_PATH}"mkdir -p "${LOG_DIR}"# 日志文件LOG_FILE="${LOG_DIR}/backup_$(date +%Y%m%d).log"# 函数:记录日志log_message() {    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"}# 函数:错误处理error_exit() {    log_message "错误: $1"    exit 1}# 开始备份log_message "=== 开始 MinIO 备份 ==="log_message "备份源: ${MINIO_ALIAS}/${BUCKET_NAME}"log_message "备份目标: ${BACKUP_PATH}"# 检查 mc 命令是否存在if ! command -v /opt/mc &> /dev/null; then    error_exit "mc 命令未找到,请确保 MinIO Client 已安装"fi# 检查备份目录是否可写if [ ! -w "${BACKUP_BASE_DIR}" ]; then    error_exit "备份目录 ${BACKUP_BASE_DIR} 不可写"fi# 执行备份log_message "开始复制数据..."/opt/mc cp "${MINIO_ALIAS}/${BUCKET_NAME}/${YEAR}/${MONTH}/${DAY}" "${BACKUP_PATH}/" --recursive 2>&1 | tee -a "$LOG_FILE"# 检查备份结果if [ ${PIPESTATUS[0]} -eq 0 ]; then    log_message "备份成功完成"        # 显示备份统计信息    BACKUP_SIZE=$(du -sh "${BACKUP_PATH}" | cut -f1)    FILE_COUNT=$(find "${BACKUP_PATH}" -type f | wc -l)    log_message "备份大小: ${BACKUP_SIZE}"    log_message "文件数量: ${FILE_COUNT}"    log_message "备份位置: ${BACKUP_PATH}"else    error_exit "备份过程中出现错误"filog_message "=== 备份完成 ==="

使用Java实现一个DNS服务

2025年8月14日 00:00

有时,我们所在单位的电脑只允许上内网,外网被断掉了,如果想要同时上内外网,我们可以通过修改路由表,然后双网卡一机两网的方式来实现分流上网,例如网线连公司内网,用WiFi连接自己的手机热点,或者额外购买一个USB网卡插入电脑,同时连接公司的AP和自己手机热点。

但是这样会衍生出一个问题,有些公司的内部系统例如OA系统等,也是通过域名而不是难以记忆的IP地址来访问的,这些内部系统的域名不是注册商注册的,更不在公共DNS上,而是公司内网上使用的内网域名,使用公司自建的内网DNS服务器才能解析,解析出通常是一个本地局域网地址,在公网无法解析和访问,当接入公司内网,企业路由器会通过DHCP下发内网DNS给网卡,现在同时上内外网时,外网网卡也会获得运营商下发的外网DNS地址,操作系统会按照跃点数只选择某个网卡上获得的的DNS用作DNS解析,如果默认了内网网卡优先,且内网DNS只解析公司内网域名,同样会导致外网无法访问,如果内网DNS能解析外部域名,同样存在利用DNS屏蔽某些网站或服务(例如影视剧,游戏,向日葵远控等)甚至后台偷偷记录DNS解析记录的可能,因此为了保险起见,我们可以自己用代码实现一个DNS代理服务器来进行代理和分流,根据特定后缀等特征判断出内网域名,交给内网DNS解析,对于外网域名则直接选择一些公共DNS来解析(例如谷歌,阿里,114的DNS服务)

这里采用Java实现一个多线程的DNS代理服务器,对于内网域名直接通过内网DNS的UDP:53进行解析,对于外网域名则以加密的DOH(DNS Over Https)方式通过阿里云DNS进行解析,并解析DNS服务器返回的报文并打印日志。需要依赖dnsjava这个类库的支持,程序启动后,只需要将网卡DNS服务器地址和备用地址修改为127.0.0.1127.0.0.2即可实现DNS的分流。

<dependencies>    <!-- DNS 处理库 -->    <dependency>        <groupId>dnsjava</groupId>        <artifactId>dnsjava</artifactId>        <version>3.6.0</version>    </dependency>    <!-- HTTP 客户端(用于DoH请求) -->    <dependency>        <groupId>org.apache.httpcomponents.client5</groupId>        <artifactId>httpclient5</artifactId>        <version>5.3</version>    </dependency></dependencies>
package com.changelzj.dns;import org.apache.hc.core5.http.ContentType;import org.xbill.DNS.*;import org.apache.hc.client5.http.classic.methods.HttpPost;import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;import org.apache.hc.client5.http.impl.classic.HttpClients;import org.apache.hc.core5.http.io.entity.ByteArrayEntity;import java.io.ByteArrayInputStream;import java.io.DataInputStream;import java.io.IOException;import java.net.DatagramPacket;import java.net.DatagramSocket;import java.net.InetAddress;import java.nio.charset.StandardCharsets;import java.time.Duration;import java.time.Instant;import java.util.ArrayList;import java.util.Arrays;import java.util.List;import java.util.concurrent.*;public class LoggedDnsServer {    /**      * 需要内网DNS才能解析的内网域名    */     private static final String[] INTERNAL_DOMAINS = {"p****c.com", "s******c.com"};    /**     * 内网NDS服务器IP地址     */    private static final String INTERNAL_DNS = "10.249.35.11";    private static final String DOH_URL = "https://223.5.5.5/dns-query";    private static final ExecutorService executor = new ThreadPoolExecutor(            Runtime.getRuntime().availableProcessors() * 2,            Runtime.getRuntime().availableProcessors() * 2,            60L,            TimeUnit.SECONDS,            new LinkedBlockingQueue<>(200),            new ThreadPoolExecutor.CallerRunsPolicy()    );    public static void main(String[] args) throws IOException {        DatagramSocket socket = new DatagramSocket(53);        System.out.println("Multi-threaded DNS Server with Logging started on port 53");        byte[] buffer = new byte[512];        while (true) {            DatagramPacket requestPacket = new DatagramPacket(buffer, buffer.length);            socket.receive(requestPacket);            byte[] requestData = new byte[requestPacket.getLength()];            System.arraycopy(requestPacket.getData(), 0, requestData, 0, requestPacket.getLength());            executor.submit(() -> {                Instant start = Instant.now();                String domain = "";                String method = "";                boolean success = false;                String ip = "";                try {                    Message query = new Message(requestData);                    domain = query.getQuestion().getName().toString(true).toLowerCase();                    byte[] responseData;                    if (isInternalDomain(domain)) {                        method = "Internal DNS (" + INTERNAL_DNS + ")";                        responseData = forwardToUdpDns(query, INTERNAL_DNS);                    } else {                        method = "Ali DNS DoH (" + DOH_URL + ")";                        responseData = forwardToDoh(query);                    }                    success = true;                    ip = parseDnsResponse(responseData).toString();                     DatagramPacket responsePacket = new DatagramPacket(                            responseData,                            responseData.length,                            requestPacket.getAddress(),                            requestPacket.getPort()                    );                    socket.send(responsePacket);                } catch (Exception e) {                    System.err.println("[ERROR] " + e.getMessage());                } finally {                    long ms = Duration.between(start, Instant.now()).toMillis();                    System.out.printf(                            "[%s] %s -> %s | %s | %s | %dms | %s  %n",                            requestPacket.getAddress().getHostAddress(),                            domain,                            method,                            success ? "OK" : "FAIL",                            ip,                            ms,                            Thread.currentThread().getName()                    );                }            });        }    }    private static boolean isInternalDomain(String domain) {        for (String suffix : INTERNAL_DOMAINS) {            if (domain.endsWith(suffix)) {                return true;            }        }        return false;    }    private static byte[] forwardToUdpDns(Message query, String dnsServer) throws IOException {        SimpleResolver resolver = new SimpleResolver(dnsServer);        resolver.setTCP(false);        resolver.setTimeout(3);        Message response = resolver.send(query);        return response.toWire();    }    private static byte[] forwardToDoh(Message query) throws IOException {        try (CloseableHttpClient client = HttpClients.createDefault()) {            HttpPost post = new HttpPost(DOH_URL);            post.setHeader("Content-Type", "application/dns-message");            post.setEntity(new ByteArrayEntity(query.toWire(), ContentType.create("application/dns-message")));            return client.execute(post, httpResponse -> {                try (java.io.InputStream in = httpResponse.getEntity().getContent();                     java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream()) {                    byte[] buf = new byte[1024];                    int len;                    while ((len = in.read(buf)) != -1) {                        bos.write(buf, 0, len);                    }                    return bos.toByteArray();                }            });        }    }    public static List<String> parseDnsResponse(byte[] msg) throws Exception {        List<String> result = new ArrayList<>();        int pos = 0;        // 头部 12 字节        pos += 4; // ID + Flags        int qdCount = ((msg[pos] & 0xFF) << 8) | (msg[pos + 1] & 0xFF); pos += 2;        int anCount = ((msg[pos] & 0xFF) << 8) | (msg[pos + 1] & 0xFF); pos += 2;        int nsCount = ((msg[pos] & 0xFF) << 8) | (msg[pos + 1] & 0xFF); pos += 2;        int arCount = ((msg[pos] & 0xFF) << 8) | (msg[pos + 1] & 0xFF); pos += 2;        // 跳过 Question 区        for (int i = 0; i < qdCount; i++) {            // 读 QNAME(支持压缩指针)            pos = readName(msg, pos, null);            pos += 4; // QTYPE + QCLASS        }        int rrCount = anCount + nsCount + arCount;        for (int i = 0; i < rrCount; i++) {            pos = readName(msg, pos, null);            int type = ((msg[pos] & 0xFF) << 8) | (msg[pos + 1] & 0xFF); pos += 2;            pos += 2; // CLASS            pos += 4; // TTL            int rdlen = ((msg[pos] & 0xFF) << 8) | (msg[pos + 1] & 0xFF); pos += 2;            if (type == 1 && rdlen == 4) { // A                byte[] addr = Arrays.copyOfRange(msg, pos, pos + 4);                result.add(InetAddress.getByAddress(addr).getHostAddress());            } else if (type == 28 && rdlen == 16) { // AAAA                byte[] addr = Arrays.copyOfRange(msg, pos, pos + 16);                result.add(InetAddress.getByAddress(addr).getHostAddress());            }            pos += rdlen;        }        return result;    }    // 工具:读取域名(含压缩指针),返回新的 pos    private static int readName(byte[] msg, int pos, StringBuilder out) {        int jumpedPos = -1;        while (true) {            int len = msg[pos] & 0xFF;            if ((len & 0xC0) == 0xC0) { // 压缩                int ptr = ((len & 0x3F) << 8) | (msg[pos + 1] & 0xFF);                if (jumpedPos == -1) jumpedPos = pos + 2;                pos = ptr;                continue;            }            pos++;            if (len == 0) break;            if (out != null) {                if (out.length() > 0) out.append('.');                out.append(new String(msg, pos, len, StandardCharsets.ISO_8859_1));            }            pos += len;        }        return jumpedPos != -1 ? jumpedPos : pos;    }}

简单理解AI智能体

2025年6月13日 00:00

一、智能体是什么

文章的开头,先来举一个身边最简单的例子,比如字节推出的云雀是大模型,而豆包和Coze就是智能体,豆包是一个实现了对话功能的智能体,而Coze是一个可以实现工作流编排的智能体。

1986年,智能体(AIAgent、人工智能代理)的概念最早由被誉为“AI之父”的马文·明斯基(Marvin Minsky)在《意识社会》(The society of Mind)中提出。

明斯基定义的智能体的核心要素:

  • 要素1:分布式智能体集合
  • 要素2:层级协作机制
  • 要素3:无中央控制

但是,明斯基对智能体的定义和现代的智能体定义有很大区别,直到2023年6月,OpenAl的元老翁丽莲在个人博客(https://lilianweng.github.io/posts/2023-06-23-agent/)中首次提出了现代AI Agent架构:智能体(AI Agent)是一种能够自主行动、感知环境、 做出决策并与环境交互的计算机系统或实体,通常依赖大型语言模型作为其核心决策和处理单元,具备独立思考、调用工具去逐步完成给定目标的能力。

二、智能体的核心要素

智能体有以下核心要素:

  • 核心要素1: 大模型(LLM)

    大模型作为“大脑”: 提供推理、规划和知识理解能力,是AIAgent的决策中枢。

  • 核心要素2: 记忆(Memory)

    • 长期记忆: 可以横跨多个任务或时间周期,可存储并调用核心知识,非即时任务。可以通过模型参数微调(固化知识),知识图谱(结构化语义网络)或向量数据库(相似性检索)方式实现。

    • 短期记忆:存储单次对话周期的上下文信息,属于临时信息存储机制。受限于模型的上下文窗口长度。

  • 核心要素3: 工具使用(Tool Use)

    调用外部工具(如API、数据库)扩展能力边界。

  • 核心要素4: 规划决策(Planning)

    通过任务分解、反思与自省框架实现复杂任务处理。例如,利用思维链(chain of Thought)将目标拆解为子任务,并通过反馈优化策略。

  • 核心要素5: 行动(Action)

    实际执行决策的模块,涵盖软件接口操作(如自动订票)和物理交互(如机器人执行搬运)。比如:检索、推理、编程等。

三、智能体的运用

智能体在PC,手机以及自动驾驶等方面都有广泛的应用。在单一智能体的基础上,多个智能体之间可以交互写作。

参考

  1. 0代码0基础,小白搭建智能体&知识库,尚硅谷,2025-03-17

大模型和大模型应用

2025年6月13日 00:00

本文更新中

1.AI与大模型

AI,即人工智能(Artificial Intelligence),使机器能够像人类一样思考、学习和解决问题的技术

AI发展主要经历了三个阶段:

  1. 1950-1980,规则和符号AI的时代,基于逻辑和规则,使用符号表示知识和推理。依赖预定义的知识库和推理规则,应用于化学结构分析以及医学诊断
  2. 1980-2010,机器学习,基于数据,通过统计和优化方法训练模型,包括监督学习无监督学习和强化学习等子领域,应用于游戏,推荐引擎
  3. 2010-今,深度学习,模仿人脑的结构和功能,使用多层神经元网络处理复杂任务,例如卷积神经网络,应用于图像识别,自然语言处理

大模型中最常见的大语言模型(Large Language Models,LLM),就是采用了深度学习中的自然语言处理这一分支,在自然语言处理(Natural Language Processing,NLP)中,有一项关键技术叫Transformer,这是一种先进的神经网络模型,是现如今AI高速发展的最主要原因,我们所熟知的大语言模型,例如GPT、Deepseek底层都是采用Transformer神经网络模型

2.大模型应用的架构和技术方案

大模型应用,就是基于大模型的推理、分析、生成能力,结合传统编程能力,开发出的各种应用。

大模型对比传统应用,更加适合处理复杂模式和模糊问题,例如写诗,写文章,判断动物物种,音视频识别等,而传统应用更加擅长精确控制和需要高可靠性的场景,所以可以将传统应用和大模型相结合,两者就可以实现互相调用和增强

例如我们可以在数据库缓存和大模型的对话内容,每次调用大模型时一并发送,使大模型形成记忆

在架构上,大模型应用架构大致分为交互层,服务层,模型层和存储层:

按照技术方案划分,大模型应用可大致分为:

  • Prompt问答 利用大模型的推理能力,通过Prompt提问来完成业务,应用于文字摘要分析,舆情分析,AI对话等场景

  • Agent + Function calling(智能体 AI拆解任务,通过将AI能力和业务端的能力相结合,通过调用业务端提供的接口实现复杂业务,大模型可以适时调用业务端提供的函数来获取信息来进一步做判断,可以应用于数据提取和聚合分析等,例如要用大模型来进行行程规划同时提供一个天气的function给大模型,来为大模型做行程规划提供天气信息。

  • RAG(Retrieval Augmented Generation) 给大模型外挂一个知识库,让大模型基于知识库内容做推理和回答,因为大模型的训练语料可能与当前时间相比是落后的,且很多专业领域的知识并不公开,无法被用于训练,对大模型外挂一个私有的知识库可以弥补这种缺陷,这种模式下,首先要将文档切分写入知识库,当用户提问时,首先到知识库中加载获取有关的片段,然后和用户的提问包装成Prompt一块发送给大模型,由大模型来进行后续的回答

  • Fine-tuning(模型微调) 针对特有业务场景对基础大模型做数据训练和微调,以满足特定场景的需求,需要完全部署模型,难度和门槛较高

参考

  1. https://www.bilibili.com/video/BV1MtZnYtEB3

一个解析Excel2007的POI工具类

2025年5月19日 00:00

通过apache-poi解析读取excel2007表格中的文字和图片,数字按照字符形式读取,表格中的图片和文字都按照行和列顺序读取到二维数组中相应的位置上。

package com.util;import org.apache.poi.hssf.usermodel.HSSFSheet;import org.apache.poi.hssf.usermodel.HSSFWorkbook;import org.apache.poi.ooxml.POIXMLDocumentPart;import org.apache.poi.ss.usermodel.*;import org.apache.poi.xssf.usermodel.*;import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTMarker;import java.io.ByteArrayInputStream;import java.io.IOException;import java.io.InputStream;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;public class POIUtil {    /**     * 读入excel2007文件     *     * @param file     * @throws IOException     */    public static List<String[]> readExcel(String fileName, byte[] bytes, int sheetNum) throws IOException {        // 获取excel文件的io流        InputStream is = new ByteArrayInputStream(bytes);        // 根据文件后缀名不同(xls和xlsx)获得不同的Workbook实现类对象        Workbook workbook =  new XSSFWorkbook(is);;        // 创建返回对象,把每行中的值作为一个数组,所有行作为一个集合返回        List<String[]> list = new ArrayList<String[]>();        if (workbook != null) {            // for (int sheetNum = 0; sheetNum < workbook.getNumberOfSheets(); sheetNum++) {            // 获得当前sheet工作表            Sheet sheet = workbook.getSheetAt(sheetNum);            // if (sheet == null) {            // continue;            // }            // 获得当前sheet的开始行            int firstRowNum = sheet.getFirstRowNum();            // 获得当前sheet的结束行            int lastRowNum = sheet.getLastRowNum();            // 循环除了第一行的所有行            for (int rowNum = firstRowNum + 0; rowNum <= lastRowNum; rowNum++) {                // 获得当前行                Row row = sheet.getRow(rowNum);                if (row == null || row.getPhysicalNumberOfCells()==0) {                    continue;                }                // 获得当前行的开始列                int firstCellNum = row.getFirstCellNum();                // 获得当前行的列数                int lastCellNum = row.getPhysicalNumberOfCells();                String[] cells = new String[row.getPhysicalNumberOfCells()];                // 循环当前行                for (int cellNum = firstCellNum; cellNum < lastCellNum; cellNum++) {                    Cell cell = row.getCell(cellNum);                    cells[cellNum] = getCellValue(cell);                }                list.add(cells);            }            // }            workbook.close();        }        return list;    }    private static String getCellValue(Cell cell) {        String cellValue = "";        if (cell == null) {            return cellValue;        }        // 把数字当成String来读,避免出现1读成1.0的情况        if (cell.getCellType() == CellType.NUMERIC) {            cell.setCellType(CellType.STRING);        }        // 判断数据的类型        switch (cell.getCellType()) {            case NUMERIC: // 数字                cellValue = String.valueOf(cell.getNumericCellValue());                break;            case STRING: // 字符串                cellValue = String.valueOf(cell.getStringCellValue());                break;            case BOOLEAN: // Boolean                cellValue = String.valueOf(cell.getBooleanCellValue());                break;            case FORMULA: // 公式                cellValue = String.valueOf(cell.getCellFormula());                break;            case BLANK: // 空值                cellValue = "";                break;            case ERROR: // 故障                cellValue = "非法字符";                break;            default:                cellValue = "未知类型";                break;        }        return cellValue;    }    public static Map<String, byte[]> getExcelPictures(String fileName, byte[] bytes, int sheetNum) throws IOException {        Map<String, byte[]> map = new HashMap<String, byte[]>();        // 获取excel文件的io流        InputStream is = new ByteArrayInputStream(bytes);        // 获得Workbook工作薄对象        Workbook workbook =  new XSSFWorkbook(is);;        XSSFSheet sheet = (XSSFSheet) workbook.getSheetAt(sheetNum);        List<POIXMLDocumentPart> list = sheet.getRelations();        for (POIXMLDocumentPart part : list) {            if (part instanceof XSSFDrawing) {                XSSFDrawing drawing = (XSSFDrawing) part;                List<XSSFShape> shapes = drawing.getShapes();                for (XSSFShape shape : shapes) {                    XSSFPicture picture = (XSSFPicture) shape;                    XSSFClientAnchor anchor = picture.getPreferredSize();                    CTMarker marker = anchor.getFrom();                    String key = marker.getRow() + "-" + marker.getCol();                    byte[] data = picture.getPictureData().getData();                    map.put(key, data);                }            }        }        return map;    }}

Java实现将数据导出为Word文档

2025年1月10日 00:00

我们在开发一些系统的时候,例如OA系统,经常能遇到将审批单数据导出为word和excel文档的需求,导出为excel是比较简单的,因为excel有单元格来供我们定位数据位置,但是word文档的格式不像表格那样可以轻松的定位,要想将数据导出为一些带有图片和表格的这种结构复杂的word文档该怎样实现呢。

poi-tl [1]是一款可以帮助我们实现这种功能的Java开源项目,它把POI和Freemarker相结合,可以基于我们绘制好的word文档模板来填充数据进去,然后生成新的word文档。poi-tl托管在GitHub:https://github.com/Sayi/poi-tl

例如,我们要生成一个差旅行程单,首先要绘制这样的一个word文档模板,用{{name}}代表姓名进行占位,姓名就是普通文字类型,以此类推。tripList作为渲染行程表格的数据源的名字,是ArrayList集合类型,放在表格的表头,用[from]表示tripList集合中每个元素的from属性的值,渲染到当前行的某一列上,以此类推。最后的三个签署对应的是领导的签名笔迹图片,图片类型要用变量名前多一个@的形式{{@****Pin}}来表示

如果表格中某一列是图片,则表示为[@变量]

模板绘制好以后,开始使用poi-tl工具生成word文档,首先新建maven项目,引入poi-tl的依赖和需要的其他依赖,然后将这个绘制好的word模板文件放在工程的根目录

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>    <groupId>org.example</groupId>    <artifactId>poi-tl</artifactId>    <version>1.0-SNAPSHOT</version>    <properties>        <maven.compiler.source>8</maven.compiler.source>        <maven.compiler.target>8</maven.compiler.target>        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>    </properties>    <dependencies>        <dependency>            <groupId>com.deepoove</groupId>            <artifactId>poi-tl</artifactId>            <version>1.12.2</version>        </dependency>        <dependency>            <groupId>org.projectlombok</groupId>            <artifactId>lombok</artifactId>            <version>1.18.24</version>            <scope>provided</scope>        </dependency>    </dependencies></project>

然后,新建一个Entity类: org.example.TravelApplyExportVO

package org.example;import com.deepoove.poi.data.PictureRenderData;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import java.util.ArrayList;import java.util.List;@Data@AllArgsConstructor@NoArgsConstructorpublic class TravelApplyExportVO {    private String no;    private String name;    private String dept;    private String employeeNo;    private String start;    private String end;    private String days;    private String address;    private String reason;    /**     * com.deepoove.poi.data.PictureRenderData 代表图片     */    private PictureRenderData applyPin;    private PictureRenderData bossPin;    private PictureRenderData leaderPin;    private String date;    /**     * 用于渲染表格的集合     */    private List<Route> tripList = new ArrayList<>();    @Data    @AllArgsConstructor    @NoArgsConstructor    public static class Route {        private String from;        private String to;        private String flight;        private String depTime;        private String arrTime;        private String cabin;    }}

新建测试类: org.example.Main,用poi-tl组件基于刚刚绘制的word模板生成一个差旅行程单

package org.example;import com.deepoove.poi.XWPFTemplate;import com.deepoove.poi.config.Configure;import com.deepoove.poi.data.PictureRenderData;import com.deepoove.poi.data.Pictures;import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;import lombok.SneakyThrows;import java.nio.file.Files;import java.nio.file.Paths;import java.util.ArrayList;public class Main {    @SneakyThrows    public static void main(String[] args) {        TravelApplyExportVO vo = new TravelApplyExportVO();        vo.setNo("202500001");        vo.setName("lzj");        vo.setDept("技术部");        vo.setEmployeeNo("00000001");        vo.setStart("2025-01-01");        vo.setEnd("2025-02-01");        vo.setDays("30");        vo.setAddress("中国香港");        vo.setReason("系统维护");        // 在项目根路径读取笔迹图片,并设置大小        PictureRenderData data1 = Pictures.ofBytes(Files.readAllBytes(Paths.get("img2.png")))                .size(120, 60)                .create();        PictureRenderData data2 = Pictures.ofBytes(Files.readAllBytes(Paths.get("img.png")))                .size(120, 60)                .create();        PictureRenderData data3 = Pictures.ofBytes(Files.readAllBytes(Paths.get("img.png")))                .size(120, 60)                .create();        vo.setApplyPin(data1 );        vo.setBossPin( data2);        vo.setLeaderPin( data3);        vo.setDate("2025-01-10");        // 行程List,最终渲染到文档的表格中        vo.setTripList(new ArrayList<TravelApplyExportVO.Route>() {            {                add(new TravelApplyExportVO.Route("BJX","ZQZ","ZH5643","2025-01-01 15:00","2025-01-01 16:00","E"));                add(new TravelApplyExportVO.Route("ZQZ","CDE","JUH6532","2026-01-01 15:00","2025-12-01 16:00","A"));                add(new TravelApplyExportVO.Route("BJX","ZQZ","KJU0954","2027-01-01 15:00","2025-05-01 16:00","Q"));            }        });        LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();        // !!将tripList通过表格来渲染        Configure config = Configure.builder()                .bind("tripList", policy)                .build();        XWPFTemplate template = XWPFTemplate                .compile("模板.docx", config)                .render(vo);        template.writeAndClose(Files.newOutputStream(Paths.get("output.docx")));    }}

然后领导签名笔记图片素材img.png,img2.png也需要放进工程根目录下

都完成后,执行main()方法测试,程序运行结束后,将在根路径生成文件output.docx,打开就是我们想要的效果了。

参考

OA系统的天数该怎样计算

2024年12月31日 00:00

在开发一些OA系统的过程中,经常能遇到一个问题,就是时长计算,比如请假有请假的时长,出差有出差的时长,有的公司请假只能按照整天或小时为单位请假,这种都比较好处理,只要排除休息日节假日天数或排除下班非工作小时数直接相减即可,但是如果需求是按照半天为单位,OA系统该怎样计算总时长呢?

以半天为最小单位时,机械的加减有时可能无法和实际情况相符,例如我在OA系统提交休假审批,从1号上午到2号上午为假期,如果直接假定开始的小时都一样直接把日期时间简单相减,那么休假时间就是24小时构成的1天,但是实际上假期的构成是1号上午,1号下午,以及2号上午,按照工时的普遍计算逻辑就是1.5个工作日,OA系统应计算实际休假时长是1.5天,那这种场景下OA系统要怎样计算才准确呢,我把情况分为4种分别处理,分别计算休假一天,两天和三天的情况,进而推导到更长时间

1. 上午开始,上午结束

1天内的情况是0.5天,2天内的是1.5天,3天内是2.5天

2. 上午开始,下午结束

1天内的情况是1天,2天内的是2天,3天内是3天

3. 下午开始,下午结束

1天内的情况是0.5天,2天内的是1.5天,3天内是2.5天

4. 下午开始,上午结束

1天内不会有这种情况,2天内的是1天,3天内是2天

综上,计算的代码就是这样的:

public static void main(String[] args) {    System.out.println(test(            LocalDate.parse("2024-12-01"),            LocalDate.parse("2024-12-03"),            1,            1    ));}/** *  * @param start 开始日期 * @param end 结束日期 * @param startPeriod 开始 1上午2下午 * @param endPeriod 结束 1上午 2下午 * @return */public static double test(LocalDate start, LocalDate end, int startPeriod, int endPeriod) {    // 如果开始时间或结束时间为空,则返回 0 天    if (start == null || end == null) {        return 0.0; // 确保返回值类型一致    }    // 计算两个日期之间的整天数    double between = (double) java.time.temporal.ChronoUnit.DAYS.between(start, end);    //天数相减会把涉及的天数算少一天,所以要加回来    between ++;    // 根据时间段调整天数    if (startPeriod == 1) { // 开始是上午        if (endPeriod == 1) { // 结束是上午            between =  between - 0.5 ;        }    }    else if (startPeriod == 2) { // 开始是下午        if (endPeriod == 1) { // 结束是上午            between = between - 1;        }        else if (endPeriod == 2) { // 结束是下午            between = between - 0.5 ;        }    }    // 返回计算后的天数    return between;}

一些场景下,为了小数计算更加精确,可以使用java.math.BigDecimal进行计算

IPv4和IPv6

2024年11月23日 00:00

原文地址:https://www.rockylinux.cn/notes/rocky-linux-9-network-configuration.html
原文作者:木子
Rocky Linux 中文社区欢迎您 https://www.rockylinux.cn

IPv4 与 IPv6

在进行 IP 配置之前,我们延伸了解一下 IPv4 与 IPv6 。 IPv4(Internet Protocol version 4)和 IPv6(Internet Protocol version 6)是互联网上用于数据包交换的两个版本的网络层协议。它们是互联网协议套件的核心部分,负责在网络设备之间路由和传递数据。

IPv4

IPv4 是第四版互联网协议,自 1981 年以来一直被广泛使用。IPv4 的特点包括: 地址空间: IPv4 使用 32 位地址,这意味着它可以支持大约 42 亿个独特的 IP 地址。 地址表示: IPv4 地址通常以点分十进制格式表示,例如 192.168.1.1。 地址配置: IPv4 地址可以手动配置(静态)或通过动态主机配置协议(DHCP)自动分配。 分片: IPv4 允许在传输过程中对数据包进行分片,这可以由发送端、接收端或中间路由器处理。 由于互联网的快速增长,IPv4 地址已经耗尽,这促使了对更广泛地址空间协议的需求。 在 IPv4 地址空间中,地址分为公网 IP、私有 IP 和 CGN(Carrier Grade NAT)地址。以下是详细区分:

公网 IP 地址

公网 IP 地址是全球唯一的,可以在整个互联网中进行通信的 IP 地址。它们不属于下列提到的私有 IP 和 CGN 地址的范围。所以,除了以下私有 IP、CGN 地址以及保留地址和特殊用途地址(如多播地址、环回地址等),其他的都属于公网 IP。

私有 IP 地址

私有 IP 地址用于局域网(LAN)内部通信,是不会在互联网中进行路由的。这些地址范围由 IANA(Internet Assigned Numbers Authority)分配:

  1. 10.0.0.0 到 10.255.255.255
  2. 172.16.0.0 到 172.31.255.255
  3. 192.168.0.0 到 192.168.255.255

CGN (Carrier Grade NAT) 地址

CGN 地址也称为共享地址空间,用于 ISP 提供的 NAT 方案,以减少 IPv4 地址的消耗。以下是该范围:

  1. 100.64.0.0 到 100.127.255.255

这些地址也不会在全球互联网中进行路由,用于解决多个用户共享一个公共 IP 地址的需求(Tailscale 用的这个地址段)。

其他特殊地址

还有一些保留和特殊用途的地址,例如:

  • 环回地址: 127.0.0.1
  • 广播地址: 255.255.255.255
  • 多播地址: 224.0.0.0 到 239.255.255.255

IPv6

IPv6 是互联网协议的最新版本,旨在解决 IPv4 地址耗尽的问题,并引入了一些新的特性和改进。IPv6 的特点包括:

  • 地址空间: IPv6 使用 128 位地址,极大地扩展了地址空间,可以支持近乎无限数量的独特 IP 地址。
  • 地址表示: IPv6 地址通常以冒号分隔的十六进制格式表示,例如 2001:0db8:85a3:0000:0000:8a 2 e:0370:7334。
  • 地址配置: IPv6 地址可以通过多种方式配置,包括静态配置、状态无关地址自动配置(SLAAC)和动态主机配置协议版本 6(DHCPv 6)。
  • 无分片: IPv6 设计时取消了路由器的分片功能,要求发送端执行路径最大传输单元(PMTU)发现,并发送适合路径上最小链路 MTU 的数据包。 在 IPv6 中,没有对应 IPv4 的私有 IP 和公网 IP 的概念,但有类似的机制来实现内网和公网的区别与应用。以下是一些重要的 IPv6 地址类型和其用途:

全球单播地址(Global Unicast Address)

全球单播地址就是 IPv6中用于在全球范围内进行通信的唯一地址,类似于 IPv4的公网 IP。其地址范围一般是以 2000::/3 开头。

唯一本地地址(Unique Local Address, ULA)

唯一本地地址在某种程度上类似于 IPv4的私有 IP 地址,用于局域网通信,不会在全球互联网中进行路由。其地址范围是 FC00::/7,也可以细分为以下两个范围:

  • 随机分配的 ULA: FD00::/8,通用情况下会使用这个范围,通过随机生成的方式保证在局部网络内的唯一性。
  • 原始分配的 ULA: FC00::/8,目前未正式广泛使用。

链路本地地址(Link-Local Address)

这些地址只能用于单个网络链路的节点之间,不能路由到其他链路。所有 IPv6 接口在启动时都会自动生成一个链路本地地址以支持邻居发现协议。其地址范围是 FE80::/10

不管是 Linux、macOS 还是 Windows 都会分配一个 Link-Local Address,以inet6 FE80::开头。

# Linux[root@localhost ~]# ifconfigens18: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500        inet 192.168.1.3  netmask 255.255.255.0  broadcast 192.168.1.255        inet6 fe80::486a:e224:31e4:d1fc  prefixlen 64  scopeid 0x20<link>        ether 52:ea:eb:77:3d:fe  txqueuelen 1000  (Ethernet)        RX packets 112410841  bytes 40294807433 (37.5 GiB)        RX errors 0  dropped 0  overruns 0  frame 0        TX packets 64910  bytes 24656852 (23.5 MiB)        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0 # macOS❯ ifconfigen0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500    options=400<CHANNEL_IO>    ether f8:28:19:6a:2b:0f    inet6 fe80::18cd:9189:ab4:ef40%en0 prefixlen 64 secured scopeid 0x6    inet 192.168.1.2 netmask 0xffffff00 broadcast 192.168.1.255    nd6 options=201<PERFORMNUD,DAD>    media: autoselect    status: active # WindowsPS C:\Users\muzi> ipconfigWindows IP 配置以太网适配器 以太网:    连接特定的 DNS 后缀 . . . . . . . :   本地链接 IPv6 地址. . . . . . . . : fe80::530b:7d8a:998a:f3f5%16   IPv4 地址 . . . . . . . . . . . . : 192.168.1.1   子网掩码  . . . . . . . . . . . . : 255.255.255.0   默认网关. . . . . . . . . . . . . : 192.168.1.254

其他类型地址

还有一些其他特殊用途的地址,比如:

  • 多播地址: FF00::/8,用于多播通信。
  • 组播地址: FF00::/8,用于组播通信。

IPv4 与 IPv6 之间的主要区别

  • 地址长度: IPv4 是 32 位,IPv6 是 128 位。
  • 地址表示法: IPv4 使用点分十进制,而 IPv6 使用冒号分隔的十六进制。
  • 地址空间: IPv6 提供了比 IPv4 更广阔的地址空间。
  • NAT 转换: 消除 NAT 以将地址空间从 32 位扩展到 128 位。
  • IPSec 支持: 在 IPv6 中,IPSec 是核心特性的一部分,但同样也需要进行配置,比如采用 strongswan 等。
  • 数据包处理: IPv6 简化了数据包头部,以提高路由效率,并取消了路由器分片功能。
  • 自动配置: IPv6 支持更高级的自动配置能力。
  • 多播和广播: IPv6 支持多播,但不支持 IPv4 那样的网络广播。取而代之,IPv6 使用多播和邻居发现协议来实现网络上的设备发现和配置。

IPv4 与 IPv6 这些区别反映了互联网协议在安全性、效率、可扩展性方面的进步,同时也提出了新的挑战,例如迁移和兼容性问题。随着 IPv6 逐渐被广泛采用,这些挑战将得到解决。

以下两图为 IPv4 与 IPv6 报文头对比:


使用GraalVM原生编译打包SpringBoot工程

2024年11月10日 00:00

1.GraalVM

GraalVM (https://www.graalvm.org/) 是一个高性能的JDK,旨在加速用Java和其他JVM语言编写的应用程序的执行,同时还提供JavaScript,python和许多其他流行语言的运行时。

GraalVM提供了两种运行Java应用程序的方式:

  1. 在hotspot jvm上使用graal即时编译器(JIT)
  2. 作为预先编译的本机可执行文件运行

GraalVM的多语言能力使得在单个应用程序中混合多种编程语言成为可能,同时消除了外部语言的调用成本。

2.Windows平台安装和配置GraalVM

Windows平台环境安装和配置比较复杂

2.1 安装Visual Studio

Visual Studio(特别是其中的 Visual C++ 工具)在 Windows 系统上提供了 C/C++ 编译器和相关工具链,它们是用来编译和链接本地代码的。当使用 GraalVM 在 Windows 上构建本地可执行文件时,需要依赖 Visual Studio 提供的这些编译工具来完成编译和链接的过程。

安装界面,勾选使用C++的桌面开发

等待安装完成后,测试x64 Native Tools Command Prompt for VS 2022

以管理员身份测试打开x64 Native Tools Command Prompt for VS 2022

2.2 Windows安装GraalVM并配置

打开社区版GraalVM下载的GitHub页面
https://github.com/graalvm/graalvm-ce-builds/releases

以22.3.3版Java17为例,找到22.3.3版本,分别下载Windows平台的GraalVM graalvm-ce-java17-windows-amd64-22.3.3.zip和调用底层工具的原生镜像支持工具native-image-installable-svm-java17-windows-amd64-22.3.3.jar

下载地址:

  • graalvm-ce-java17-windows-amd64-22.3.3.zip

https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-22.3.3/graalvm-ce-java17-windows-amd64-22.3.3.zip

  • native-image-installable-svm-java17-windows-amd64-22.3.3.jar

https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-22.3.3/native-image-installable-svm-java17-windows-amd64-22.3.3.jar

设置环境变量

将GraalVM压缩包解压到F:\graalvm-ce-java17-22.3.3,然后设置环境变量JAVA_HOMEF:\graalvm-ce-java17-22.3.3,设置环境变量PATH新增%JAVA_HOME%/bin

检查环境变量生效

java -version

安装native-image

在jar包目录打开cmd窗口,输入命令,完成安装

gu install --file ./native-image-installable-svm-java17-windows-amd64-22.3.3.jar

输入命令,测试是否安装成功

native-image

至此,Windows平台GraalVM和支持将应用打包成本地镜像的工具安装配置完成。

2.3 新建简单项目测试编译打包

打开IDEA,新建项目springboot3,JDK选择安装的graalvm-ce-java17,然后先编写一段非常简单的Java代码,然后将其编译为原生.exe可执行文件

public class Main {    public static void main(String[] args) {        System.out.printf("hello graalvm!");    }}

用Maven将项目打包为jar包

打开x64 Native Tools Command Prompt for VS 2022工具,使用底层能力运行native-image工具,将class编译为.exe。

以管理员身份打开x64 Native Tools Command Prompt for VS 2022,并CD切换到jar包所在target目录,然后执行命令

-cp 编译
-o 目标.exe文件名称

native-image -cp ./springboot3-1.0-SNAPSHOT.jar org.example.Main -o springboot3

编译完成,生成Windows平台的.exe可执行文件

打开CMD,执行exe文件,输出代码运行结果,和Java虚拟机解释执行结果相同

2.4 新建SpringBoot3项目测试编译打包

首先要在Windows下配置三个环境变量,每个人环境和位置不同,需要自己按照自己的实际情况新增或修改。

1.path

path环境变量新增以下路径

F:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.41.34120\bin\Hostx64\x64

2.lib

新建lib环境变量,并将以下路径配置进去,之间用英文分号;间隔,以下位置目录均为VS创建

F:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.41.34120\lib\x64F:\Windows Kits\10\Lib\10.0.22621.0\ucrt\x64F:\Windows Kits\10\Lib\10.0.22621.0\um\x64

3.INCLUDE(大写)

新建INCLUDE环境变量,并将以下路径配置进去,之间用英文分号;间隔,以下位置目录均为VS创建

F:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.41.34120\includeF:\Windows Kits\10\Include\10.0.22621.0\sharedF:\Windows Kits\10\Include\10.0.22621.0\ucrtF:\Windows Kits\10\Include\10.0.22621.0\umF:\Windows Kits\10\Include\10.0.22621.0\winrt

! ! !环境变量配置好后,一定要重新打开CMD窗口编译打包,使用Intellij IDEA的,一定也要重启IDEA。

接下来,编写一个简单的SpringBoot应用,并编译打包为原生.exe文件

pom.xml

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>    <parent>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-parent</artifactId>        <version>3.0.6</version>    </parent>    <groupId>org.example</groupId>    <artifactId>springboot3</artifactId>    <version>1.0-SNAPSHOT</version>    <properties>        <maven.compiler.source>17</maven.compiler.source>        <maven.compiler.target>17</maven.compiler.target>        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>    </properties>    <dependencies>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>    </dependencies>    <build>        <plugins>            <plugin>                <groupId>org.graalvm.buildtools</groupId>                <artifactId>native-maven-plugin</artifactId>            </plugin>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>            </plugin>        </plugins>    </build></project>

org.example.controller.TestController

package org.example.controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("test")public class TestController {    @GetMapping("hello")    public String hello(String s) {        return "hello " + s;    }}

org.example.Main

package org.example;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class Main {    public static void main(String[] args) {        SpringApplication.run(Main.class);    }}

application.yml

server:  port: 8081

接下来先用普通的编译命令将代码编译为.class文件。

然后,执行spring-bootprocess-aot插件,进行编译前的前置处理。

前置处理完成后,target目录下生成了一些AOT编译相关的文件,用于指定主类的位置等

最后,运行native插件的build启动native-image,调用VS工具链将工程编译为二进制.exe文件

开始编译后等待编译完成


我的IDEA输出:

[INFO] Scanning for projects...[INFO] [INFO] ----------------------< org.example:springboot3 >-----------------------[INFO] Building springboot3 1.0-SNAPSHOT[INFO] --------------------------------[ jar ]---------------------------------[INFO] [INFO] --- native-maven-plugin:0.9.21:build (default-cli) @ springboot3 ---[WARNING] 'native:build' goal is deprecated. Use 'native:compile-no-fork' instead.[INFO] Found GraalVM installation from JAVA_HOME variable.[INFO] Executing: F:\graalvm-ce-java17-22.3.3\bin\native-image.cmd @target\tmp\native-image-3043011469918092793.args========================================================================================================================GraalVM Native Image: Generating 'springboot3' (executable)...========================================================================================================================[1/7] Initializing...                                                                                   (21.0s @ 0.17GB) Version info: 'GraalVM 22.3.3 Java 17 CE' Java version info: '17.0.8+7-jvmci-22.3-b22' C compiler: cl.exe (microsoft, x64, 19.41.34123) Garbage collector: Serial GC 1 user-specific feature(s) - org.springframework.aot.nativex.feature.PreComputeFieldFeatureField org.apache.commons.logging.LogAdapter#log4jSpiPresent set to true at build timeField org.apache.commons.logging.LogAdapter#log4jSlf4jProviderPresent set to true at build timeField org.apache.commons.logging.LogAdapter#slf4jSpiPresent set to true at build timeField org.apache.commons.logging.LogAdapter#slf4jApiPresent set to true at build timeField org.springframework.core.NativeDetector#imageCode set to true at build timeField org.springframework.core.KotlinDetector#kotlinPresent set to false at build timeField org.springframework.core.KotlinDetector#kotlinReflectPresent set to false at build timeField org.springframework.format.support.DefaultFormattingConversionService#jsr354Present set to false at build timeField org.springframework.cglib.core.AbstractClassGenerator#imageCode set to true at build timeField org.springframework.boot.logging.log4j2.Log4J2LoggingSystem$Factory#PRESENT set to false at build timeField org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#romePresent set to false at build timeField org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#jaxb2Present set to false at build timeField org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#jackson2Present set to true at build timeField org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#jackson2XmlPresent set to false at build timeField org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#jackson2SmilePresent set to false at build timeField org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#jackson2CborPresent set to false at build timeField org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#gsonPresent set to false at build timeField org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#jsonbPresent set to false at build timeField org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#kotlinSerializationCborPresent set to false at build timeField org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#kotlinSerializationJsonPresent set to false at build timeField org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#kotlinSerializationProtobufPresent set to false at build timeField org.springframework.boot.logging.java.JavaLoggingSystem$Factory#PRESENT set to true at build timeField org.springframework.boot.logging.logback.LogbackLoggingSystem$Factory#PRESENT set to true at build timeField org.springframework.http.converter.json.Jackson2ObjectMapperBuilder#jackson2XmlPresent set to false at build timeField org.springframework.web.servlet.view.InternalResourceViewResolver#jstlPresent set to false at build timeField org.springframework.web.context.support.StandardServletEnvironment#jndiPresent set to true at build timeField org.springframework.boot.logging.logback.LogbackLoggingSystemProperties#JBOSS_LOGGING_PRESENT set to false at build timeField org.springframework.web.context.support.WebApplicationContextUtils#jsfPresent set to false at build timeField org.springframework.web.context.request.RequestContextHolder#jsfPresent set to false at build timeField org.springframework.context.event.ApplicationListenerMethodAdapter#reactiveStreamsPresent set to false at build timeField org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#jaxb2Present set to false at build timeField org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#jackson2Present set to true at build timeField org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#jackson2XmlPresent set to false at build timeField org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#jackson2SmilePresent set to false at build timeField org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#gsonPresent set to false at build timeField org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#jsonbPresent set to false at build timeField org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#kotlinSerializationCborPresent set to false at build timeField org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#kotlinSerializationJsonPresent set to false at build timeField org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#kotlinSerializationProtobufPresent set to false at build timeField org.springframework.core.ReactiveAdapterRegistry#reactorPresent set to false at build timeField org.springframework.core.ReactiveAdapterRegistry#rxjava3Present set to false at build timeField org.springframework.core.ReactiveAdapterRegistry#kotlinCoroutinesPresent set to false at build timeField org.springframework.core.ReactiveAdapterRegistry#mutinyPresent set to false at build timeField org.springframework.web.client.RestTemplate#romePresent set to false at build timeField org.springframework.web.client.RestTemplate#jaxb2Present set to false at build timeField org.springframework.web.client.RestTemplate#jackson2Present set to true at build timeField org.springframework.web.client.RestTemplate#jackson2XmlPresent set to false at build timeField org.springframework.web.client.RestTemplate#jackson2SmilePresent set to false at build timeField org.springframework.web.client.RestTemplate#jackson2CborPresent set to false at build timeField org.springframework.web.client.RestTemplate#gsonPresent set to false at build timeField org.springframework.web.client.RestTemplate#jsonbPresent set to false at build timeField org.springframework.web.client.RestTemplate#kotlinSerializationCborPresent set to false at build timeField org.springframework.web.client.RestTemplate#kotlinSerializationJsonPresent set to false at build timeField org.springframework.web.client.RestTemplate#kotlinSerializationProtobufPresent set to false at build timeField org.springframework.boot.autoconfigure.web.format.WebConversionService#JSR_354_PRESENT set to false at build timeSLF4J: No SLF4J providers were found.SLF4J: Defaulting to no-operation (NOP) logger implementationSLF4J: See https://www.slf4j.org/codes.html#noProviders for further details.Field org.springframework.web.servlet.mvc.method.annotation.ReactiveTypeHandler#isContextPropagationPresent set to false at build timeField org.springframework.web.servlet.support.RequestContext#jstlPresent set to false at build time[2/7] Performing analysis...  [**********]                                                              (98.5s @ 1.27GB)  15,315 (92.75%) of 16,513 classes reachable  24,931 (67.59%) of 36,886 fields reachable  73,546 (62.21%) of 118,224 methods reachable     802 classes,   244 fields, and 4,549 methods registered for reflection      83 classes,    78 fields, and    68 methods registered for JNI access       5 native libraries: crypt32, ncrypt, psapi, version, winhttp[3/7] Building universe...                                                                               (9.2s @ 1.48GB)[4/7] Parsing methods...      [**]                                                                       (3.0s @ 1.71GB)[5/7] Inlining methods...     [***]                                                                      (2.1s @ 1.75GB)[6/7] Compiling methods...    [*********]                                                               (92.6s @ 1.80GB)[7/7] Creating image...                                                                                (162.3s @ 1.29GB)  34.44MB (52.32%) for code area:    48,187 compilation units  30.94MB (47.01%) for image heap:  354,772 objects and 118 resources 445.88KB ( 0.66%) for other data  65.81MB in total------------------------------------------------------------------------------------------------------------------------Top 10 packages in code area:                               Top 10 object types in image heap:   1.64MB sun.security.ssl                                     7.33MB byte[] for code metadata   1.05MB java.util                                            3.64MB java.lang.Class 835.67KB java.lang.invoke                                     3.41MB java.lang.String 725.04KB com.sun.crypto.provider                              2.87MB byte[] for general heap data 560.78KB org.apache.catalina.core                             2.83MB byte[] for java.lang.String 527.20KB org.apache.tomcat.util.net                           1.29MB com.oracle.svm.core.hub.DynamicHubCompanion 495.45KB org.apache.coyote.http2                            880.07KB byte[] for reflection metadata 477.20KB java.lang                                          775.15KB byte[] for embedded resources 470.10KB c.s.org.apache.xerces.internal.impl.xs.traversers  664.68KB java.lang.String[] 467.89KB java.util.concurrent                               621.94KB java.util.HashMap$Node  26.94MB for 621 more packages                                5.69MB for 3067 more object types------------------------------------------------------------------------------------------------------------------------                      134.1s (26.7% of total time) in 116 GCs | Peak RSS: 2.60GB | CPU load: 3.09------------------------------------------------------------------------------------------------------------------------Produced artifacts: C:\Users\LiuZijian\IdeaProjects\springboot3\target\springboot3.build_artifacts.txt (txt) C:\Users\LiuZijian\IdeaProjects\springboot3\target\springboot3.exe (executable)========================================================================================================================Finished generating 'springboot3' in 8m 20s.[INFO] ------------------------------------------------------------------------[INFO] BUILD SUCCESS[INFO] ------------------------------------------------------------------------[INFO] Total time:  08:30 min[INFO] Finished at: 2024-11-14T23:22:01+08:00[INFO] ------------------------------------------------------------------------Process finished with exit code 0

编译完成,得到.exe文件

运行编译成的.exe文件并测试

.exe文件很快启动,并可以正常处理http请求,虽然命令窗口打印存在问题,但是接口是能调通的,文章写到这已经很晚了,故这个问题先留着后续有空解决…

3.linux平台安装和配置GraalVM

Linux平台安装和配置GraalVM,我使用Ubuntu22进行测试

3.1 安装Linux编译工具链

编译native-image,需要安装Linux下的编译环境

sudo apt-get install build-essential libz-dev zlib1g-dev

! tips: 如果是redhat系列linux,可能需要这样安装

sudo yum install -y gcc glibc glibc-devel zlib-devel

3.2 Linux下载安装GraalVM

接下来,和Windows系统一样,下载GraalVMnative-image的Linux x64版本,我在这里还是选择和Windows一样的22.3.3版本。

下载地址仍然是 https://github.com/graalvm/graalvm-ce-builds/releases

  • graalvm-ce-java17-linux-amd64-22.3.3.tar.gz

https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-22.3.3/graalvm-ce-java17-linux-amd64-22.3.3.tar.gz

  • native-image-installable-svm-java17-linux-amd64-22.3.3.jar

https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-22.3.3/native-image-installable-svm-java17-linux-amd64-22.3.3.jar

将下载好的两个文件移动到/opt目录下,解压graalvm-ce-java17-linux-amd64-22.3.3.tar.gz到路径/opt并设置环境变量

解压到/opt

tar -zxvf ./graalvm-ce-java17-linux-amd64-22.3.3.tar.gz

打开/etc/profile设置环境变量

vim /etc/profile

将下面两句放在文件最末尾

export JAVA_HOME=/opt/graalvm-ce-java17-22.3.3export PATH=$PATH:$JAVA_HOME/bin

刷新,使环境变量生效

source /etc/profile

返回/opt目录,安装native-image.jar原生镜像打包工具包,命令和Windows之前安装的命令完全相同

cd /optgu install --file ./native-image-installable-svm-java17-linux-amd64-22.3.3.jar

验证JAVA_HOME变量是否生效,native-image.jar是否正确安装

java -versionnative-image

3.3 编译打包SpringBoot项目

首先需要安装Linux环境的maven,并设置好环境变量,然后将工程源码拷贝到/opt目录下,然后依次执行maven命令。

先用普通的编译命令将代码编译为.class文件

mvn run compile

然后,执行spring-bootprocess-aot插件,进行编译前的前置处理

mvn spring-boot:process-aot

开始编译,运行native插件的build启动native-image,调用Linux底层工具链将工程编译为Linux平台的二进制文件

mvn  native:build

终端输出

root@lzj-virtual-machine:/opt/springboot3# mvn  native:build[INFO] Scanning for projects...[INFO] [INFO] ----------------------< org.example:springboot3 >-----------------------[INFO] Building springboot3 1.0-SNAPSHOT[INFO]   from pom.xml[INFO] --------------------------------[ jar ]---------------------------------[INFO] [INFO] --- native:0.9.21:build (default-cli) @ springboot3 ---[WARNING] 'native:build' goal is deprecated. Use 'native:compile-no-fork' instead.[INFO] Found GraalVM installation from JAVA_HOME variable.[INFO] Executing: /opt/graalvm-ce-java17-22.3.3/bin/native-image -cp /opt/springboot3/target/classes:/root/.m2/repository/org/springframework/boot/spring-boot-starter/3.0.6/spring-boot-starter-3.0.6.jar:/root/.m2/repository/ch/qos/logback/logback-classic/1.4.7/logback-classic-1.4.7.jar:/root/.m2/repository/org/slf4j/slf4j-api/2.0.7/slf4j-api-2.0.7.jar:/root/.m2/repository/io/micrometer/micrometer-observation/1.10.6/micrometer-observation-1.10.6.jar:/root/.m2/repository/org/springframework/spring-expression/6.0.8/spring-expression-6.0.8.jar:/root/.m2/repository/ch/qos/logback/logback-core/1.4.7/logback-core-1.4.7.jar:/root/.m2/repository/org/springframework/spring-webmvc/6.0.8/spring-webmvc-6.0.8.jar:/root/.m2/repository/org/springframework/boot/spring-boot/3.0.6/spring-boot-3.0.6.jar:/root/.m2/repository/org/springframework/boot/spring-boot-starter-logging/3.0.6/spring-boot-starter-logging-3.0.6.jar:/root/.m2/repository/org/springframework/boot/spring-boot-starter-json/3.0.6/spring-boot-starter-json-3.0.6.jar:/root/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.14.2/jackson-datatype-jsr310-2.14.2.jar:/root/.m2/repository/org/springframework/spring-aop/6.0.8/spring-aop-6.0.8.jar:/root/.m2/repository/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar:/root/.m2/repository/org/apache/tomcat/embed/tomcat-embed-websocket/10.1.8/tomcat-embed-websocket-10.1.8.jar:/root/.m2/repository/org/springframework/spring-context/6.0.8/spring-context-6.0.8.jar:/root/.m2/repository/org/slf4j/jul-to-slf4j/2.0.7/jul-to-slf4j-2.0.7.jar:/root/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.14.2/jackson-databind-2.14.2.jar:/root/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.14.2/jackson-datatype-jdk8-2.14.2.jar:/root/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/3.0.6/spring-boot-autoconfigure-3.0.6.jar:/root/.m2/repository/org/apache/tomcat/embed/tomcat-embed-el/10.1.8/tomcat-embed-el-10.1.8.jar:/root/.m2/repository/io/micrometer/micrometer-commons/1.10.6/micrometer-commons-1.10.6.jar:/root/.m2/repository/org/apache/logging/log4j/log4j-api/2.19.0/log4j-api-2.19.0.jar:/root/.m2/repository/org/springframework/spring-web/6.0.8/spring-web-6.0.8.jar:/root/.m2/repository/com/fasterxml/jackson/module/jackson-module-parameter-names/2.14.2/jackson-module-parameter-names-2.14.2.jar:/root/.m2/repository/org/apache/logging/log4j/log4j-to-slf4j/2.19.0/log4j-to-slf4j-2.19.0.jar:/root/.m2/repository/org/apache/tomcat/embed/tomcat-embed-core/10.1.8/tomcat-embed-core-10.1.8.jar:/root/.m2/repository/org/springframework/spring-core/6.0.8/spring-core-6.0.8.jar:/root/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/2.14.2/jackson-annotations-2.14.2.jar:/root/.m2/repository/org/yaml/snakeyaml/1.33/snakeyaml-1.33.jar:/root/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.14.2/jackson-core-2.14.2.jar:/root/.m2/repository/org/springframework/boot/spring-boot-starter-web/3.0.6/spring-boot-starter-web-3.0.6.jar:/root/.m2/repository/org/springframework/boot/spring-boot-starter-tomcat/3.0.6/spring-boot-starter-tomcat-3.0.6.jar:/root/.m2/repository/org/springframework/spring-jcl/6.0.8/spring-jcl-6.0.8.jar:/root/.m2/repository/org/springframework/spring-beans/6.0.8/spring-beans-6.0.8.jar --no-fallback -H:Path=/opt/springboot3/target -H:Name=springboot3========================================================================================================================GraalVM Native Image: Generating 'springboot3' (executable)...========================================================================================================================[1/7] Initializing...                                                                                   (38.9s @ 0.18GB) Version info: 'GraalVM 22.3.3 Java 17 CE' Java version info: '17.0.8+7-jvmci-22.3-b22' C compiler: gcc (linux, x86_64, 11.4.0) Garbage collector: Serial GC 1 user-specific feature(s) - org.springframework.aot.nativex.feature.PreComputeFieldFeatureField org.springframework.core.NativeDetector#imageCode set to true at build timeField org.apache.commons.logging.LogAdapter#log4jSpiPresent set to true at build timeField org.apache.commons.logging.LogAdapter#log4jSlf4jProviderPresent set to true at build timeField org.apache.commons.logging.LogAdapter#slf4jSpiPresent set to true at build timeField org.apache.commons.logging.LogAdapter#slf4jApiPresent set to true at build timeField org.springframework.core.KotlinDetector#kotlinPresent set to false at build timeField org.springframework.core.KotlinDetector#kotlinReflectPresent set to false at build timeField org.springframework.cglib.core.AbstractClassGenerator#imageCode set to true at build timeField org.springframework.format.support.DefaultFormattingConversionService#jsr354Present set to false at build timeField org.springframework.boot.logging.logback.LogbackLoggingSystem$Factory#PRESENT set to true at build timeField org.springframework.web.servlet.view.InternalResourceViewResolver#jstlPresent set to false at build timeField org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#romePresent set to false at build timeField org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#jaxb2Present set to false at build timeField org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#jackson2Present set to true at build timeField org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#jackson2XmlPresent set to false at build timeField org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#jackson2SmilePresent set to false at build timeField org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#jackson2CborPresent set to false at build timeField org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#gsonPresent set to false at build timeField org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#jsonbPresent set to false at build timeField org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#kotlinSerializationCborPresent set to false at build timeField org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#kotlinSerializationJsonPresent set to false at build timeField org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#kotlinSerializationProtobufPresent set to false at build timeField org.springframework.boot.logging.log4j2.Log4J2LoggingSystem$Factory#PRESENT set to false at build timeField org.springframework.boot.logging.java.JavaLoggingSystem$Factory#PRESENT set to true at build timeField org.springframework.http.converter.json.Jackson2ObjectMapperBuilder#jackson2XmlPresent set to false at build timeField org.springframework.web.context.support.StandardServletEnvironment#jndiPresent set to true at build timeField org.springframework.web.context.support.WebApplicationContextUtils#jsfPresent set to false at build timeField org.springframework.web.context.request.RequestContextHolder#jsfPresent set to false at build timeField org.springframework.boot.logging.logback.LogbackLoggingSystemProperties#JBOSS_LOGGING_PRESENT set to false at build timeField org.springframework.context.event.ApplicationListenerMethodAdapter#reactiveStreamsPresent set to false at build timeField org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#jaxb2Present set to false at build timeField org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#jackson2Present set to true at build timeField org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#jackson2XmlPresent set to false at build timeField org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#jackson2SmilePresent set to false at build timeField org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#gsonPresent set to false at build timeField org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#jsonbPresent set to false at build timeField org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#kotlinSerializationCborPresent set to false at build timeField org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#kotlinSerializationJsonPresent set to false at build timeField org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#kotlinSerializationProtobufPresent set to false at build timeField org.springframework.web.client.RestTemplate#romePresent set to false at build timeField org.springframework.web.client.RestTemplate#jaxb2Present set to false at build timeField org.springframework.web.client.RestTemplate#jackson2Present set to true at build timeField org.springframework.web.client.RestTemplate#jackson2XmlPresent set to false at build timeField org.springframework.web.client.RestTemplate#jackson2SmilePresent set to false at build timeField org.springframework.web.client.RestTemplate#jackson2CborPresent set to false at build timeField org.springframework.web.client.RestTemplate#gsonPresent set to false at build timeField org.springframework.web.client.RestTemplate#jsonbPresent set to false at build timeField org.springframework.web.client.RestTemplate#kotlinSerializationCborPresent set to false at build timeField org.springframework.web.client.RestTemplate#kotlinSerializationJsonPresent set to false at build timeField org.springframework.web.client.RestTemplate#kotlinSerializationProtobufPresent set to false at build timeField org.springframework.core.ReactiveAdapterRegistry#reactorPresent set to false at build timeField org.springframework.core.ReactiveAdapterRegistry#rxjava3Present set to false at build timeField org.springframework.core.ReactiveAdapterRegistry#kotlinCoroutinesPresent set to false at build timeField org.springframework.core.ReactiveAdapterRegistry#mutinyPresent set to false at build timeField org.springframework.boot.autoconfigure.web.format.WebConversionService#JSR_354_PRESENT set to false at build timeSLF4J: No SLF4J providers were found.SLF4J: Defaulting to no-operation (NOP) logger implementationSLF4J: See https://www.slf4j.org/codes.html#noProviders for further details.Field org.springframework.web.servlet.mvc.method.annotation.ReactiveTypeHandler#isContextPropagationPresent set to false at build timeField org.springframework.web.servlet.support.RequestContext#jstlPresent set to false at build time[2/7] Performing analysis...  [*********]                                                              (450.4s @ 2.25GB)  15,331 (92.43%) of 16,586 classes reachable  24,926 (67.58%) of 36,883 fields reachable  73,471 (62.05%) of 118,406 methods reachable     784 classes,   246 fields, and 4,536 methods registered for reflection      64 classes,    70 fields, and    55 methods registered for JNI access       4 native libraries: dl, pthread, rt, z[3/7] Building universe...                                                                              (71.4s @ 1.81GB)[4/7] Parsing methods...      [*******]                                                                 (48.4s @ 1.67GB)[5/7] Inlining methods...     [***]                                                                     (18.7s @ 2.49GB)[6/7] Compiling methods...    [**********]                                                             (112.1s @ 2.00GB)[7/7] Creating image...                                                                                 (53.6s @ 2.85GB)  33.97MB (51.08%) for code area:    48,119 compilation units  29.90MB (44.97%) for image heap:  353,714 objects and 118 resources   2.63MB ( 3.96%) for other data  66.50MB in total------------------------------------------------------------------------------------------------------------------------Top 10 packages in code area:                               Top 10 object types in image heap:   1.63MB sun.security.ssl                                     7.29MB byte[] for code metadata   1.04MB java.util                                            3.64MB java.lang.Class 829.18KB java.lang.invoke                                     3.41MB java.lang.String 717.16KB com.sun.crypto.provider                              2.86MB byte[] for general heap data 558.99KB org.apache.catalina.core                             2.82MB byte[] for java.lang.String 519.74KB org.apache.tomcat.util.net                           1.29MB com.oracle.svm.core.hub.DynamicHubCompanion 491.53KB org.apache.coyote.http2                            878.75KB byte[] for reflection metadata 476.53KB java.lang                                          775.14KB byte[] for embedded resources 467.23KB c.s.org.apache.xerces.internal.impl.xs.traversers  664.31KB java.lang.String[] 461.61KB sun.security.x509                                  619.31KB java.util.HashMap$Node  26.53MB for 628 more packages                                5.45MB for 3069 more object types------------------------------------------------------------------------------------------------------------------------                       189.1s (21.7% of total time) in 60 GCs | Peak RSS: 4.14GB | CPU load: 8.45------------------------------------------------------------------------------------------------------------------------Produced artifacts: /opt/springboot3/target/springboot3 (executable) /opt/springboot3/target/springboot3.build_artifacts.txt (txt)========================================================================================================================Finished generating 'springboot3' in 14m 28s.[INFO] ------------------------------------------------------------------------[INFO] BUILD SUCCESS[INFO] ------------------------------------------------------------------------[INFO] Total time:  14:40 min[INFO] Finished at: 2024-11-16T00:59:34+08:00[INFO] ------------------------------------------------------------------------

编译打包完成

编译完成,得到Linux上的可执行文件

运行可执行文件,并打开浏览器访问测试,功能正常

至此,Linux平台上原生镜像编译就完成了。

4.注意事项

  1. 不是所有Java代码都支持原生编译打包,例如代码中含有反射创建对象,反射调用方法等操作会导致AOT损失动态能力,如果编译需要额外处理,需要提前告知GraalVM未来会反射调用哪些方法或构造器,例如springboot就提供了一些注解,保证AOT编译时功能正常,但不是所有框架都像Spring那样适配了AOT

  2. 配置文件不能放进可执行文件,也需要额外处理,可以在程序代码改为相对路径读取配置文件。

使用python将excel表格转换为SQL INSERT

2024年10月16日 10:00
import pandas as pd# 读取Excel文件file_path = 'D:/1.xlsx'  # 替换为你的文件路径sheet_name = '1'  # 替换为你的工作表名称table_name = 'your_table_name'  # 替换为你的数据库表名称# 使用pandas读取Excel数据df = pd.read_excel(file_path, sheet_name=sheet_name)# 获取表的列名columns = ', '.join(df.columns)# 将每行数据转换为SQL INSERT语句insert_statements = []for index, row in df.iterrows():    # 将每行数据转换为元组格式    values = ', '.join([f"'{str(value)}'" for value in row.values])    sql = f"INSERT INTO {table_name} ({columns}) VALUES ({values});"    insert_statements.append(sql)# 输出SQL INSERT语句for statement in insert_statements:    print(statement)

使用python压缩图片

2024年10月15日 20:30
  1. 首先Linux上面要安装一些软件包
yum install -y libjpeg-develyum install -y zlib-develyum install -y libjpeg libtiff freetype 
  1. 其次需要安装python的依赖
pip3 install tinifypip3 install Pillow
  1. 编写并运行以下代码
import osfrom PIL import Imageimport tinifyfrom concurrent.futures import ThreadPoolExecutor# 配置部分input_folder = '/img'  # 要压缩的图片文件夹whitelist = {'example1.jpg', 'important_image.png'}  # 白名单中的文件quality = 75  # 压缩质量def compress_image(img_path, quality):    """    压缩单张图片并覆盖原文件。        :param img_path: 图片路径    :param quality: 压缩质量    """    try:        img = Image.open(img_path)        img.save(img_path, optimize=True, quality=quality)        print(f"Compressed and saved in place: {img_path}")    except Exception as e:        print(f"Failed to compress {img_path}: {e}")def compress_images_in_place(input_folder, whitelist=None, quality=75):    """    使用 Pillow 压缩图片并覆盖原文件,支持白名单功能。使用多线程加速处理。        :param input_folder: 原始图片文件夹路径    :param whitelist: 白名单列表,包含不希望被压缩的图片文件名(可选)    :param quality: 压缩质量,默认75    """    if whitelist is None:        whitelist = set()    # 创建一个 ThreadPoolExecutor 来处理压缩任务    with ThreadPoolExecutor() as executor:        futures = []        for root, dirs, files in os.walk(input_folder):            for file in files:                if file in whitelist:                    print(f"Skipping (whitelisted): {file}")                    continue                if file.lower().endswith(('.jpg', '.jpeg', '.png')):                    img_path = os.path.join(root, file)                    futures.append(executor.submit(compress_image, img_path, quality))        # 等待所有任务完成        for future in futures:            future.result()# 调用压缩函数compress_images_in_place(input_folder, whitelist, quality)

利用Python实现Hexo站点的持续集成

2024年10月15日 20:00

引言

Hexo博客编写完文章需要从头构建并重新上传生成的静态文件到服务器,不能增量更新十分不便,每次上传的文件也越来越大,于是一个比较好的解决办法是在提交文章、资源和配置后,让服务器自动去从git远程仓库拉取最新的提交到服务器上的本地git仓库中,并部署。使用Jenkins等持续集成工具比较繁琐也消耗资源,于是我选择用python编写一个基于flask的webhook脚本来实现。

具体实现流程是:文章提交到git代码托管平台的远程仓库后,远程仓库发送HTTP回调请求给服务器上的webhook脚本,脚本程序根据回调请求的参数,先调用git拉取远程仓库最新提交内容,再去调用npm构建和部署最新版hexo博客到nginx下。

这里的git代码托管平台我选择Gitee:https://gitee.com/

1. 安装pip和python

首先,确保你已经安装了python。如果没有安装,可以使用以下命令来安装python和pip:

1.1 检查python版本

python3 --version

如果你已经安装了python 3.x版本,可以跳过安装python的步骤。否则,继续安装:

1.2 安装python3

sudo yum install python3 -y  # 适用于 CentOS 或其他 RHEL 系统

1.3 安装pip

安装pip的方法:

sudo yum install python3-pip -y  # CentOS/RHEL 系统

安装完成后,确认pip是否已经成功安装:

pip3 --version

2. 使用pip安装依赖

一旦 pip 安装好,你可以使用以下命令来安装需要的库:

pip3 install flask gitpython

3.编写脚本

vim webhook.py
import osimport subprocessfrom flask import Flask, request, jsonifyimport gitapp = Flask(__name__)# 配置你的本地仓库路径和构建命令REPO_PATH = "/path/to/your/hexo/blog"PUBLIC_PATH = os.path.join(REPO_PATH, 'public')# 拉取代码的函数def pull_code():    try:        repo = git.Repo(REPO_PATH)        origin = repo.remotes.origin        origin.pull()        return True    except Exception as e:        print(f"Failed to pull code: {e}")        return False# 构建 Hexo 站点的函数def build_hexo():    try:        # 执行 Hexo 命令        subprocess.run(["npm", "run", "build"], cwd=REPO_PATH, check=True)                return True    except subprocess.CalledProcessError as e:        print(f"Failed to build Hexo: {e}")        return False@app.route("/webhook", methods=["POST"])def webhook():    # 验证请求是否来自 Gitee    if request.headers.get("X-Gitee-Token") != "": #这里改成你设置的密码        return jsonify({"message": "Unauthorized"}), 401    # 获取事件类型,确保是 push 事件    event = request.headers.get("X-Gitee-Event")    if event != "Push Hook":        return jsonify({"message": "Not a push event"}), 400    # 拉取代码并构建    if pull_code() and build_hexo():        return jsonify({"message": "Hexo build success"}), 200    else:        return jsonify({"message": "Failed to pull or build"}), 500if __name__ == "__main__":    app.run(host="0.0.0.0", port=5000)

代码优化,加入线程控制,防止webhook链接被并发调用后,两个hook任务线程同时执行出现安全问题。

import osimport subprocessfrom flask import Flask, request, jsonifyimport gitimport threadingapp = Flask(__name__)# 配置你的本地仓库路径和构建命令REPO_PATH = "/blog"PUBLIC_PATH = os.path.join(REPO_PATH, 'public')lock = threading.Lock()is_building = False  # 标志位,用于指示是否有任务正在进行# 拉取代码的函数def pull_code():    try:        repo = git.Repo(REPO_PATH)        origin = repo.remotes.origin        origin.pull()        return True    except Exception as e:        print(f"Failed to pull code: {e}")        return False# 构建 Hexo 站点的函数def build_hexo():    try:        # 执行 Hexo 的清理和生成命令        subprocess.run(["npm", "run", "build"], cwd=REPO_PATH, check=True)        #subprocess.run(["hexo", "generate"], cwd=REPO_PATH, check=True)        return True    except subprocess.CalledProcessError as e:        print(f"Failed to build Hexo: {e}")        return False@app.route("/webhook", methods=["POST"])def webhook():    global is_building    # 验证请求是否来自 Gitee    if request.headers.get("X-Gitee-Token") != "":        return jsonify({"message": "Unauthorized"}), 401    # 获取事件类型,确保是 push 事件    event = request.headers.get("X-Gitee-Event")    if event != "Push Hook":        return jsonify({"message": "Not a push event"}), 400    if is_building:        return jsonify({"message": "Build in progress, try again later"}), 429    with lock:        is_building = True  # 设置标志位为 True,表示任务开始        try:            # 拉取代码并构建            if pull_code() and build_hexo():                return jsonify({"message": "Hexo build success"}), 200            else:                return jsonify({"message": "Failed to pull or build"}), 500        finally:            is_building = False  # 重置标志位,表示任务结束            if __name__ == "__main__":    app.run(host="0.0.0.0", port=5000)

代码还可以进一步优化,只对master分支提交推送进行触发构建。

import osimport subprocessfrom flask import Flask, request, jsonifyimport gitimport threadingapp = Flask(__name__)# 配置你的本地仓库路径和构建命令REPO_PATH = "/blog"PUBLIC_PATH = os.path.join(REPO_PATH, 'public')lock = threading.Lock()is_building = False  # 标志位,用于指示是否有任务正在进行# 拉取代码的函数def pull_code():    try:        repo = git.Repo(REPO_PATH)        origin = repo.remotes.origin        origin.pull()        return True    except Exception as e:        print(f"Failed to pull code: {e}")        return False# 构建 Hexo 站点的函数def build_hexo():    try:        # 执行 Hexo 的清理和生成命令        subprocess.run(["npm", "run", "build"], cwd=REPO_PATH, check=True)        #subprocess.run(["hexo", "generate"], cwd=REPO_PATH, check=True)        return True    except subprocess.CalledProcessError as e:        print(f"Failed to build Hexo: {e}")        return False@app.route("/webhook", methods=["POST"])def webhook():    global is_building    # 验证请求是否来自 Gitee    if request.headers.get("X-Gitee-Token") != "":        return jsonify({"message": "Unauthorized"}), 401    # 获取事件类型,确保是 push 事件    event = request.headers.get("X-Gitee-Event")    if event != "Push Hook":        return jsonify({"message": "Not a push event"}), 400    # 解析推送数据    payload = request.get_json()    if not payload:        return jsonify({"message": "Invalid payload"}), 400    # 检查是否是 master 分支的推送    ref = payload.get("ref")    if ref != "refs/heads/master":        return jsonify({"message": "Not a master branch push, ignored"}), 200    if is_building:        return jsonify({"message": "Build in progress, try again later"}), 429    with lock:        is_building = True  # 设置标志位为 True,表示任务开始        try:            # 拉取代码并构建            if pull_code() and build_hexo():                return jsonify({"message": "Hexo build success"}), 200            else:                return jsonify({"message": "Failed to pull or build"}), 500        finally:            is_building = False  # 重置标志位,表示任务结束            if __name__ == "__main__":    app.run(host="0.0.0.0", port=5000)

4.执行脚本

执行前,服务器还需要安装好node、npm、git并配置好环境变量

nohup python3 webhook.py &

5.配置hook到gitee

设置好签名(密码),设置回调地址,勾选两项

除此外,还要将自己gitee账户相匹配的ssh密钥设置在服务器的上用于拉取我们私有仓库的内容。

基于Hexo实现一个静态的个人博客

2024年9月20日 00:00

引言

Hexo是中国台湾开发者Charlie在2012年创建的一个开源项目,旨在提供一个简单、快速且易于扩展的静态博客生成器。

Hexo的设计理念是轻量级、易用和支持插件扩展,因此它非常适合那些有技术背景的用户,尤其是喜欢使用Markdown和Git进行内容管理的开发者。Hexo使用Node.js构建,并且支持通过主题和插件来扩展功能。

1.初始化Hexo

Hexo基于node.js,所以首先要安装node和npm

1.全局安装hexo

npm install hexo -g

2.生成hexo项目

安装完成后,选择一个目录,执行以下命令,生成一个hexo脚手架

hexo init myblog

然后生成脚手架结构如下

myblog/ ├── node_modules ├── scaffolds │── scripts    ├── source/ │    ├── _posts │    └── _drafts ├── themes ├── _config.landscape.yml ├── _config.yml └── package.json

文件目录解释

  • node_modules不解释
  • scaffolds 存放博客内容模板的地方
  • source 资源文件夹,下面用来放建站需要的各种资源,包括markdown格式的博客原稿,图片,css,js,robots.txt等等
  • source/_posts 博客的”源码”,里面放我们写作的markdown文件,会被hexo引擎按照一定规则转换成html页面
  • source/_drafts 草稿,不适合放在_posts里面打包发布的草稿就放在这里
  • themes 存放第三方主题
  • _config.yml hexo配置文件
  • _config.landscape.yml 默认主题landscape的配置文件
  • package.json不解释

提前剧透:hexo使用过程中,还会经常用到两个文件夹

  • scripts hexo自定义脚本,在构建过程中执行,用于扩展hexo的功能或实现一些特殊需求
  • public 构建生成的静态网站,可以直接使用nginx root反代访问

3.启动预览

修改_config.yml配置文件

# Sitetitle: '我的博客'subtitle: '我的技术博客'description: '分享一些实用的东西'author: 'liuzijian'language: zh-CNtimezone: 'Asia/Shanghai'

然后,运行package.json里面的目标npm run server

直到输出提示Hexo is running at http://localhost:4000/ . Press Ctrl+C to stop.以后,访问http://127.0.0.1:4000进行预览,浏览器出现这个界面说明启动成功

4.构建打包

运行npm run build后,根目录就会生成一个public文件夹,就是构建成的静态网站,上传到服务器并使用nginx代理,访问/index.html即可打开博客首页。

除了部署到自己的服务器,还可以通过VercelGitHub Pages等进行部署。

2.整合主题Fluid

hexo默认自带一个叫landscape的主题,根目录下的_config.landscape.yml就是它的配置文件,landscape比较简陋,也不美观,所以很多人选择美观且功能强大的第三方主题,在这里,我使用主题fluid来建站。

集成第三方主题到hexo中非常简单,只需要将主题解压放在theme文件夹,复制主题的配置文件到根目录下,然后到hexo配置文件内切换主题即可。

1.下载主题fluid

项目首页和文档地址: https://hexo.fluid-dev.com/

下载地址: https://github.com/fluid-dev/hexo-theme-fluid/releases

找到最新的1.9.8版本下载连接

https://github.com/fluid-dev/hexo-theme-fluid/archive/refs/tags/v1.9.8.zip

2.集成主题

将压缩包下载下来,解压,压缩包内文件夹名字叫hexo-theme-fluid-1.9.8,这个就是主题,不过要把解压后的hexo-theme-fluid-1.9.8文件夹重命名为fluid,紧接着要再将fluid文件夹里面的_config.yml文件复制一个副本,将副本重命名为_config.fluid.yml,再将_config.fluid.yml剪切到hexo项目根目录下和hexo脚手架自带的的_config.yml文件放在一起,接下来还要再把整个fluid文件夹拷贝到根目录下的themes文件夹内。

⚠️注意: 主题文件夹内的_config.yml一定要创建副本,而不是直接改名,副本_config.fluid.yml剪切走后,原有的_config.yml文件必须原样保留! hexo-theme-fluid-1.9.8文件夹下的_config.yml是主题的配置,与脚手架生成在根目录的hexo配置文件_config.yml是不同的,重命名后的主题配置文件_config.fluid.yml和hexo脚手架自带的配置文件_config.yml放在一起共同作为hexo的配置文件,其他的第三方主题基本也遵循这个方式。关于配置文件的更多解释可以参考Hexo官方文档以及主题的官方文档。
实际上和根目录下脚手架自带的_config.landscape.yml文件是一样的道理,现在换成了别的主题,_config.landscape.yml也就没用了,可以直接删除

myblog/    ├── ... ...    ├── themes    │     └── fluid    ├── _config.fluid.yml    ├── _config.yml    └── ... ...

要想主题生效,还需要在配置文件进行指定,修改_config.yml,找到以下配置

# Extensions## Plugins: https://hexo.io/plugins/## Themes: https://hexo.io/themes/theme: landscape

修改为以下,然后保存

# Extensions## Plugins: https://hexo.io/plugins/## Themes: https://hexo.io/themes/theme: fluid

再次启动npm run server,浏览器预览博客的效果已经变为主题的风格,即为集成主题成功

主题更多玩法,在主题的官网都有文档说明,主题配置文件_config.fluid.yml内也有详细注释,这里不再详细一一介绍,集成其他主题的方式方法同理。

3.部署评论系统Waline

评论系统一般由主题提供支持,支持的评论系统有许许多多,这里我使用的是Waline,并且采用Docker独立部署Waline

Waline的使用部署等可参考官网,其官网地址为: https://waline.js.org/,源码位于GitHub: https://github.com/walinejs/waline.git

1.构建镜像

先用git clone将源码下载到本地,然后进入对应目录,构建镜像。

docker build -t lizheming/waline -f packages/server/Dockerfile .

2.启动Waline服务

使用参数启动镜像,参数根据Waline指定的服务端运行参数而来,这里采用MySQL作为数据存储为例,常见参数有:

  • OAUTH_URL 变量是因为oauth服务也是我自己私有部署的,如果使用waline公共的则不用这个变量
  • IPQPS IP评论频次限制
  • MYSQL_DB 数据库名,使用MySQL部署,需要提前导入数据库脚本,详情查阅waline官网文档
  • MYSQL_USER 不解释
  • MYSQL_PASSWORD 不解释
  • MYSQL_PORT 不解释
  • SERVER_URL 访问waline系统时的地址前缀,因为docker部署由nginx反代,建议设置为反代后的地址
  • COMMENT_AUDIT 评论审核,布尔值
  • SITE_NAME 邮件中展示的网站名
  • SITE_URL 邮件中展示的网站地址
  • AUTHOR_EMAIL 发送者邮件地址
  • SMTP_PASS 邮件服务密码
  • SMTP_USER 邮件用户名
  • SMTP_SERVICE 邮件提供商,具体列表可见官方文档

启动命令示例:

docker run -d \-e OAUTH_URL=https://oauth.liuzijian.com \-e IPQPS=10 \-e MYSQL_DB=waline \-e MYSQL_USER=****** \-e MYSQL_PASSWORD=********** \-e MYSQL_PORT=3306 \-e SERVER_URL=https://waline.liuzijian.com \-e COMMENT_AUDIT=true \-e SITE_NAME="Liu Zijian's Blog"  \-e SITE_URL=https://blog.liuzijian.com  \-e SMTP_PASS=****************  \-e AUTHOR_EMAIL=****@foxmail.com  \-e SMTP_USER=******@foxmail.com  \-e SMTP_SERVICE=QQ  \-p 8360:8360 \--network=host \lizheming/waline

有时,我们选取的服务器配置很低,运行一个MySQL后,内存资源可能就所剩无几,而且我们的个人博客可能初期影响力也不是那么大,评论寥寥无几,使用MySQL等数据库就性价比很低了,因此可以先换成SQLite数据库,SQLite将数据保存在单文件,使用代码本地调用SQLite的API方法实现增删改查,对于初期的评论功能足够用了,使用下面命令就可以启动一个采用SQLite数据库的Waline,Waline官方已经为我们准备好了一个空的waline.sqlite数据文件,里面初始化好了库和表,直接按照官方文档指定的地址下载即可。

  • SQLITE_PATH 本地数据库文件路径,指定到文件夹,不是文件
  • JWT_TOKEN Waline官方要求,我也不知道为什么
docker run -d \-e IPQPS=10 \-e SQLITE_PATH=/opt/sqlite \-e JWT_TOKEN=******** \-e SERVER_URL=https://waline.liuzijian.com \-e COMMENT_AUDIT=true \-e SITE_NAME="Liu Zijian's Blog"  \-e SITE_URL=https://blog.liuzijian.com  \-e SMTP_PASS=****************  \-e AUTHOR_EMAIL=****@foxmail.com  \-e SMTP_USER=******@foxmail.com  \-e SMTP_SERVICE=QQ  \-p 8360:8360 \-v /opt/sqlite_data:/opt/sqlite  \--network=host \lizheming/waline

怎样在fluid主题上使用waline评论,主题文档有说明,不再赘述。

4.采用Nginx部署

页面和评论插件都可以通过nginx根据不同域名进行反代访问,同时https证书也是集成在nginx上的。

主要配置如下:

server {    listen       80;    listen       [::]:80;    server_name  *.liuzijian.com;    return 301 https://$host$request_uri;}## 静态页面server {    listen 443 ssl;    server_name blog.liuzijian.com ;    # SSL 证书路径,按照实际情况填写    ssl_certificate /xxxxx/domain.cert;    ssl_certificate_key /xxxxxx/domain.key;    # 推荐的 SSL 配置    ssl_protocols TLSv1.2 TLSv1.3;    ssl_ciphers HIGH:!aNULL:!MD5;    location / {        try_files $uri $uri/ $uri.html   =404;        root /opt/blog/public;    }    error_page 404 /404.html;}## 评论插件server {    listen 443 ssl;    server_name    waline.liuzijian.com;    # SSL 证书路径,按照实际情况填写    ssl_certificate /xxxxx/domain.cert;    ssl_certificate_key /xxxxxx/domain.key;    # 推荐的 SSL 配置    ssl_protocols TLSv1.2 TLSv1.3;    ssl_ciphers HIGH:!aNULL:!MD5;    location / {        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;        proxy_set_header Host $host;        proxy_set_header X-Real-IP $remote_addr;        proxy_pass http://127.0.0.1:8360;    }}    ## 自动部署hook,选用server {    listen 443 ssl;    server_name hook.liuzijian.com;    # SSL 证书路径,按照实际情况填写    ssl_certificate /xxxxx/domain.cert;    ssl_certificate_key /xxxxxx/domain.key;    # 推荐的 SSL 配置    ssl_protocols TLSv1.2 TLSv1.3;    ssl_ciphers HIGH:!aNULL:!MD5;    # 代理到其他端口    location / {        proxy_pass http://127.0.0.1:5000;    }}

Hexo站点可以采用持续集成的方式部署在服务器,参考:利用Python实现Hexo的持续集成

浅谈OAuth2.0授权原理

2023年8月15日 00:00

一、引言

OAuth(Open Authorization)是一种开放授权协议,允许用户授权第三方应用访问其在其他服务中的资源(如个人信息、照片等)目前的版本是2.0版(OAuth 2.0),其标准是:RFC 6749

OAuth在客户端与服务提供商之间,设置了一个授权层(authorization layer)客户端不能直接登录服务提供商,只能登录授权层获取令牌,以此将用户与客户端区分开来。客户端登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期,客户端登录授权层以后,服务提供商根据令牌的权限范围和有效期,向客户端开放用户储存的资料。

很多场景都有采用OAuth2登录,例如登录ProcessOn或知识星球时可以使用微信登陆,登陆时会先跳转到微信的二维码页面,扫码登录后会再跳转到ProcessOn或知识星球,此时显示的是用户微信的昵称和头像,再比如使用GitHub登录Vercel时,也是先跳到GitHub的登录页面,输入用户名密码登陆后,跳转到Vercel,Vercel里面除了能展示用户GitHub的头像用户名等信息之外,还能获取用户GitHub中的仓库信息,甚至可以获取仓库中各分支下的源代码。这样就实现了脱离第三方系统进行认证,避免了用户直接将自己在微信等服务商的账户密码告诉ProcessOn等第三方平台进行获取数据所导致的安全问题。在这些场景下,上述的的客户端指的就是Vercel/知识星球/ProcessOn等第三方系统,服务提供商就是微信/GitHub。

OAuth2.0规定了四种获得令牌的流程和授权方式:

  • 授权码(authorization code)
  • 隐藏式(implicit),又叫简化模式
  • 密码式(password)
  • 客户端凭证(client credentials)

二、名词定义

在详细讲解OAuth 2.0之前,需要了解几个专用名词

  • Resource Owner 资源所有者,本文中又称用户(user)

  • User Agent 用户代理,本文中就是指浏览器。

  • Third-party application 第三方应用程序,又称客户端

  • HTTP service HTTP服务提供商,本文中简称服务提供商

  • Authorization server 认证服务器,即服务提供商专门用来处理认证的服务器。

  • Resource server 资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。

三、授权方式和原理

3.1 授权码模式(authorization code)

授权码模式(authorization code)是功能最完整、流程最严密的一种模式。它的特点就是通过客户端的后台服务器,与服务提供商的认证服务器进行互动,而且这种场景很常见,是toc场景下使用最多的OAuth授权模式,下面以微信扫码登陆知识星球为例具体说明:

  1. 知识星球要求用户给予授权可以理解为点击微信登陆,浏览器弹出微信的二维码登录页面,扫码后微信APP上会得到一个授权许可页面
  2. 知识星球用户在微信APP的授权许可页面点击了同意用微信登录知识星球,随即微信授权服务器生成授权码并使得浏览器上的扫码登陆页面跳转到知识星球的微信登录链接(这个链接在开发者在微信开放平台注册应用时就要提前填写好)并通过在这个链接上追加URL参数的方式传把授权码给到了知识星球平台
  3. 知识星球平台将接收到的微信登录授权码作为参数,后台调用微信认证服务器的获取令牌接口
  4. 微信认证服务器验证授权码通过,返回令牌给知识星球平台
  5. 知识星球平台紧接着使用令牌访问微信的资源服务器,获取微信登录用户的微信数据(昵称,ID,甚至步数等)
  6. 微信资源服务器验证令牌通过,返回用户的微信数据(昵称,ID,甚至步数等)给知识星球平台,知识星球平台根据用户的微信ID建立会话,到此就微信登录成功了,知识星球平台一般就会跳转到首页,还会在首页上展示用户的微信昵称头像等个人标识。

3.2 隐藏式(implicit)

隐藏式,又称简化模式(implicit grant type),不通过第三方应用程序的服务器,而是直接在浏览器中向认证服务器申请令牌,跳过了授权码这个步骤,因此得名。所有步骤可以在浏览器中完成,令牌对访问者是可见的。

和授权码模式不一样的是,客户端引导用户打开服务提供商认证页面,通过扫码或用户名密码认证后,跳转到指定的客户端系统页面,并直接在URL参数上传递访问令牌,省去了通过授权码再去获取令牌的过程。

3.3 密码式(password)

密码模式中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向服务商提供商索要授权。

在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。

3.4 客户端凭证(client credentials)

客户端模式指客户端以自己的名义,而不是以用户的名义,向服务提供商进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求服务提供商提供服务,其实不存在授权问题。

四、参考

  1. 理解OAuth 2.0, 阮一峰, 2014.5
  2. OAuth 2.0 的一个简单解释, 阮一峰, 2019.04

一个通用的CloseableHttpClient工厂类

2023年1月1日 00:00

一个CloseableHttpClient工厂,基于java知名开源库apache-httpclient,能够忽略SSL,并且超时和状态异常时可以重试

<dependencies>    <dependency>        <groupId>org.apache.httpcomponents</groupId>        <artifactId>httpclient</artifactId>        <version>4.5.9</version>    </dependency>    <dependency>        <groupId>org.apache.httpcomponents</groupId>        <artifactId>httpmime</artifactId>        <version>4.5.9</version>    </dependency></dependencies>
package util;import lombok.extern.slf4j.Slf4j;import org.apache.http.ConnectionClosedException;import org.apache.http.HttpResponse;import org.apache.http.NoHttpResponseException;import org.apache.http.client.CookieStore;import org.apache.http.client.ServiceUnavailableRetryStrategy;import org.apache.http.conn.ConnectTimeoutException;import org.apache.http.conn.ssl.NoopHostnameVerifier;import org.apache.http.conn.ssl.SSLConnectionSocketFactory;import org.apache.http.impl.client.BasicCookieStore;import org.apache.http.impl.client.CloseableHttpClient;import org.apache.http.impl.client.HttpClients;import org.apache.http.protocol.HttpContext;import org.apache.http.ssl.SSLContextBuilder;import javax.net.ssl.SSLContext;import javax.net.ssl.SSLHandshakeException;import java.net.SocketTimeoutException;@Slf4jpublic class HttpClientUtil {    public static CloseableHttpClient createSSLClientDefault() {        try {            SSLContextBuilder sslContextBuilder = new SSLContextBuilder();            SSLContext sslContext = sslContextBuilder.loadTrustMaterial(null, (chain, authType) -> true).build();            SSLConnectionSocketFactory ssl = new SSLConnectionSocketFactory(                    sslContext,                    new String[]{"TLSv1", "TLSv1.1", "TLSv1.2"},                    null,                    SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);            return HttpClients.custom()                    .setSSLSocketFactory(ssl)                    .setRetryHandler((e, executionCount, context) -> {                        if (executionCount <= 20) {                            if (e instanceof NoHttpResponseException                                    || e instanceof ConnectTimeoutException                                    || e instanceof SocketTimeoutException                                    || e instanceof SSLHandshakeException) {                                log.info("{} 异常, 重试 {}", e.getMessage(), executionCount);                                return true;                            }                        }                        return false;                    })                    .setServiceUnavailableRetryStrategy(new ServiceUnavailableRetryStrategy() {                        @Override                        public boolean retryRequest(HttpResponse response, int executionCount, HttpContext context) {                            int statusCode = response.getStatusLine().getStatusCode();                            if (statusCode != 200) {                                if (executionCount <= 20) {                                    log.info("{} 状态码异常, 重试 {}", statusCode, executionCount);                                    return true;                                }                            }                            return false;                        }                        @Override                        public long getRetryInterval() {                            return 0;                        }                    })                    .build();        }        catch (Exception e) {            throw new RuntimeException(e);        }    }}

现在大学编程课的课件里,惊现二十年前的回忆

作者崔话记
2025年11月6日 20:20

  几天前,有个老同学微信问我代码问题,是老同学家上大学的娃的作业里的题,说运行出错让我看看。其实我挺没底,因为编程语言那么多,我就知道这么一丁点JS,要是问的是JS之外的,大概率我也不懂。截图发过来了,我一看是SQL,这是一种用于数据库的查询语言,这个我还略知一二,于是帮其修正了代码中的问题。

老同学微信问我代码问题

  后面我有说现在ai基本都能解答这类问题了,对方说娃一到交作业的时候豆包都得干冒烟,可能这题没有问ai或者没得到正确回答。

  那个代码的问题本身很简单,没什么好说,激起了我思绪小浪花的是,随问题发我的课件,那是大学编程课老师的课件,我从中看到一些复古的截图,尘封二十年的记忆被唤醒了。

  下面这个图里面的元素,Windows XP + IE6,当年可红火了,陪伴过我好几年,也折腾过我好几年。80后应该熟悉,90后也可能用过,00后大概不太会接触到。

课件里的截图

  老师的这个课件,可能是压箱底的吧,20多年了也不换个截图。软件技术的发展太快了,名副其实的日新月异,大学的编程课往往会比较滞后,老师教的原理没问题,但实操的往往不是当前市面上的流行产品版本,可能是老师们也偷懒图省事吧。在大学里学C/C++的还好,而学Asp.Net,Delphi的八成是学的时候就已经过时了。

  但是编程很适合自学,很多动手能力强的人,在学校的时候就已经熟练使用流行的编程语言和编程工具了。

  想学编程的人,还是推荐网上找教程,不知道从哪开始的可以先找B站的视频看看,说不定看了就放弃了这个可怕的想法呢。

是爱情,还是情绪价值?

作者崔话记
2025年11月3日 21:25

  有一种很类似爱情,甚至和爱情难以分辨的东西,我们现在都叫做“情绪价值”。爱情太难定义了,“情绪价值”就清晰的多了,可以像商品一样定量分析,有价值,可交换,可比较。大多数人所爱的爱情,约等于对象间的情绪价值。略微有一点点区别的,可能是:爱情里的情绪价值提供,更加双向愉悦,更少功利因素。

  有没有纯粹的爱情,很多人都会怀疑吧,甚至有没有爱情,都有很多人不信。作为四十加的中年人,再奢谈爱情,通常是极不现实的,能够和平共处搭伙过日子就已经不错了。四十多岁的时候,会不会真正再爱另一个人呢?有当然是有,但是少。首先要相信爱情,信则有,不信则无,这有点类似宗教信仰里的神了。

  我得感谢我生命里遇到的有缘人,给予过我关于爱情的享受,即使离开也没有破灭我对爱情的信仰。虽然我也不知道爱情的定义是什么。爱情本来也不是每个人都能拥有,有实力的人靠实力,普通人更多的时候靠运气,就像在海边玩耍的小孩,偶尔幸运捡到漂亮的贝壳。我属于幸运小孩之一。

  中年比起青年时期,看待爱情的态度,还是有显著变化的。我年轻的时候,不太会想两个人是否真的适合长期共处,也不会想以后会怎样,因为我狂妄觉得我都能搞定。中年了,就会认真想想这两个问题,在考虑以后的几种可能情况之后,觉得自己可以承受所想到的不同结局而不后悔。需要说明的是,不后悔不等于不会痛苦。一旦真正动过情,再失去的时候是不可能不痛苦的,只是每个人的承受能力和修复方式不同。虽然你有中年人的阅历,但是不要把中年人的患得患失和算计过多夹杂到感情里面,理性的控制风险,但不是市侩的算计斤两。

  相熟的朋友一起,也许会讨论社会热点,讨论各自的家庭和工作,或者吃喝玩乐,但是几乎不会讨论爱情这个话题,因为这个话题太难讨论了,每个人对自己的爱情观可能都说不清楚。但是在网络平台上,还是很容易见到深入讨论的。现实的朋友中,大家更关注八卦一些。八卦谁不喜欢呢。

  年轻的时候,似乎没怎么听过“情绪价值”这个说法,现在已经是所有的亲密关系里面最重要的关键词之一了,一大半的矛盾可能都与情绪价值的供需矛盾有关。如何向别人提供情绪价值,如何向别人提出情绪价值的需求,都是有讲究的,都是可通过学习提升的。但是有几个人是爱学习的呢?这一代的年轻人比上一代强多了,对自己和对他人的情绪价值,都更注重了。

  爱情不等于情绪价值,但没有情绪价值,就不算爱情。不论在爱情中,还是其它人际关系中,都不要过于索取情绪价值,也不要过于吝啬提供情绪价值。

杨绛的《杂忆与杂写》,跟博客的风格有点像

作者崔话记
2025年10月26日 18:42

  可能是看到某些博友对于博客中记录生活的感悟,使我想到杨绛的《我们仨》,我原以为写一家三口的回忆文应该与现在写博客记录生活有很多相似,但看了一部分之后,并不是我以为的那样。《我们仨》篇幅更长、文学性更强,很多地方的情感抒发也更沉重,不像写博客那么轻快。《杂忆与杂写》则和博客的风格更接近,篇幅较短,记录生活中某些有意思的瞬间或某个印象深刻的人,范围也比较大。

  《我们仨》目前还只看了很少一部分,里面印象较深的是钱钟书对家里杂事的一窍不通,家里的门坏了、灯坏了之类,一律告诉老婆,老婆就会跟他说“不要紧,我来修”。

  《杂忆与杂写》目前也只看了很少一部分,印象最深的是其中讲杨绛的小妹杨必小时候的趣事,一个几岁小孩子有模有样的模仿大人语气说话逗得大人捧腹。

  《干校六记》还没开始翻,但我猜应该会很好看,就像我会喜欢《浮生六记》一样。

  里面有些趣事是每个普通家庭里面都会发生的事情,我也经历过,但我没有写,也写不了那么好,更大的问题在于懒惰,既有身体的懒惰,也有思想的懒惰。身体的懒惰就是很多时候吃饱了懒得动,睡下了懒得起,或者得闲了刷手机或玩游戏。思想的懒惰就是很多时候没有下决心写,要费一些心力去回忆过去发生的事情,要费一些心力去组织通顺的语言,写作也不比做初中数学题容易。事情发生的当时,没有一冲动就写下来,等事后多日甚至多年,再要写的概率就很小了,我的症状就是这样。

  夸一下深圳图书馆的服务,可以在公众号网页上查书借书,预借之后以灵活的方式拿到书。我今年用过两种借书方式了,一是网上借书+快递到家,二是网上借书+自助书柜取书。方式一可以足不出户,但是要花几块钱运费,方式二可以不用去图书馆里找书,只需要像取快递那样就行,但是只能在深图那边才有。还书就不用拿到总馆去了,在哪个分馆都可以还书。

《我们仨》

《杂忆与杂写》

《干校六记》

  还没怎么读,还书期就到了,赶紧在公众号上续期了😀,希望接下来的半个月里再读几个章节哈哈。

❌