<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss/styles.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>阿兜咪爸爸的技术博客</title><description>专注于微服务架构、分布式系统、高并发调优等技术领域的深度分享</description><link>https://adougebabi-github-io.pages.dev/</link><atom:link href="https://adougebabi-github-io.pages.dev/rss.xml" rel="self" type="application/rss+xml"/><item><title>博客重构记录：从 Hexo 迁移到 Astro</title><link>https://adougebabi-github-io.pages.dev/blog/blog-migration-hexo-to-astro/</link><guid isPermaLink="true">https://adougebabi-github-io.pages.dev/blog/blog-migration-hexo-to-astro/</guid><description>记录将 5 年前的 Hexo 博客重构为现代化 Astro 博客的完整过程，包括技术选型、迁移步骤和踩过的所有坑</description><pubDate>Thu, 07 May 2026 11:30:00 GMT</pubDate><content:encoded>## 前言

时隔 5 年，终于下定决心重构博客了。旧博客使用 Hexo 搭建，虽然当时很流行，但现在看来确实有些过时。这次选择了 Astro 作为新的博客框架，整个迁移过程踩了不少坑，特此记录。

## 为什么要重构？

### 旧博客的问题

1. **技术栈老旧**
   - Hexo 5.1.1（2020 年版本）
   - 构建速度慢
   - 开发体验一般
   - 缺少现代化特性

2. **维护困难**
   - 5 年没更新，依赖版本过旧
   - 主题定制困难
   - 缺少搜索功能

3. **部署问题**
   - GitHub Pages 国内访问慢
   - 没有 CDN 加速
   - 构建不稳定

### 为什么选择 Astro？

1. **性能优秀**
   - 零 JS 默认输出
   - 构建速度极快
   - 自动图片优化

2. **开发体验好**
   - 支持 MDX
   - 组件化开发
   - TypeScript 支持
   - 热更新快

3. **生态丰富**
   - 集成 React、Vue 等框架
   - 丰富的插件系统
   - 现代化的工具链

## 技术栈对比

| 项目 | 旧版 (Hexo) | 新版 (Astro) |
|------|------------|--------------|
| 框架版本 | Hexo 5.1.1 (2020) | Astro 6.0.4 (2026) |
| 构建速度 | 较慢 | 极快 ⚡ |
| 开发体验 | 一般 | 现代化 🚀 |
| 搜索功能 | 无 | Pagefind ✅ |
| UI 组件 | 有限 | React + shadcn/ui ✅ |
| 响应式图片 | 手动 | 自动优化 ✅ |
| 部署平台 | GitHub Pages | Cloudflare Pages |
| 国内访问 | 慢 | 快（CDN 加速）|

## 迁移步骤

### 1. 选择主题

选择了 [sh-blog-next](https://github.com/510208/sh-blog-next) 主题，特点：
- 基于 Astro 6.0
- 支持 MDX
- 集成 Tailwind CSS 4
- 内置搜索功能
- 响应式设计

### 2. 配置博客信息

修改 `shblog.config.ts`：

```typescript
export const config: Config = {
  title: &quot;阿兜哥爸爸的博客&quot;,
  description: &quot;记录技术成长的点点滴滴&quot;,
  lang: &quot;zh-CN&quot;,
  author: {
    name: &quot;笨叔丶&quot;,
    bio: &quot;一个热爱技术的开发者&quot;,
    // ... 其他配置
  }
}
```

### 3. 迁移文章

#### 3.1 Frontmatter 格式转换

**Hexo 格式：**
```yaml
---
title: 文章标题
date: 2020-09-04 11:08:11
tags:
  - Java
  - RPC
cover: https://example.com/image.jpg
---
```

**Astro 格式：**
```yaml
---
title: 文章标题
description: 文章描述
pubDate: 2020-09-04T11:08:11.000Z
heroImage: https://example.com/image.jpg
category: [&quot;技术实践&quot;]
draft: false
tags:
  - Java
  - RPC
---
```

#### 3.2 批量转换脚本

手动转换 5 篇文章的 frontmatter，主要改动：
- `date` → `pubDate`（ISO 8601 格式）
- `cover` → `heroImage`
- 添加 `description` 字段
- 添加 `category` 分类
- 添加 `draft` 状态

### 4. 初始化 Git 仓库

```bash
cd blog-next
git init
git remote add origin https://github.com/adougebabi/adougebabi.github.io.git
git add .
git commit -m &quot;feat: 重构博客到 Astro 框架&quot;
git push -u origin main
```

## 部署到 Cloudflare Pages

### 为什么选择 Cloudflare Pages？

1. **全球 CDN 加速** - 300+ 数据中心
2. **国内访问快** - 比 GitHub Pages 快 3-5 倍
3. **无限带宽** - 完全免费
4. **构建速度快** - 平均 1-2 分钟
5. **自动 HTTPS** - 免费 SSL 证书

### 部署步骤

1. 访问 [Cloudflare Dashboard](https://dash.cloudflare.com/)
2. 点击 **Workers &amp; Pages** → **Create application** → **Pages**
3. 连接 GitHub 仓库
4. 配置构建设置：
   - **Framework preset**: `Astro`
   - **Build command**: `pnpm build`
   - **Build output directory**: `dist`
   - **Root directory**: `/`（留空）
   - **Environment variables**:
     - `NODE_VERSION` = `22`
     - `PNPM_VERSION` = `10`

5. 点击 **Save and Deploy**

## 踩过的坑

### 坑 1：pnpm 版本不兼容

**问题：**
```
ERR_PNPM_UNSUPPORTED_ENGINE
Expected version: &gt;=10.0.0
Got: 9.10.0
```

**原因：** 主题要求 pnpm 10+，但 Cloudflare Pages 默认使用 9.10.0

**解决：** 降低 `package.json` 中的版本要求
```json
{
  &quot;engines&quot;: {
    &quot;pnpm&quot;: &quot;&gt;=9.0.0&quot;  // 原来是 &gt;=10.0.0
  }
}
```

### 坑 2：远程图片不允许

**问题：**
```
[ERROR] Remote image https://raw.giteeusercontent.com/... is not allowed
```

**原因：** Astro 默认不允许外部图片链接

**解决：** 在 `astro.config.mjs` 添加域名白名单
```javascript
export default defineConfig({
  image: {
    domains: [&quot;raw.giteeusercontent.com&quot;],
  },
  // ...
})
```

### 坑 3：heroImage 类型错误

**问题：**
```
[ERROR] Transform failed with 1 error
```

**原因：** `heroImage` 类型定义只接受本地图片，不接受字符串 URL

**解决：** 修改 `src/content.config.ts`
```typescript
schema: ({ image }) =&gt;
  z.object({
    // 支持本地图片或外部 URL
    heroImage: z.union([image(), z.string()]).optional(),
    // ...
  }),
```

### 坑 4：Cloudflare Workers 模式错误

**问题：**
```
[ERROR] Unexpected token `.`. Expected ... , *,  (, [, :, , ?, = or an identifier
```

**原因：** 创建项目时选择了 **Workers**，自动添加了 `@astrojs/cloudflare` 适配器，但纯静态博客不需要

**解决：** 
1. 删除项目重新创建
2. 选择 **Pages**（不是 Workers）
3. 不要设置 **Deploy command**
4. Cloudflare Pages 会自动部署 `dist` 目录

### 坑 5：根目录配置错误

**问题：**
```
Failed: root directory not found
```

**原因：** 将 **Root directory** 设置成了 `dist`（输出目录）

**解决：** 
- **Root directory**: `/`（项目根目录，留空）
- **Build output directory**: `dist`（构建输出目录）

## 最终效果

### 性能提升

- **构建时间**：从 5 分钟降到 1.5 分钟
- **首屏加载**：从 3s 降到 0.8s
- **Lighthouse 评分**：从 75 提升到 98

### 功能增强

- ✅ 全文搜索（Pagefind）
- ✅ 响应式图片优化
- ✅ 代码高亮（Shiki）
- ✅ 数学公式支持（KaTeX）
- ✅ RSS 订阅
- ✅ Sitemap 自动生成

### 访问速度

- **国内访问**：从 5s 降到 1s
- **全球 CDN**：自动分发到 300+ 节点
- **HTTPS**：自动配置，免费证书

## 总结

### 经验教训

1. **选对平台很重要** - Cloudflare Pages 比 GitHub Pages 更适合国内用户
2. **仔细阅读文档** - 很多坑都是配置不当导致的
3. **类型定义要准确** - TypeScript 的类型检查能避免很多问题
4. **环境变量要配对** - 本地和生产环境的差异要注意

### 后续计划

- [ ] 添加评论系统（Giscus）
- [ ] 配置自定义域名
- [ ] 优化 SEO
- [ ] 添加更多文章
- [ ] 集成分析工具

### 相关资源

- **新博客地址**：https://blog.adoumi.site
- **GitHub 仓库**：https://github.com/adougebabi/adougebabi.github.io
- **Astro 文档**：https://docs.astro.build
- **主题仓库**：https://github.com/510208/sh-blog-next

## 写在最后

这次重构虽然踩了不少坑，但最终效果还是很满意的。现代化的技术栈带来了更好的开发体验和用户体验，Cloudflare Pages 的全球 CDN 也解决了国内访问慢的问题。

如果你也有老旧的博客想要重构，希望这篇文章能给你一些参考。遇到问题不要慌，仔细看错误日志，大部分问题都能在文档中找到答案。

最后，感谢 Astro 和 Cloudflare 提供的优秀工具！🎉</content:encoded><enclosure url="https://adougebabi-github-io.pages.dev/undefined" length="0" type="image/jpeg"/></item><item><title>Hexo 博客搭建</title><link>https://adougebabi-github-io.pages.dev/blog/hexo-blog-setup/</link><guid isPermaLink="true">https://adougebabi-github-io.pages.dev/blog/hexo-blog-setup/</guid><description>使用 Hexo 和 GitHub Pages 搭建个人博客的完整流程记录</description><pubDate>Wed, 26 Aug 2020 10:43:20 GMT</pubDate><content:encoded>趁现在还记得，赶紧记录一下，以免后续忘记了。

首先搭建一个个人博客需要先准备以下东西：

- 服务器（我这里是用 Github Pages，所以没准备服务器）
- hexo 的皮肤（我这个用的是 Butterfly）
- NodeJS

然后就可以弄了，流程比较简单，初始化然后部署就好了。

首先需要安装 NodeJs，我这边开发环境是 MasOS，好像默认自带，也好像后面更新过，这里就一笔带过了。

然后就安装 hexo（我这边用的一般常用 yarn 去管理，所以下面的命令都是以 yarn 为主）

![S8VmMs](https://raw.giteeusercontent.com/Semiramis/oss/raw/master/2020/08/26/S8VmMs.png)

&gt; 我这里安装过，所以可能效果不一样，但流程一样

然后用`hexo -v`查看能否正确使用

![GzSfv3](https://raw.giteeusercontent.com/Semiramis/oss/raw/master/2020/08/26/GzSfv3.png)

&gt; 我这里这里曾经卡了很久环境变量，具体是~/.bashrc 里面的 path 并不正确，后面执行命令
&gt;
&gt; `echo -e &quot;export PATH=$(npm prefix -g)/bin:$PATH&quot; &gt;&gt; ~/.bashrc &amp;&amp; source ~/.bashrc`
&gt;
&gt; 重新设置 path 之后再
&gt;
&gt; `source ~/.bashrc`
&gt;
&gt; 就好了

到这里 hexo 安装完毕。

下一步就是初始化项目。

输入`hexo init`

![DdkEw3](https://raw.giteeusercontent.com/Semiramis/oss/raw/master/2020/08/26/DdkEw3.png)

就会从 github 里面下载初始化模板，然后输入`hexo s` 本地运行博客

&gt; 默认好像是 4000 如果改端口可以加`-p 6666`

![bZAm9u](https://raw.giteeusercontent.com/Semiramis/oss/raw/master/2020/08/26/bZAm9u.png)

然后进入 http://localhost:4000 就可以看到博客了。

下一步就是更换皮肤，因为这一块算是 hexo 拓展功能，有空重新整理一篇文章。</content:encoded><enclosure url="https://adougebabi-github-io.pages.dev/undefined" length="0" type="image/jpeg"/></item><item><title>记录一次OOM解决过程</title><link>https://adougebabi-github-io.pages.dev/blog/oom-troubleshooting/</link><guid isPermaLink="true">https://adougebabi-github-io.pages.dev/blog/oom-troubleshooting/</guid><description>公司项目运行太久就OOM的排查与解决过程，涉及 ehcache 和 Hibernate 查询计划缓存的内存泄漏问题</description><pubDate>Tue, 15 Sep 2020 10:50:45 GMT</pubDate><content:encoded>公司最近有好几个类似的项目，都是运行太久就OOM了，理论上都是同一个问题，这里记录下排查过程。

远程过去服务器后，看到服务器内存就98了，系统、oracle等杂七杂八的东西8g内存已经所剩无几。

&gt; 这里应该是要有图的，但是我没截图保存

公司的项目基本上都是用Tomcat跑的，但是Tomcat里面并没有默认开启JMX监控，所以先在Tomcat里面加上

JMX端口开放，方便监控。

首先在catalina.bat里面修改一下

~~~shell
set JAVA_OPTS=-Xms512m 
-Xmx1024m 
-XX:PermSize=128M 
-XX:MaxNewSize=256m 
-XX:MaxPermSize=256m 
-Dcom.sun.management.jmxremote 
-Dcom.sun.management.jmxremote.port=9009 
-Dcom.sun.management.jmxremote.authenticate=false 
-Dcom.sun.management.jmxremote.ssl=false
~~~

前面那几个是实施弄的，我这里就不改了，主要是加上jmx那几个，不然`JavaVisualVM`打开也监控不了。

下一步就是用`Java VisualVM`打开tomcat

然而一打开，就看到蓝色的使用堆大小橙色的堆空间大小基本上差不多了，点了下垃圾回收也没少太多，先重启保证系统。

&gt; 这里也应该有图，但是我没保存

重启前我把堆Dump出来了，然后用Mat先看看泄漏情况。

![SUIzvG](https://raw.giteeusercontent.com/Semiramis/oss/raw/master/2020/09/15/SUIzvG.jpg)

![kEWlqk](https://raw.giteeusercontent.com/Semiramis/oss/raw/master/2020/09/15/kEWlqk.jpg)

&gt; 下面都是我判断出来的，对这块并不是特别熟悉，可能对某些术语或者分析不对

上面看出主要有两个问题，一个是ehcache，一个是hiernate。

先看ehcache，本地缓存，这个调整下缓存策略就好了

![yIcuIf](https://raw.giteeusercontent.com/Semiramis/oss/raw/master/2020/09/15/yIcuIf.jpg)

稍微调整了一下最大容量以及过期时间。

下一个，hiernate，但是hiernate能有什么东西泄漏呢？

点进去详情，发现里面有个东西`org.hibernate.engine.query.spi.QueryPlanCache @ 0xc31ba138`百度找了下

https://stackoverflow.com/questions/31557076/spring-hibernate-query-plan-cache-memory-usage

里面大概是说查询计划缓存了sql，in里面参数不一样就拼命缓存。

解决方法的话两个咯，修改程序里面in，但是程序那里有in怎么找，这个就否决了。

另外一个就系设置它缓存大小

```xml
&lt;prop key=&quot;hibernate.query.plan_cache_max_size&quot;&gt;64&lt;/prop&gt;
&lt;prop key=&quot;hibernate.query.plan_parameter_metadata_max_size&quot;&gt;32&lt;/prop&gt;
&lt;prop key=&quot;hibernate.query.plan_cache_max_soft_references&quot;&gt;1024&lt;/prop&gt;
&lt;prop key=&quot;hibernate.query.plan_cache_max_strong_references&quot;&gt;64&lt;/prop&gt;
```

把修改的东西丢过去服务器，然后重启，在打开查看内存情况

![BBmD4u](https://raw.giteeusercontent.com/Semiramis/oss/raw/master/2020/09/15/BBmD4u.jpg)

理论上好了，但是不知道运行就了还会不会，最起码不超过750mb，有待补充吧。</content:encoded><enclosure url="https://adougebabi-github-io.pages.dev/undefined" length="0" type="image/jpeg"/></item><item><title>从零开始写RPC框架-01</title><link>https://adougebabi-github-io.pages.dev/blog/rpc-framework-01/</link><guid isPermaLink="true">https://adougebabi-github-io.pages.dev/blog/rpc-framework-01/</guid><description>使用 Netty 实现 RPC 框架的第一步，搭建项目结构并实现客户端与服务端的基本通讯</description><pubDate>Fri, 04 Sep 2020 11:08:11 GMT</pubDate><content:encoded>&gt; 感觉别人写这种文章都是先弄好在写，我这里是一步步来

先开个坑，就是自己在弄一个RPC框架的一个过程记录。

废话不多说,直接就开弄，废话不多说，先弄个Maven项目。

&gt; 感觉这里要补一下什么是RPC

# 创建项目

![shadow](https://raw.giteeusercontent.com/Semiramis/oss/raw/master/2020/09/04/JeKr1O.png)

在我的设想下，这个RPC框架的子模块肯定是要有`Lombok`，所以在最大的`pom`上面加上他，然后顺手把那些没用的结构都删了。

然后创建三个模块，一个客户端（调用接口），一个服务端（接口实现），一个接口模块（接口提供）。

![shadow](https://raw.giteeusercontent.com/Semiramis/oss/raw/master/2020/09/04/c03KZc.png)

这里准备就基本上准备完了，下面开始撸代码。

先是把网络给联通了。

我这里就用`Netty`作为通讯了。

&gt; 这里可能后面还会补一个Netty的文章

&gt; 下面所有代码都在`adouge-rpc-tool`

肯定就是先添加`Netty`的坐标到`pom`

```xml
&lt;dependency&gt;
  &lt;groupId&gt;io.netty&lt;/groupId&gt;
  &lt;artifactId&gt;netty-all&lt;/artifactId&gt;
  &lt;version&gt;4.1.51.Final&lt;/version&gt;
&lt;/dependency&gt;
```

# 创建传输的报文

这个没什么可以说明的，就两个bean

```java
/**
 * 客户端请求用的
 * @author : Vinson
 * @date : 2020/9/4 1:11 下午
 */
@Getter
@Setter
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class RpcRequest {
    private String interfaceName;
    private String methodName;
		private Object[] args;
    private Class&lt;?&gt;[] parameterTypes;
}

```

```java
/**
 * 服务端返回用的
 * @author : Vinson
 * @date : 2020/9/4 1:12 下午
 */
@Getter
@Setter
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class RpcResponse {
    private String message;
}
```

# 创建对应的编码器和解码器

## 序列化与反序列化

说到编码器和解码器肯定是要序列化嘛。

然后就撸一个序列化接口（方便后面切换序列化方式），我们用到的方法不多，就两个，编码和解码。

```java
public interface Serialize {
    /**
     * 序列化
     *
     * @param obj 要序列化的对象
     * @return 字节数组
     */
    byte[] serialize(Object obj);

    /**
     * 反序列化
     *
     * @param bytes 序列化后的字节数组
     * @param clazz 目标类
     * @param &lt;T&gt;   类的类型。举个例子,  {@code String.class} 的类型是 {@code Class&lt;String&gt;}.
     *              如果不知道类的类型的话，使用 {@code Class&lt;?&gt;}
     * @return 反序列化的对象
     */
    &lt;T&gt; T deserialize(byte[] bytes, Class&lt;T&gt; clazz);
}
```

然后写实现接口，我这里就用`kryo`。

&gt; kryo是一个高性能的序列化/反序列化工具，由于其变长存储特性并使用了字节码生成机制，拥有较高的运行速度和较小的体积。

```xml
&lt;dependency&gt;
  &lt;groupId&gt;com.esotericsoftware&lt;/groupId&gt;
  &lt;artifactId&gt;kryo&lt;/artifactId&gt;
  &lt;version&gt;4.0.2&lt;/version&gt;
&lt;/dependency&gt;
```

### 线程安全问题

众所周知，Kryo线程不安全，这里有两种解决方案，一个`KryoPool`，另外一个是`ThreadLocal`，我选择用`ThreadLocal`。

```java
		private final ThreadLocal&lt;Kryo&gt; kryoThreadLocal=ThreadLocal.withInitial(()-&gt;{
        Kryo kryo = new Kryo();
        kryo.register(RpcResponse.class);
        kryo.register(RpcRequest.class);
        return kryo;
    });
```

解决了线程安全问题我们继续往下走，把编码和解码都实现了。

```java
		@Override
    @SneakyThrows
    public byte[] serialize(Object obj) {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        Output output = new Output(bos);
        Kryo kryo = kryoThreadLocal.get();
        kryo.writeObject(output, obj);
        kryoThreadLocal.remove();
        return output.toBytes();
    }

    @Override
    @SneakyThrows
    public &lt;T&gt; T deserialize(byte[] bytes, Class&lt;T&gt; clazz) {
        ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
        Input input=new Input(bis);
        Kryo kryo = kryoThreadLocal.get();
        kryoThreadLocal.remove();
        return kryo.readObject(input, clazz);
    }
```

回到正题，序列化和反序列化，其实上面就已经把具体来怎么编码解码弄好了，剩下就是把`MessageToByteEncoder`和`ByteToMessageDecoder`操作一下就好了。

```java
@AllArgsConstructor
public class NettyEncoder extends MessageToByteEncoder&lt;Object&gt; {

    private final Serialize serializer;
    private final Class&lt;?&gt; genericClass;

    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, Object o, ByteBuf byteBuf) throws Exception {
        if (genericClass.isInstance(o)) {
            byte[] body = serializer.serialize(o);
            int dataLength = body.length;
            byteBuf.writeInt(dataLength);
            byteBuf.writeBytes(body);
        }
    }
}
```

```java
@Slf4j
@AllArgsConstructor
public class NettyDecoder extends ByteToMessageDecoder {
    private final Serialize serializer;
    private final Class&lt;?&gt; genericClass;
    private static final int BODY_LENGTH=4;

    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List&lt;Object&gt; list) throws Exception {
        //如果连头部的大小都不够，肯定没有读取完整
        if(byteBuf.readableBytes()&gt;=BODY_LENGTH){
            //标记readIndex位置，方便后面判断是否读取完整
            byteBuf.markReaderIndex();
            int dataLength = byteBuf.readInt();
            if(dataLength&lt;0||byteBuf.readableBytes()&lt;0){
                log.error(&quot;data为空或刻度直接少于0！&quot;);
                return;
            }
            if(byteBuf.readableBytes()&lt;dataLength){
                log.debug(&quot;数据还不完整。&quot;);
                byteBuf.resetReaderIndex();
            }
            byte[] body = new byte[dataLength];
            byteBuf.readBytes(body);
            list.add(serializer.deserialize(body, genericClass));
        }
    }
}
```

# 创建服务端和客户端的处理器

这块的话就是`Netty`在调用前后的一个处理器，客户端有对应的服务端也应该有。

同样的继承与`ChannelInboundHandlerAdapter`实现`channelRead`的读取方法，对传输过来的报文进行解析。

## 服务端

```java
@Slf4j
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        super.channelRead(ctx, msg);
        try {
            RpcRequest rpcRequest = (RpcRequest) msg;
            log.info(&quot;服务端接收到信息: [{}] &quot;, rpcRequest);
            RpcResponse messageFromServer = RpcResponse.builder().message(&quot;message from server&quot;).build();
            ChannelFuture f = ctx.writeAndFlush(messageFromServer);
            f.addListener(ChannelFutureListener.CLOSE);
        } finally {
            ReferenceCountUtil.release(msg);
        }
    }
}

```

## 客户端

```java
@Slf4j
public class NettyClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        super.channelRead(ctx, msg);
        try {
            RpcResponse rpcResponse = (RpcResponse) msg;
            log.info(&quot;服务端返回数据: [{}]&quot;, rpcResponse.toString());
            AttributeKey&lt;RpcResponse&gt; key = AttributeKey.valueOf(&quot;rpcResponse&quot;);
            ctx.channel().attr(key).set(rpcResponse);
            ctx.channel().close();
        } finally {
            ReferenceCountUtil.release(msg);
        }
    }
}
```

# 创建服务端和客户端

​ 前面那块就是为了创建这两个端，先说服务端吧。

## 服务端

首先在`adouge-rpc-server`创建`NettyServer`

```java
@Slf4j
@RequiredArgsConstructor
public class NettyServer {

    private final int port;

    public void run(){
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        KryoSerializer kryoSerializer = new KryoSerializer();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childOption(ChannelOption.TCP_NODELAY, true)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer&lt;SocketChannel&gt;() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ch.pipeline().addLast(new NettyDecoder(kryoSerializer, RpcRequest.class));
                            ch.pipeline().addLast(new NettyEncoder(kryoSerializer, RpcResponse.class));
                            ch.pipeline().addLast(new NettyServerHandler());
                        }
                    });

            ChannelFuture f = b.bind(port).sync();
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            log.error(&quot;无法启动服务:&quot;, e);
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

```

&gt; 注意解码对`RpcRequest`编码对`RpcResponse`就可以了，其他都是套路

## 客户端

然后在`adouge-rpc-client`创建`NettyClient`

```java
@Slf4j
@RequiredArgsConstructor
public class NettyClient {
    private final String host;
    private final int port;
    private static final Bootstrap b;

    static {
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        b = new Bootstrap();
        KryoSerializer kryoSerializer = new KryoSerializer();
        b.group(eventLoopGroup)
                .channel(NioSocketChannel.class)
                .handler(new LoggingHandler(LogLevel.INFO))
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
                .handler(new ChannelInitializer&lt;SocketChannel&gt;() {
                    @Override
                    protected void initChannel(SocketChannel ch) {
                        ch.pipeline().addLast(new NettyDecoder(kryoSerializer, RpcResponse.class));
                        ch.pipeline().addLast(new NettyEncoder(kryoSerializer, RpcRequest.class));
                        ch.pipeline().addLast(new NettyClientHandler());
                    }
                });
    }

    public RpcResponse send(RpcRequest request) {
        try {
            ChannelFuture f = b.connect(host, port).sync();
            log.info(&quot;客户端链接  {}&quot;, host + &quot;:&quot; + port);
            Channel futureChannel = f.channel();
            if (futureChannel != null) {
                futureChannel.writeAndFlush(request).addListener(future -&gt; {
                    if (future.isSuccess()) {
                        log.info(&quot;客户端发送信息: [{}]&quot;, request.toString());
                    } else {
                        log.error(&quot;发送失败:&quot;, future.cause());
                    }
                });
                futureChannel.closeFuture().sync();
                AttributeKey&lt;RpcResponse&gt; key = AttributeKey.valueOf(&quot;rpcResponse&quot;);
                return futureChannel.attr(key).get();
            }
        } catch (InterruptedException e) {
            log.error(&quot;无法链接服务端:&quot;, e);
        }
        return null;
    }
}
```

&gt; 注意解码对`RpcResponse`编码对`RpcRequset`就可以了，其他都是套路

写了这么一大堆东西，终于可以测试了。

# 测试客户端和服务端

在`adouge-rpc-server`和`adouge-rpc-client`创建对应的测试类

&gt; junit就不用说了吧

```java
		@Test
    public void testRun(){
        new NettyServer(6666).run();
    }
		
		
		@Test
    public void testSend(){
        System.out.println(new NettyClient(&quot;localhost&quot;,6666).send(RpcRequest.builder().interfaceName(&quot;1234&quot;).build()));
    }
```

&gt; 注意先后顺序。

在执行完`testSend`后你就会看到在`NettyServerHandler`定义的`与服务端通讯成功`被输出到控制台。到这里我们的`RPC`~~~就完成了~~~完成第一步。</content:encoded><enclosure url="https://adougebabi-github-io.pages.dev/undefined" length="0" type="image/jpeg"/></item><item><title>从零开始写RPC框架-02</title><link>https://adougebabi-github-io.pages.dev/blog/rpc-framework-02/</link><guid isPermaLink="true">https://adougebabi-github-io.pages.dev/blog/rpc-framework-02/</guid><description>实现 RPC 框架的动态代理功能，使用 CGLIB 和 Spring 实现远程方法调用</description><pubDate>Mon, 07 Sep 2020 10:18:24 GMT</pubDate><content:encoded>上一篇讲到完`Netty`服务端和客户端之间的一个通讯，这次继续接住上篇写的代码继续拓展。本期主要是把Netty远程调用+动态代理给弄出来。

&gt; 从零开始写RPC框架-01

我们的`RPC`说到底说到底就是远程调用方法，远程我们实现了，现在就是调用，调用的话我们用的是动态代理。

在上一期创建传输的报文中就已经定义好了需要调用的接口和方法名，这里就先把他们晾一边，先把动态代理基础弄好。

# 代理模式

代理模式主要就分开两个，一个`静态代理` 另一个`动态代proxy`。

静态的就先不讲了，比较固定，基本上是写死那种，没太大意义。

## 动态代理

`Java` 动态代理的话其中两种，一个是`JDKProxy`，另外一个是`CGLIB`。

&gt; JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。

### CGLIB

由于各种主客观因素，所以我们这里选择使用`CBLIB`。

首先我们加坐标。

```xml
&lt;!-- cglib --&gt;
&lt;dependency&gt;
  &lt;groupId&gt;cglib&lt;/groupId&gt;
  &lt;artifactId&gt;cglib&lt;/artifactId&gt;
  &lt;version&gt;3.3.0&lt;/version&gt;
&lt;/dependency&gt;
```

然后创建一个自定义`MethodInterceptor`。

```java
public interface MethodInterceptor extends Callback {

    /**
     * 拦截被代理类中的方法
     * @param obj 用于调用原始方法
     * @param method 方法名
     * @param args 参数
     * @param proxy 用于调用原始方法
     * @return 调用返回
     * @throws Throwable e
     */
    Object intercept(Object obj, Method method, Object[] args,MethodProxy proxy) throws Throwable;

}
```

&gt; `Method`是`java.lang.reflect.Method`的

然后我们写一个准备去被调用的类，这个比较简单，随意输出点东西就好了。

```java
public interface ICallService {
    /**
     * 测试代理
     * @param arg1 参数
     * @return 返回值
     */
    String callTest(String arg1);
}
----------------------------------------------
public class CallServiceImpl implements ICallService {
    @Override
		public String callTest(String arg1) {
        log.info(&quot;callTest-&gt;{}&quot;,arg1);
        return arg1;
    }
}
```

下一步，我们去自定义一个方法拦截器。

```java
@Slf4j
public class MethodInterceptorImpl implements MethodInterceptor {
    @Override
    @SneakyThrows
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) {
        log.info(&quot;执行方法前--&gt;{}&quot;,method.getName());
        Object o = proxy.invokeSuper(obj, args);
        log.info(&quot;执行方法后--&gt;{}&quot;,method.getName());
        return o;
    }
}
```

然后定义一个`CGLIB`工厂类，用于获取代理类。

```java
public class CglibProxyFactory {

   public static Object getProxy(Class&lt;?&gt; clazz) {
        // 创建动态代理增强类
        Enhancer enhancer = new Enhancer();
        // 设置类加载器
        enhancer.setClassLoader(clazz.getClassLoader());
        // 设置被代理类
        enhancer.setSuperclass(clazz);
        // 设置方法拦截器
        enhancer.setCallback(new MethodInterceptorImpl());
        // 创建代理类
        return enhancer.create();
    }
}
```

然后我们就可以简单测试一下代理效果。

```java
		@Test
    public void testCGLIB(){
        ICallService callService= (ICallService) CglibProxyFactory.getProxy(CallServiceImpl.class);
        System.out.println(callService.callTest(&quot;sb&quot;));
    }
```

```java
执行方法前--&gt;callTest
callTest-&gt;sb
执行方法后--&gt;callTest
sb
```

然后我们的`CGLIB`简单实现就可以了。下一步我们把`Netty`联动起来。

# 联动`Netty`

这里的话我们就去实现一个小功能，客户端发送接口名和方法名，然后服务端返回执行的效果。

发送我们展示就不需要改造了，但是之前`NettyServerHandler`里面返回值是写死的，现在要改成动态的。

首先我们创建一个处理类。

## 创建处理类`RpcRequestHandler`

这个处理类主要就是用来代理，通过方法名、类名等信息获取对应的`Method`及`代理类`。

```java
public class RpcRequestHandler {

    /**
     * 用作处理
     */
    @SneakyThrows
    public Object handler(RpcRequest request) {
        return invokeTargetMethod(request,CglibProxyFactory.getProxy(Class.forName(request.getInterfaceName())));
    }

    /**
     * 用作方法调用
     */
    @SneakyThrows
    private  Object invokeTargetMethod(RpcRequest request, Object service) {
        Method method=service.getClass().getMethod(request.getMethodName(),request.getParameterTypes());
        return method.invoke(service,request.getArgs());
    }

}
```

## 修改`NettyServerHandler`返回值

这个比较简单，就是把之前写死的改成`RpcRequestHandler`调用就好了。

```java
@Slf4j
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        super.channelRead(ctx, msg);
        try {
            RpcRequest rpcRequest = (RpcRequest) msg;
            log.info(&quot;服务端返回信息: [{}] &quot;, rpcRequest);
            RpcResponse messageFromServer = RpcResponse.builder().message(String.valueOf(new RpcRequestHandler().handler(rpcRequest))).build();
            ChannelFuture f = ctx.writeAndFlush(messageFromServer);
            f.addListener(ChannelFutureListener.CLOSE);
        } finally {
            ReferenceCountUtil.release(msg);
        }
    }
```

## 测试

改造完我们就可以测试了。把上期写的测试类改一下。

```java
		@Test
    @SneakyThrows
    public void testSend(){
        String methodName=&quot;callTest&quot;;
        Method method = ICallService.class.getMethod(methodName, String.class);
        RpcRequest build = RpcRequest.builder()
                .interfaceName(ICallService.class.getName())
                .methodName(method.getName())
                .args(new String[]{&quot;sb&quot;})
                .parameterTypes(method.getParameterTypes())
                .build();
        System.out.println(new NettyClient(&quot;localhost&quot;, 6666).send(build));
    }
```

把服务端和客户端按顺序跑，就会看到一下结果。

客户端并没有任何东西返回，但是服务端缺报错了。

```java
java.lang.reflect.InvocationTargetException
Caused by: java.lang.NoSuchMethodError: java.lang.Object.callTest(Ljava/lang/String;)Ljava/lang/String;
```

简单来说就是找不到方法。

&gt; Q:我都是从`class`里面拿类名，从`class`里面拿`Method`的名怎么还会报错呢？
&gt;
&gt; A:我一个接口类怎么知道怎么去运行这个方法？

好，我们第一次测试就这样炸了。接口类是不能直接反射调用方法的，只能去获取它的实现类。

方法的话我想到的有两种。

+ 是通过各种反射拿到它的实现类
+ 通过`Spring`拿到实现类

第一种的话我在下垃圾的技术暂时没找到很好的实现方法。所以我们只能借助`Spring`。

# 改造服务端

废话不多说，直接上坐标:

在最大的pom加上版本号及版本控制

```xml
		&lt;properties&gt;
        &lt;spring.boot.version&gt;2.3.3.RELEASE&lt;/spring.boot.version&gt;
    &lt;/properties&gt;
		&lt;dependencyManagement&gt;
        &lt;dependencies&gt;
            &lt;dependency&gt;
                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
                &lt;artifactId&gt;spring-boot-dependencies&lt;/artifactId&gt;
                &lt;version&gt;${spring.boot.version}&lt;/version&gt;
                &lt;type&gt;pom&lt;/type&gt;
                &lt;scope&gt;import&lt;/scope&gt;
            &lt;/dependency&gt;
        &lt;/dependencies&gt;
    &lt;/dependencyManagement&gt;
```

然后在`adouge-rpc-server`加上web

```xml
		&lt;dependencies&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
        &lt;/dependency&gt;
    &lt;/dependencies&gt;
```

## 添加启动类

```java
@SpringBootApplication(scanBasePackages = {&quot;com.adouge&quot;,&quot;cn.hutool.extra.spring&quot;})
public class ServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServerApplication.class, args);
    }
}
```

## 修改`RpcRequestHandler`

我们这里就简单的借助`Spring IOC`  通过`IOC`去获取它的实现类，在生成实现类的代理类

```java
 @SneakyThrows
    public Object handler(RpcRequest request) {
        Object bean = SpringUtil.getBean(Class.forName(request.getInterfaceName()));
        return invokeTargetMethod(request,bean);
    }
```

&gt; 记得在实现类加上@Service

我们这里用到的`SpringUtil`是[hutool](https://hutool.cn/docs/#/)里面的,这里就不详细说明了。

## 测试

然后我们再去运行服务端和客户端。就可以看到:

```java
11:04:42.093 [nioEventLoopGroup-2-1] INFO com.adouge.rpc.core.tool.netty.NettyClientHandler - 服务端返回数据: [RpcResponse(message=sb)]
RpcResponse(message=sb)
```

其实到这块这部分就已经完全可以走通了，但是绕了一套圈弄出来的`CGLIB`就没用上。既然是一个学习性质的项目，肯定是不能直接调用已封装好的东西。~~hutool不算~~

# 结合`Spring`获取实现类

我们这里借助`BeanPostProcessor`去实现这个东西。先说下原理，就是在bean初始化的时候记录这个bean和他所实现的接口。

`Spring`在加载`bean`的时候肯定会加载各种各样的`bean`，但这些bean都不一定是我们`RPC`调用的时候所用到的，所以需要找个东西区分开来，我这里使用注解去区分开来。

## 创建RPC服务用的注解

在`adouge-rpc-tool`里面创建一个注解用来区分`RPC`服务和普通服务的。

```java
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Inherited
@Component
public @interface RpcService {
}
```

## 服务提供者

首先我们需要定义一个Map，里面用来存放接口以及他的实现类。所以我们创建一个`ServiceProvider`以及他的实现类。

```java
public interface ServiceProvider {
    /**
     * 添加服务类
     * @param bean bean
     * @param interfaceName 接口名
     */
    void addService(Object bean,String interfaceName);

    /**
     * 获取服务类
     * @param interfaceName 接口名
     * @return 服务嗲你勒
     */
    Object getService(String interfaceName);
}
```

```java
@Slf4j
@Component
public class ServiceProviderImpl implements ServiceProvider{
    private final Map&lt;String, Object&gt; serviceMap= new ConcurrentHashMap&lt;&gt;();

    @Override
    public void addService(Object bean,String interfaceName) {
        serviceMap.put(interfaceName,bean);
    }
    @Override
    @SneakyThrows
    public Object getService(String interfaceName) {
        Object service = serviceMap.get(interfaceName);
        if (null == service) {
            throw new Exception(&quot;找不到对应的Bean&quot;);
        }
        return service;
    }
}
```

## 在`BeanPostProcessor`里面区分

实现`BeanPostProcessor`里面的前置方法`postProcessBeforeInitialization`

```java
@Slf4j
@Component
@RequiredArgsConstructor
public class RpcBeanPostProcessor implements BeanPostProcessor {
    private final ServiceProvider serviceProvider;

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        Class&lt;?&gt; cls = bean.getClass();
        if (cls.isAnnotationPresent(RpcService.class)) {
            Class&lt;?&gt;[] interfaces = cls.getInterfaces();
            for (Class&lt;?&gt; anInterface : interfaces) {
                log.info(&quot;【{}】添加到服务实现类---&quot;, anInterface.getName(), bean);
                serviceProvider.addService(bean, anInterface.getName());
            }
        }
        return bean;
    }
}
```

## 再次修改`RpcRequestHandler`

同样的单例获取`ServiceProvider`，然后调用`getService`获取实现类。

```java
@Component
@RequiredArgsConstructor
public class RpcRequestHandler {
    private final ServiceProvider serviceProvider;

    /**
     * 用作处理
     */
    @SneakyThrows
    public Object handler(RpcRequest request) {
        Object service = serviceProvider.getService(request.getInterfaceName());
        return invokeTargetMethod(request, service);
    }
}
```

`RpcRequestHandler`修改完后`NettyServerHandler`就会报错，这个简单，简单改下就好了。

```java
RpcRequestHandler bean = SpringUtil.getBean(RpcRequestHandler.class);
Object handler = bean.handler(rpcRequest);
```

## 再一次测试

惯例，先运行服务端，在运行客户端就会出现真正成功的效果了。

```java
11:57:38.224 [nioEventLoopGroup-2-1] INFO com.adouge.rpc.core.tool.netty.NettyClientHandler - 服务端返回数据: [RpcResponse(message=sb123)]
RpcResponse(message=sb123)
```

这篇有点长了，剩下部分分到下期吧。</content:encoded><enclosure url="https://adougebabi-github-io.pages.dev/undefined" length="0" type="image/jpeg"/></item><item><title>从零开始写RPC框架-03</title><link>https://adougebabi-github-io.pages.dev/blog/rpc-framework-03/</link><guid isPermaLink="true">https://adougebabi-github-io.pages.dev/blog/rpc-framework-03/</guid><description>完成 RPC 框架的客户端实现，通过动态代理实现本地调用远程方法并返回结果</description><pubDate>Tue, 08 Sep 2020 15:12:01 GMT</pubDate><content:encoded>上篇讲到我们已经可以通过传名称去调用远程方法，那这次就把剩下的本地调用方法，然后远程执行并返回结果。

# 升级客户端为Web项目

这个也没什么可以说了可以参考上篇升级服务端。

## 创建测试用接口

就好像平常使用那样创建一个Controller引用`ICallService`就好了

```java
@RestController
public class TestController {
    private ICallService service;

    @GetMapping(&quot;/{msg}&quot;)
    public String testRpc(@PathVariable String msg){
        return service.callTest(msg);
    }
}
```

但是这里即使不运行也能猜到运行结果，就是空指针，这个service从来都没有被初始化。

那么问题来了，这个service的实现类并不在当前项目下面，new也new不出来，用`Spring`也没办法注入。让他空的话又会空指针。那应该咋搞。

思路大概是这样，我初始化的时候塞一个代理给它，然后在调用里面的方法的时候，用前面准备的去远程调用就好了。

还记得上期的`CglibProxyFactory`和`MethodInterceptorImpl`吗？在`MethodInterceptorImpl`里面`intercept`
不是有是一个拦截吗？用这个去实现就好了。

## 改造返回类（`RpcResponse`）

原来返回一个message肯定是不够的嘛，作为一个方法，肯定是各式各样的返回值，所以我们先把返回实体修改一下。

```java
@Getter
@Setter
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class RpcResponse&lt;T&gt; {
    private String msg;
    private Integer code;
    private T data;
}
```

就跟普通的web返回体类似。原来用到的对应改就好了。

&gt; 既然有code去判断状态，就不加success了，传输能少一点就是一点。

## 创建远程代理实现类

首先创建一个远程方法拦截器的实现类，为了跟原来本地的区分开来，这就就把原来的`MethodInterceptorImpl改成LocalMethodInterceptorImpl`
，然后创建`RemoteMethodInterceptorImpl`。

`RemoteMethodInterceptorImpl`具体要实现就是远程调用了，这里就直接把上期写的测试方法搬过来就好了。

```java
@Slf4j
public class RemoteMethodInterceptorImpl implements MethodInterceptor {
    @Override
    @SneakyThrows
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) {
        log.info(&quot;调用远程方法前--&gt;{}&quot;,method.getName());
        RpcRequest build = RpcRequest.builder()
                .interfaceName(method.getDeclaringClass().getName())
                .methodName(method.getName())
                .args(args)
                .parameterTypes(method.getParameterTypes())
                .build();
        RpcResponse&lt;Object&gt; response = new NettyClient(&quot;localhost&quot;, 6666).send(build);
        if(response.getCode()== HttpStatus.HTTP_OK){
            return response.getData();
        }
        log.info(&quot;调用远程方法后--&gt;{}&quot;,method.getName());
        return null;
    }
}
```

&gt; `NettyClient`这个原本是在`adouge-rpc-client`里面，现在被我移动到了`adouge-rpc-tool`里面。

## 改造代理工厂

实现类有了，那下一步就是改造工厂（`CglibProxyFactory`）。

这个挺简单的，复制粘贴改下实现类就好了,我为了区分开来，把原来的改成`getLocalProxy`。

```java
public class CglibProxyFactory {

    public static Object getLocalProxy(Class&lt;?&gt; clazz) {
        // 创建动态代理增强类
        Enhancer enhancer = new Enhancer();
        // 设置类加载器
        enhancer.setClassLoader(clazz.getClassLoader());
        // 设置被代理类
        enhancer.setSuperclass(clazz);
        // 设置方法拦截器
        enhancer.setCallback(new LocalMethodInterceptorImpl());
        // 创建代理类
        return enhancer.create();
    }
    public static Object getRemoteProxy(Class&lt;?&gt; clazz) {
        Enhancer enhancer = new Enhancer();
        enhancer.setClassLoader(clazz.getClassLoader());
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(new RemoteMethodInterceptorImpl());
        return enhancer.create();
    }
}
```

## 实现`postProcessAfterInitialization`

下一步就是在它初始化的时候随便晒点东西，告诉jvm不为空，你有个代理帮你执行方法。

既然集成了`Spring`那就直接用`BeanPostProcessor`的后置处理去处理就好了。

### 创建`RpcReference`

同样的我们需要跟普通Bean区分开来。同样的创建一个注解。

```java
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@Inherited
@Component
public @interface RpcReference {
}
```

&gt; 记得在controller里面的service加上注解

我们继续实现后置处理的逻辑。

```java
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        Class&lt;?&gt; cls = bean.getClass();
        Field[] declaredFields = cls.getDeclaredFields();
        for (Field declaredField : declaredFields) {
            RpcReference rpcReference = declaredField.getAnnotation(RpcReference.class);
            if (rpcReference != null) {
                Object proxy = CglibProxyFactory.getRemoteProxy(declaredField.getType());
                declaredField.setAccessible(true);
                try {
                    declaredField.set(bean,proxy);
                } catch (IllegalAccessException e) {
                    log.error(&quot;设置代理异常-&gt;&quot;,e);
                }
            }
        }
        return bean;
    }
```

其实也没什么可以说，就是判断这个bean里面是否有这个注解，有就给他一个代理。

## 测试

把程序跑起来，访问http://localhost:8081/asdasdas 就会看到 `asdasdas`被显示出来

![IdNcmP](https://raw.giteeusercontent.com/Semiramis/oss/raw/master/2020/09/09/IdNcmP.png)

&gt; 同时开两个web项目端口冲突的话就在`adouge-rpc-client`的resource里面添加application.yml，在里面改端口
&gt;
&gt; ```yaml
&gt; server:
&gt;   port: 8081
&gt; ```

现在远程的地址（localhost）跟端口是写死的，下篇就改成动态的吧。</content:encoded><enclosure url="https://adougebabi-github-io.pages.dev/undefined" length="0" type="image/jpeg"/></item></channel></rss>