<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
    <channel>
            <title>老蒋的知识库</title>
            <link>http://halo.ljdzsk.com</link>
        <generator>Halo 1.6.0</generator>
        <lastBuildDate>Mon, 05 May 2025 20:02:17 CST</lastBuildDate>
                <item>
                    <title>
                        <![CDATA[openjdk docker生产部署问题]]>
                    </title>
                    <link>http://halo.ljdzsk.com/archives/openjdkdocker-sheng-chan-bu-shu-wen-ti</link>
                    <description>
                            <![CDATA[<h1 id="docker-image%E4%BD%BF%E7%94%A8openjdk%E6%97%A0%E6%B3%95%E4%BD%BF%E7%94%A8%E5%AD%97%E4%BD%93%E6%8E%A7%E4%BB%B6" tabindex="-1">docker image使用openjdk无法使用字体控件</h1><h3 id="%E9%97%AE%E9%A2%98%E6%8F%8F%E8%BF%B0" tabindex="-1">问题描述</h3><p>Java服务使用openjdk:17-jdk-alpine为基础镜像进行构建的存在两个问题：<br />1.openjdk不包括sum.awt的字体控件<br />2.alpine linux的基础镜像也未安装有fontconfig和ttf-dejavu字体。</p><p><strong>openjdk明确表示只适用于预发布，非生产环境</strong><br /><img src="/upload/2025/05/image.png" alt="image" /></p><h3 id="%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95" tabindex="-1">解决方法</h3><p>切换其他发行商生产环境镜像，我目前使用的是亚马逊的</p><pre><code class="language-dockerfile">FROM amazoncorretto:17.0.15</code></pre><h3 id="%E7%94%9F%E4%BA%A7%E7%8E%AF%E5%A2%83%E9%95%9C%E5%83%8F%E6%8E%A8%E8%8D%90" tabindex="-1">生产环境镜像推荐</h3><table><thead><tr><th>发行商</th><th>介绍</th></tr></thead><tbody><tr><td><a href="https://hub.docker.com/r/alibabadragonwell/dragonwell" target="_blank">亚马逊</a></td><td>Amazon Corretto提供的完全免费、多平台、生产就绪型发行版，提供长期支持，其中包括性能增强和安全修复。它基于openjdk，承诺100%兼容openjdk；在紧密跟进openjdk的同时，AWS&amp;Amazon也会做例行的安全修复和性能增强。是目前最受欢迎的JDK</td></tr><tr><td><a href="https://hub.docker.com/_/eclipse-temurin" target="_blank">Eclipse Temurin</a></td><td>Eclipse Temurin 是 Eclipse Adoptium 项目的一部分，Eclipse Temurin是 OpenJDK 的免费、开源、生产就绪的实现。Temurin 是 Oracle JDK 的完全兼容的替代品，并提供与 Oracle JDK 相同的功能和性能。</td></tr><tr><td><a href="https://hub.docker.com/r/alibabadragonwell/dragonwell" target="_blank">阿里</a></td><td>Alibaba Dragonwell 是一款免费的, 生产就绪型Open JDK 发行版，提供长期支持，包括性能增强和安全修复。阿里巴巴拥有最丰富的Java应用场景，覆盖电商，金融，物流等众多领域，世界上最大的Java用户之一。Alibaba Dragonwell作为Java应用的基石，支撑了阿里经济体内所有的Java业务。<strong>Dragonwell 在docker hub上的文档和维护好像没有上述的其他家用心，基本没什么文档。</strong></td></tr></tbody></table>]]>
                    </description>
                    <pubDate>Mon, 05 May 2025 20:02:17 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Spring Boot JPA  PGSQL，自动创建表、初始化数据库、自动获得 CRUD 方法、SQL参数明文显示]]>
                    </title>
                    <link>http://halo.ljdzsk.com/archives/springbootjpapgsql-zi-dong-chuang-jian-biao--chu-shi-hua-shu-ju-ku--zi-dong-huo-de-crud-fang-fa-sql-can-shu-ming-wen-xian-shi</link>
                    <description>
                            <![CDATA[<ol><li>Docker部署PGSQL</li><li>MVN包配置</li><li>数据库配置</li><li>服务启动时自动删除、新建表用于测试环境</li><li>JpaRepository 自动创建CRUD方法</li><li>SQL语句参数明文输出</li></ol><h2 id="docker%E9%83%A8%E7%BD%B2pgsql" tabindex="-1">Docker部署PGSQL</h2><pre><code class="language-">docker run -d --name pgsql -e POSTGRES_PASSWORD=12345678 -p 5432:5432 postgres:17.4</code></pre><p>连接数据库后自动创建数据库和用户名，默认用户名密码<code>postgres</code>:<code>postgres</code></p><h2 id="mvn%E5%8C%85%E9%85%8D%E7%BD%AE" tabindex="-1">MVN包配置</h2><pre><code class="language-">        &lt;!--  JPA 依赖  --&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;            &lt;artifactId&gt;spring-boot-starter-data-jpa&lt;/artifactId&gt;        &lt;/dependency&gt;        &lt;!-- https://mvnrepository.com/artifact/org.postgresql/postgresql --&gt;        &lt;!--  postgresql 依赖  --&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.postgresql&lt;/groupId&gt;            &lt;artifactId&gt;postgresql&lt;/artifactId&gt;            &lt;version&gt;42.5.6&lt;/version&gt;        &lt;/dependency&gt;        &lt;!-- Model数据验证 --&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;            &lt;artifactId&gt;spring-boot-starter-validation&lt;/artifactId&gt;        &lt;/dependency&gt;</code></pre><h3 id="%E6%95%B0%E6%8D%AE%E5%BA%93%E9%85%8D%E7%BD%AE" tabindex="-1">数据库配置</h3><pre><code class="language-">spring:  datasource:    url: jdbc:postgresql://localhost:5432/ai # 这里改成自己的表    username: ai_root # 改成自己的用户名    password: 12345678  jpa:    show-sql: true # 打印sql语句    hibernate:      ddl-auto: create # 服务启动后自动读取@Entity类，删除并创建新的数据库表，update为更新    defer-datasource-initialization: true  # 启动后执行sql语句关键配置：延迟数据源初始化，确保先建表    properties:      hibernate:        format_sql: true # sql 语句格式化输出  sql:    init:      mode: always # 始终执行初始化脚本（即使是非嵌入式数据库）      schema-locations: classpath:init.sql # 需要执行的sql脚本</code></pre><h3 id="%E6%9C%8D%E5%8A%A1%E5%90%AF%E5%8A%A8%E6%97%B6%E8%87%AA%E5%8A%A8%E5%88%A0%E9%99%A4%E3%80%81%E6%96%B0%E5%BB%BA%E8%A1%A8%E7%94%A8%E4%BA%8E%E6%B5%8B%E8%AF%95%E7%8E%AF%E5%A2%83%EF%BC%88%E7%94%9F%E4%BA%A7%E7%8E%AF%E5%A2%83%E5%BB%BA%E8%AE%AE%E5%85%B3%E9%97%AD%EF%BC%89" tabindex="-1">服务启动时自动删除、新建表用于测试环境（生产环境建议关闭）</h3><h4 id="%E5%90%AF%E5%8A%A8%E7%B1%BB%E6%B7%BB%E5%8A%A0%E6%B3%A8%E8%A7%A3%40entityscan%E7%A1%AE%E5%AE%9A%E9%9C%80%E8%A6%81%E6%89%AB%E6%8F%8F%E7%9A%84%E5%AE%9E%E4%BE%8B%E7%B1%BB(%40entity)%E6%89%80%E5%9C%A8%E4%BD%8D%E7%BD%AE" tabindex="-1">启动类添加注解@EntityScan确定需要扫描的实例类(@Entity)所在位置</h4><pre><code class="language-">import org.springframework.boot.autoconfigure.domain.EntityScan;import org.springframework.cache.annotation.EnableCaching;import org.springframework.scheduling.annotation.EnableAsync;@EnableAsync@EnableCaching@EntityScan(&quot;com.jagger.ai.api&quot;)public abstract class BaseSpringApplication {    // 可以在此处定义公共的 Bean 或配置方法}</code></pre><h4 id="user-model-%E5%AE%9E%E4%BE%8B%E7%B1%BB%EF%BC%8C%E6%B7%BB%E5%8A%A0%40entity%E3%80%81%40table%E3%80%81%40id%E3%80%81%40column%E7%AD%89%E6%B3%A8%E8%A7%A3%E7%94%A8%E4%BA%8E%E5%85%B3%E8%81%94%E6%95%B0%E6%8D%AE%E5%BA%93%E8%A1%A8" tabindex="-1">User Model 实例类，添加@Entity、@Table、@Id、@Column等注解用于关联数据库表</h4><pre><code class="language-">import com.fasterxml.jackson.annotation.JsonFormat;import com.fasterxml.jackson.annotation.JsonIgnore;import io.swagger.v3.oas.annotations.media.Schema;import jakarta.persistence.Column;import jakarta.persistence.Entity;import jakarta.persistence.Id;import jakarta.persistence.Table;import lombok.Data;import lombok.experimental.Accessors;import org.hibernate.annotations.Comment;import java.io.Serializable;import java.util.Date;@Data@Accessors(chain = true)@Schema(description = &quot;用户信息&quot;)@Entity @Table(name = &quot;users&quot;) // 表名public class User implements Serializable {    @Id // 主键    @JsonFormat(shape = JsonFormat.Shape.STRING) // 强制以字符串解析    @Schema(description = &quot;ID，Snowflake算法生成&quot;)    @Comment(&quot;ID，Snowflake算法生成&quot;)    private long id;    @Column(length = 20, nullable = false)    @Schema(description = &quot;登录用户名&quot;)    private String username;    @Column(length = 20, nullable = false)    @JsonIgnore    @Schema(description = &quot;登录密码，这里用密文加密&quot;)    private String password;    @Column(length = 20, nullable = false)    @Schema(description = &quot;昵称&quot;)    private String nickname;    @Column(length = 20)    @Schema(description = &quot;手机号&quot;)    private String phoneNumber;    @Column(nullable = false)    @Schema(description = &quot;头像，这里存储的是地址&quot;)    private String avatarUrl;    @Column(length = 30)    @Schema(description = &quot;邮箱&quot;)    private String email;    @Column(nullable = false)    @JsonIgnore    @Schema(description = &quot;创建时间&quot;)    private Date createTime;}</code></pre><h4 id="%E6%B7%BB%E5%8A%A0sql%E8%AF%AD%E5%8F%A5%E8%84%9A%E6%9C%AC%EF%BC%8C%E6%9C%8D%E5%8A%A1%E5%90%AF%E5%8A%A8%E6%97%B6%E8%87%AA%E5%8A%A8%E6%89%A7%E8%A1%8Csql%E8%84%9A%E6%9C%AC" tabindex="-1">添加SQL语句脚本，服务启动时自动执行SQL脚本</h4><p>存放位置: resources/init.sql，配置内容见： <code>数据库配置</code></p><pre><code class="language-">INSERT INTO users (id, username, password, nickname, avatar_url, create_time)VALUES (1, &#39;username&#39;, &#39;password&#39;, &#39;管理员&#39;, &#39;123.image&#39;, NOW());</code></pre><h3 id="jparepository-%E8%87%AA%E5%8A%A8%E5%88%9B%E5%BB%BAcrud%E6%96%B9%E6%B3%95" tabindex="-1">JpaRepository 自动创建CRUD方法</h3><p>UserRepository</p><pre><code class="language-">import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.data.jpa.repository.Query;import org.springframework.stereotype.Repository;import java.util.List;import java.util.Optional;// 继承 JpaRepository 即可自动获得 CRUD 方法@Repositorypublic interface UserRepository extends JpaRepository&lt;User, Long&gt; {    // 自动生成查询（方法名约定）    Optional&lt;User&gt; findByUsernameAndPassword(String name, String password);    // 自定义 JPQL 查询    @Query(&quot;SELECT u FROM User u WHERE u.username LIKE %?1%&quot;)    List&lt;User&gt; searchByUsername(String username);}</code></pre><p>UserService</p><pre><code class="language-">import com.jagger.ai.services.auth.repository.UserRepository;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Service;@Service@Slf4j@RequiredArgsConstructorpublic class UserService {    private final UserRepository userRepository;    public User findByUsernameAndPassword(String username, String password) {        return userRepository.findByUsernameAndPassword(username, password).orElse(null);    }}</code></pre><h3 id="sql%E8%AF%AD%E5%8F%A5%E5%8F%82%E6%95%B0%E6%98%8E%E6%96%87%E8%BE%93%E5%87%BA" tabindex="-1">SQL语句参数明文输出</h3><pre><code class="language-">logging:  level:    org.hibernate.orm.jdbc.extract: TRACE # SQL响应参数    org.hibernate.orm.jdbc.bind: TRACE # SQL请求参数</code></pre>]]>
                    </description>
                    <pubDate>Thu, 06 Mar 2025 02:16:20 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Spring Boot 访问Redis]]>
                    </title>
                    <link>http://halo.ljdzsk.com/archives/springboot-fang-wen-redis</link>
                    <description>
                            <![CDATA[<ol><li>Docker部署Redis</li><li>Mvn配置</li><li>Redis配置文件</li><li>代码</li></ol><h3 id="docker%E9%83%A8%E7%BD%B2redis" tabindex="-1">Docker部署Redis</h3><pre><code class="language-"># docker部署redis，密码 12345678docker run -d --name redis --network ai_network -p 6379:6379 redis:7.4.2 --requirepass &quot;12345678&quot;</code></pre><h3 id="mvn%E9%85%8D%E7%BD%AE" tabindex="-1">Mvn配置</h3><pre><code class="language-">        &lt;!-- 集成redis依赖  --&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;            &lt;artifactId&gt;spring-boot-starter-data-redis&lt;/artifactId&gt;        &lt;/dependency&gt;</code></pre><h3 id="redis%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6" tabindex="-1">Redis配置文件</h3><p>最新Spring Boot多了个Data字段，如果配置不生效检查下版本</p><pre><code class="language-">spring:  data: # 多出来的字段    redis:      host: localhost      port: 6379      password: 12345678      database: 0</code></pre><h3 id="%E4%BB%A3%E7%A0%81" tabindex="-1">代码</h3><h4 id="%E8%87%AA%E5%AE%9A%E4%B9%89customredistemplate%E5%BA%8F%E5%88%97%E5%8C%96%E6%96%B9%E6%B3%95" tabindex="-1">自定义<code>customRedisTemplate</code>序列化方法</h4><p>这样通过工具访问redis查看的时候就不是乱码的了。</p><pre><code class="language-">import com.fasterxml.jackson.annotation.JsonAutoDetect;import com.fasterxml.jackson.annotation.PropertyAccessor;import com.fasterxml.jackson.databind.ObjectMapper;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;import org.springframework.data.redis.serializer.StringRedisSerializer;@Configurationpublic class RedisConfig {    @Bean    public RedisTemplate&lt;String, Object&gt; customRedisTemplate(RedisConnectionFactory factory) {        RedisTemplate&lt;String, Object&gt; template = new RedisTemplate&lt;&gt;();        template.setConnectionFactory(factory);        // 使用 StringRedisSerializer 序列化键（Key）        template.setKeySerializer(new StringRedisSerializer());        template.setHashKeySerializer(new StringRedisSerializer());        ObjectMapper objectMapper = new ObjectMapper();        //序列化包括类型描述 否则反向序列化实体会报错，一律都为JsonObject        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);        objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);        // 使用 Jackson2JsonRedisSerializer 序列化值（Value）        Jackson2JsonRedisSerializer&lt;Object&gt; valueSerializer = new Jackson2JsonRedisSerializer&lt;&gt;(objectMapper, Object.class);        template.setValueSerializer(valueSerializer);        template.setHashValueSerializer(valueSerializer);        template.afterPropertiesSet();        return template;    }}</code></pre><h4 id="redisservice%E4%BB%A3%E7%90%86%E6%9C%8D%E5%8A%A1%E7%B1%BB%EF%BC%8C%E5%B0%81%E8%A3%85%E4%B8%80%E5%B1%82redis%E6%93%8D%E4%BD%9C%E6%96%B9%E6%B3%95" tabindex="-1"><code>RedisService</code>代理服务类，封装一层Redis操作方法</h4><pre><code class="language-">import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.dao.DataAccessException;import org.springframework.data.redis.core.*;import org.springframework.stereotype.Component;import java.util.*;import java.util.concurrent.TimeUnit;/** * spring redis 工具类 **/@SuppressWarnings(value = {&quot;unchecked&quot;, &quot;rawtypes&quot;}) // 无视泛型的Idea代码警告@Componentpublic class RedisService {    private final RedisTemplate redisTemplate;    public RedisService(@Qualifier(&quot;customRedisTemplate&quot;) RedisTemplate redisTemplate) {        this.redisTemplate = redisTemplate;    }    /**     * 缓存基本的对象，Integer、String、实体类等     *     * @param key   缓存的键值     * @param value 缓存的值     */    public &lt;T&gt; void setCacheObject(final String key, final T value) {        redisTemplate.opsForValue().set(key, value);    }    /**     * 缓存基本的对象，Integer、String、实体类等     *     * @param key      缓存的键值     * @param value    缓存的值     * @param timeout  时间     * @param timeUnit 时间颗粒度     */    public &lt;T&gt; void setCacheObject(final String key, final T value, final Long timeout, final TimeUnit timeUnit) {        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);    }    /**     * 设置有效时间     *     * @param key     Redis键     * @param timeout 超时时间     * @return true=设置成功；false=设置失败     */    public boolean expire(final String key, final long timeout) {        return expire(key, timeout, TimeUnit.SECONDS);    }    /**     * 设置有效时间     *     * @param key     Redis键     * @param timeout 超时时间     * @param unit    时间单位     * @return true=设置成功；false=设置失败     */    public boolean expire(final String key, final long timeout, final TimeUnit unit) {        return redisTemplate.expire(key, timeout, unit);    }    /**     * 获取有效时间     *     * @param key Redis键     * @return 有效时间     */    public long getExpire(final String key) {        return redisTemplate.getExpire(key);    }    /**     * 判断 key是否存在     *     * @param key 键     * @return true 存在 false不存在     */    public Boolean hasKey(String key) {        return redisTemplate.hasKey(key);    }    /**     * 获得缓存的基本对象。     *     * @param key 缓存键值     * @return 缓存键值对应的数据     */    public &lt;T&gt; T getCacheObject(final String key) {        ValueOperations&lt;String, T&gt; operation = redisTemplate.opsForValue();        return operation.get(key);    }    /**     * 删除单个对象     *     * @param key     */    public boolean deleteObject(final String key) {        return redisTemplate.delete(key);    }    /**     * 删除集合对象     *     * @param collection 多个对象     * @return     */    public boolean deleteObject(final Collection collection) {        return redisTemplate.delete(collection) &gt; 0;    }    /**     * 缓存List数据     *     * @param key      缓存的键值     * @param dataList 待缓存的List数据     * @return 缓存的对象     */    public &lt;T&gt; long setCacheList(final String key, final List&lt;T&gt; dataList) {        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);        return count == null ? 0 : count;    }    /**     * 缓存List数据，单个增加     *     * @param key  缓存的键值     * @param data 待缓存的List数据     * @return 缓存的对象     */    public &lt;T&gt; long setCacheList(final String key, final T data) {        Long count = redisTemplate.opsForList().rightPush(key, data);        return count == null ? 0 : count;    }    /**     * 获得缓存的list对象：指定范围     * 下标数值如果为负数则表示倒数，如需提取倒数2位：start = -2、 end = -1     *     * @param key   缓存的键值     * @param start 开始下标     * @param end   结束下标     * @return 缓存键值对应的数据     */    public &lt;T&gt; List&lt;T&gt; getCacheList(final String key, final long start, final long end) {        return redisTemplate.opsForList().range(key, start, end);    }    /**     * 获得缓存的list对象：根据参数 num 的值，获取特定数量的值     *     * @param key 缓存的键值     * @param num 获取的数量     *            num &gt; 0，表示获取前几位     *            num &lt; 0，表示获取到倒数第几位     *            num == 0，表示获取全部     * @return 缓存键值对应的数据     */    public &lt;T&gt; List&lt;T&gt; getCacheList(final String key, final long num) {        return this.getCacheList(key, 0, num &lt; 1 ? num : num - 1);    }    /**     * 获得缓存的list对象：所有对象     *     * @param key 缓存的键值     * @return 缓存键值对应的数据     */    public &lt;T&gt; List&lt;T&gt; getCacheList(final String key) {        return this.getCacheList(key, 0, -1);    }    /**     * 获得缓存的list对象：获取并移除指定范围内的数据。     *     * @param key   缓存的键值     * @param start 开始下标     * @param end   结束下标     * @return 缓存键值对应的数据     */    public &lt;T&gt; List&lt;T&gt; removeGetCacheList(final String key, final long start, final long end) {        List&lt;T&gt; l = (List&lt;T&gt;) redisTemplate.execute(new SessionCallback&lt;List&lt;Object&gt;&gt;() {            public List&lt;T&gt; execute(RedisOperations operations) throws DataAccessException {                /*                  通过事务保证redis原子性操作                  1. 提取指定范围内数据                  2. 剪切保留 end 到 最后一位数据（注：此时会少移除左侧一条数据，不一步到位原因：移除数据时&#96;trim([超过list最大值], -1)&#96;会保留一条数据）                  3. 再从左移除一条多余数据                 */                operations.multi();                operations.opsForList().range(key, start, end);                operations.opsForList().trim(key, end, -1);                operations.opsForList().rightPop(key);                return operations.exec();            }        });        return (List&lt;T&gt;) l.get(0);    }    /**     * 获得缓存的list对象：根据参数 num 的值，移除并获取list特定数量的数据。     *     * @param key 缓存的键值     * @param num 获取的数量     *            num &gt; 0，表示获取前几位     *            num &lt; 0，表示获取到倒数第几位     *            num == 0，表示获取全部     * @return 缓存键值对应的数据     */    public &lt;T&gt; List&lt;T&gt; removeGetCacheList(final String key, final long num) {        return this.removeGetCacheList(key, 0, num &lt; 1 ? num : num - 1);    }    /**     * 获得缓存的list对象：移除并获取list所有的数据     *     * @param key 缓存的键值     * @return 缓存键值对应的数据     */    public &lt;T&gt; List&lt;T&gt; removeGetCacheList(final String key) {        return this.removeGetCacheList(key, 0, -1);    }    /**     * 移除缓存的list对象     *     * @param key   缓存的键值     * @param count 开始下标     *              count &gt; 0 : 从表头开始向表尾搜索，移除与 VALUE 相等的元素，数量为 COUNT 。     *              count &lt; 0 : 从表尾开始向表头搜索，移除与 VALUE 相等的元素，数量为 COUNT 的绝对值。     *              count = 0 : 移除表中所有与 VALUE 相等的值。     * @param value 匹配的值     * @return 缓存键值对应的数据     */    public Long removeCacheList(final String key, final long count, final Object value) {        return redisTemplate.opsForList().remove(key, count, value);    }    /**     * 移除缓存的list对象: 移除表中所有与 VALUE 相等的值。     *     * @param key   缓存的键值     * @param value 匹配的值     * @return 缓存键值对应的数据     */    public Long removeCacheList(final String key, final Object value) {        return removeCacheList(key, 0, value);    }    /**     * 缓存Set     *     * @param key     缓存键值     * @param dataSet 缓存的数据     * @return 缓存数据的对象     */    public &lt;T&gt; BoundSetOperations&lt;String, T&gt; setCacheSet(final String key, final Set&lt;T&gt; dataSet) {        BoundSetOperations&lt;String, T&gt; setOperation = redisTemplate.boundSetOps(key);        Iterator&lt;T&gt; it = dataSet.iterator();        while (it.hasNext()) {            setOperation.add(it.next());        }        return setOperation;    }    /**     * 获得缓存的set     *     * @param key     * @return     */    public &lt;T&gt; Set&lt;T&gt; getCacheSet(final String key) {        return redisTemplate.opsForSet().members(key);    }    /**     * 缓存Map     *     * @param key     * @param dataMap     */    public &lt;T&gt; void setCacheMap(final String key, final Map&lt;String, T&gt; dataMap) {        if (dataMap != null) {            redisTemplate.opsForHash().putAll(key, dataMap);        }    }    /**     * 获得缓存的Map     *     * @param key     * @return     */    public &lt;T&gt; Map&lt;String, T&gt; getCacheMap(final String key) {        return redisTemplate.opsForHash().entries(key);    }    /**     * 往Hash中存入数据     *     * @param key   Redis键     * @param hKey  Hash键     * @param value 值     */    public &lt;T&gt; void setCacheMapValue(final String key, final String hKey, final T value) {        redisTemplate.opsForHash().put(key, hKey, value);    }    /**     * 获取Hash中的数据     *     * @param key  Redis键     * @param hKey Hash键     * @return Hash中的对象     */    public &lt;T&gt; T getCacheMapValue(final String key, final String hKey) {        HashOperations&lt;String, String, T&gt; opsForHash = redisTemplate.opsForHash();        return opsForHash.get(key, hKey);    }    /**     * 获取多个Hash中的数据     *     * @param key   Redis键     * @param hKeys Hash键集合     * @return Hash对象集合     */    public &lt;T&gt; List&lt;T&gt; getMultiCacheMapValue(final String key, final Collection&lt;Object&gt; hKeys) {        return redisTemplate.opsForHash().multiGet(key, hKeys);    }    /**     * 删除Hash中的某条数据     *     * @param key  Redis键     * @param hKey Hash键     * @return 是否成功     */    public boolean deleteCacheMapValue(final String key, final String hKey) {        return redisTemplate.opsForHash().delete(key, hKey) &gt; 0;    }    /**     * 获得缓存的基本对象列表     *     * @param pattern 字符串前缀     * @return 对象列表     */    public Collection&lt;String&gt; keys(final String pattern) {        return redisTemplate.keys(pattern);    }}</code></pre>]]>
                    </description>
                    <pubDate>Wed, 05 Mar 2025 22:54:33 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Spring Boot Gateway转发Http、Websocket，Nacos服务发现，设置路由白名单]]>
                    </title>
                    <link>http://halo.ljdzsk.com/archives/springbootgateway-zhuan-fa-httpwebsocket-she-zhi-bai-ming-dan-lu-you</link>
                    <description>
                            <![CDATA[<ol><li>Mvn包配置</li><li>配置文件</li><li>代码</li></ol><h3 id="mvn%E5%8C%85%E9%85%8D%E7%BD%AE" tabindex="-1">Mvn包配置</h3><pre><code class="language-">    &lt;dependencies&gt;        &lt;!--  只需要网关服务需要  --&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;            &lt;artifactId&gt;spring-cloud-starter-gateway&lt;/artifactId&gt;        &lt;/dependency&gt;                &lt;!-- SpringCloud Alibaba Nacos 服务注册 --&gt;        &lt;dependency&gt;            &lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;            &lt;artifactId&gt;spring-cloud-starter-alibaba-nacos-discovery&lt;/artifactId&gt;        &lt;/dependency&gt;        &lt;!-- SpringCloud Alibaba Nacos 配置中心 --&gt;        &lt;dependency&gt;            &lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;            &lt;artifactId&gt;spring-cloud-starter-alibaba-nacos-config&lt;/artifactId&gt;        &lt;/dependency&gt;        &lt;!-- spring boot 2.4之后默认禁用bootstrap.yml配置文件，生效需要引入 --&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;            &lt;artifactId&gt;spring-cloud-starter-bootstrap&lt;/artifactId&gt;        &lt;/dependency&gt;    &lt;/dependencies&gt;</code></pre><h3 id="%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6" tabindex="-1">配置文件</h3><p>Gateway服务配置文件: bootstrap.yml</p><pre><code class="language-">server:  port: 8080spring:  application:    name: gateway  profiles:    active: @profiles.active@  cloud:    nacos:      discovery: # 服务注册        server-addr: @nacos.server@        username: @nacos.username@        password: @nacos.password@        namespace: @nacos.namespace-id@        service: ${spring.application.name}-${spring.profiles.active}      config: # 配置管理        server-addr: @nacos.server@        username: @nacos.username@        password: @nacos.password@        namespace: @nacos.namespace-id@        file-extension: yml        shared-configs: # 指定额外的配置文件          - data-id: application.yml          - data-id: redis.yml    loadbalancer:      nacos:        enabled: true # 开启nacos负载均衡策略    gateway:      routes: # 网关路由配置        - id: auth-service # 路由id，自定义，只要唯一即可          #uri: http://127.0.0.1:8088 # 路由的目标地址 http就是固定地址          uri: lb://auth-${spring.profiles.active} # 路由的目标地址 lb就是负载均衡，后面跟Nacos服务发现名称          predicates: # 路由断言，也就是判断请求是否符合路由规则的条件            - Path=/auth/** # 这个是按照路径匹配，只要以 /auth/ 开头就符合要求          filters: # 路由过滤器，对请求或者响应做出处理            - StripPrefix=1 # 去掉路径一条前缀，这里是: /auth          metadata:            auth-white-list:              - /login/**              - /logout/**              - /v3/api-docs/**        - id: chat-service # 路由id，自定义，只要唯一即可          #uri: http://127.0.0.1:8088 # 路由的目标地址 http就是固定地址          uri: lb://chat-${spring.profiles.active} # 路由的目标地址 lb就是负载均衡，后面跟Nacos服务发现名称          predicates: # 路由断言，也就是判断请求是否符合路由规则的条件            - Path=/chat/** # 这个是按照路径匹配，只要以 /chat/ 开头就符合要求          filters: # 路由过滤器，对请求或者响应做出处理            - StripPrefix=1 # 去掉路径一条前缀，这里是: /chat          metadata:            auth-white-list:              - /v3/api-docs/**</code></pre><h3 id="%E4%BB%A3%E7%A0%81" tabindex="-1">代码</h3><p>路由管理器，主要用于获取<code>metadata.auth-white-list</code> 的白名单列表数据，用于跳过鉴权。</p><pre><code class="language-">import jakarta.annotation.PostConstruct;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.cloud.gateway.route.Route;import org.springframework.cloud.gateway.route.RouteLocator;import org.springframework.stereotype.Component;import reactor.core.publisher.Flux;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.Optional;import java.util.stream.Collectors;/*** * 路由管理 */@Component@RequiredArgsConstructor@Slf4jpublic class RouteMetadataService {    private final RouteLocator routeLocator;    private final Map&lt;String, List&lt;String&gt;&gt; routeExcludePaths = new HashMap&lt;&gt;();    /**     * 加载所有路由的元数据     */    public void loadMetadata() {        Flux&lt;Route&gt; routes = routeLocator.getRoutes();        routes.subscribe(route -&gt; {            // 从元数据获取鉴权白名单，路径在白名单中，无需鉴权            List&lt;String&gt; authWhiteList = Optional.ofNullable(route.getMetadata().get(&quot;auth-white-list&quot;))                    .map(obj -&gt; {                        if (obj instanceof List) {                            // 直接转换为List&lt;String&gt;                            return (List&lt;String&gt;) obj;                        } else if (obj instanceof Map) {                            // 如果是Map，提取所有值组成List                            Map&lt;?, ?&gt; map = (Map&lt;?, ?&gt;) obj;                            return map.values().stream()                                    .filter(String.class::isInstance)                                    .map(String.class::cast)                                    .collect(Collectors.toList());                        } else {                            log.warn(&quot;auth-white-list 类型不匹配: {}&quot;, obj.getClass().getName());                            return List.&lt;String&gt;of();                        }                    })                    .orElse(List.of());  // 默认返回空列表            routeExcludePaths.put(route.getId(), authWhiteList);        });    }    /**     * 获取指定路由的白名单路径列表     */    public List&lt;String&gt; getauthWhiteList(String routeId) {        return routeExcludePaths.getOrDefault(routeId, List.of());    }    /**     * 动态路由更新时调用此方法刷新缓存     */    @PostConstruct    public void refreshMetadata() {        routeExcludePaths.clear();        loadMetadata();    }}</code></pre><p>AuthFilter鉴权过滤器</p><pre><code class="language-">import com.fasterxml.jackson.core.JsonProcessingException;import com.jagger.ai.services.gateway.service.RouteMetadataService;import com.fasterxml.jackson.databind.ObjectMapper;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.cloud.gateway.filter.GatewayFilterChain;import org.springframework.cloud.gateway.filter.GlobalFilter;import org.springframework.cloud.gateway.route.Route;import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;import org.springframework.core.annotation.Order;import org.springframework.http.HttpHeaders;import org.springframework.http.server.reactive.ServerHttpRequest;import org.springframework.stereotype.Component;import org.springframework.util.AntPathMatcher;import org.springframework.util.PathMatcher;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Mono;import java.util.List;@Slf4j@Component@RequiredArgsConstructor// 设置优先级高于WebsocketRoutingFilter的等级(Integer.MAX_VALUE - 1)，否则不会过滤Websocket请求，// 要大于等于2，否则影响 StripPrefix 截取路由获取path（不影响后续转发）@Order(Integer.MAX_VALUE - 2)public class AuthFilter implements GlobalFilter {    private final RouteMetadataService routeMetadataService; // gateway路由元数据    private final PathMatcher pathMatcher = new AntPathMatcher();  // Spring内置的路径匹配器    private final ObjectMapper objectMapper; // JSON 序列化工具    private final RedisService redisService;    @Override    public Mono&lt;Void&gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) {        log.debug(&quot;全局鉴权开始&quot;);        HttpHeaders headers = exchange.getRequest().getHeaders();        // 检查是否包含 WebSocket 升级头        boolean isWebSocket = headers.containsKey(HttpHeaders.UPGRADE) &amp;&amp;                headers.getConnection().contains(&quot;Upgrade&quot;) &amp;&amp;                &quot;websocket&quot;.equalsIgnoreCase(headers.getFirst(HttpHeaders.UPGRADE));        String token;        if (isWebSocket) {            // WebSocket 请求处理            log.debug(&quot;检测到 WebSocket 请求&quot;);            token = exchange.getRequest().getQueryParams().getFirst(&quot;token&quot;);            log.debug(&quot;WebSocket token: {}&quot;, token);        } else {            log.info(&quot;开始鉴权Http&quot;);            Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);            if (route != null) {                // 过滤白名单路由鉴权                List&lt;String&gt; authWhiteList = routeMetadataService.getauthWhiteList(route.getId());                String path = exchange.getRequest().getURI().getPath();                if (authWhiteList.stream().anyMatch(pattern -&gt; pathMatcher.match(pattern, path))) {                    log.debug(&quot;白名单路由: {}&quot;, path);                    return chain.filter(exchange);                }            }            // 从请求头获取Token            token = exchange.getRequest().getHeaders().getFirst(&quot;Authorization&quot;);        }        // 验证Token逻辑（如调用Auth服务）        User user = isValidToken(token);        if (user == null) {            return Result.error(HttpStatus.UNAUTHORIZED).toMono(exchange.getResponse(), objectMapper);        }        // 鉴权通过，添加用户信息到请求头        try {            objectMapper.writeValueAsString(user);            ServerHttpRequest request = exchange.getRequest()                    .mutate()                    .header(HttpHeaderKeys.USER_INFO, objectMapper.writeValueAsString(user)) // 添加完整用户 JSON                    .build();            // 构建新的 ServerWebExchange 并继续传递            ServerWebExchange newExchange = exchange.mutate().request(request).build();            return chain.filter(newExchange);        } catch (JsonProcessingException e) {            throw new RuntimeException(e);        }    }    /***     * 鉴权判断     */    private User isValidToken(String token) {        log.debug(&quot;鉴权token: {}&quot;, token);        return redisService.getCacheObject(RedisPath.USER_TOKEN + token);    }}</code></pre><h3 id="%E8%B0%83%E7%94%A8%E7%A4%BA%E4%BE%8B" tabindex="-1">调用示例</h3><p>Http调用，获取token对应用户信息</p><pre><code class="language-">curl --location --request GET &#39;127.0.0.1:8080/auth/get_user_by_token&#39; \--header &#39;Authorization: e49d0fd1-5248-4f5a-bc49-ebaf5cb1ce68&#39;</code></pre><p>Websocket因为js不能带有Header信息，所以放在请求地址上</p><pre><code class="language-">// 创建WebSocket连接（替换为你的WebSocket服务器地址）const socket = new WebSocket(&#39;ws://127.0.0.1:8080/chat/im?token=e49d0fd1-5248-4f5a-bc49-ebaf5cb1ce68&#39;);</code></pre>]]>
                    </description>
                    <pubDate>Wed, 05 Mar 2025 22:36:55 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[docker hub push 一直提示无权限]]>
                    </title>
                    <link>http://halo.ljdzsk.com/archives/dockerhubpush-yi-zhi-ti-shi-wu-quan-xian</link>
                    <description>
                            <![CDATA[<p>错误: <code>denied: requested access to the resource is denied</code></p><ol><li>检查是否登录</li><li>检查仓库是否存在</li><li>Mac检查钥匙串是否有异常</li></ol><h3 id="push%E5%91%BD%E4%BB%A4" tabindex="-1">push命令</h3><pre><code class="language-">docker push &lt;docke hub 地址（可以省略）&gt;/&lt;dockerhub的用户名&gt;/&lt;自己的仓库名称&gt;:&lt;镜像名称&gt;docker push docker.io/jagger133609/jagger:gateway-0.0.1</code></pre><h3 id="%E6%A3%80%E6%9F%A5%E6%98%AF%E5%90%A6%E7%99%BB%E5%BD%95" tabindex="-1">检查是否登录</h3><p>重复登录一次</p><pre><code class="language-">docker logindocker logout</code></pre><h3 id="%E6%A3%80%E6%9F%A5%E4%BB%93%E5%BA%93%E6%98%AF%E5%90%A6%E5%AD%98%E5%9C%A8" tabindex="-1">检查仓库是否存在</h3><p>登录: <code>https://hub.docker.com/repository/docker</code>，检查是否有仓库，仓库名是否正确</p><h3 id="mac%E6%A3%80%E6%9F%A5%E9%92%A5%E5%8C%99%E4%B8%B2%E6%98%AF%E5%90%A6%E6%9C%89%E5%BC%82%E5%B8%B8" tabindex="-1">MAC检查钥匙串是否有异常</h3><p>MAC会使用钥匙串管理docker登录鉴权信息，如果出现异常就会这样。<br />进入目录<code>~/Library/Keychains</code>。<br />删除文件夹类似<code>2210E2CA-FE29-50A3-B274-AC1BFCF0C27F</code>，每个人的都不一样，但是这个格式差不多。<br />重启电脑会重新创建一个新的类似文件夹。</p><pre><code class="language-"># 进入钥匙串目录cd ~/Library/Keychains# 删除文件夹rm -rf 2210E2CA-FE29-50A3-B274-AC1BFCF0C27F# 重启电脑</code></pre>]]>
                    </description>
                    <pubDate>Sun, 02 Mar 2025 07:46:08 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Spring boot 加载公共组件提示Bean无法注入]]>
                    </title>
                    <link>http://halo.ljdzsk.com/archives/springboot-jia-zai-gong-gong-zu-jian-ti-shi-bean-wu-fa-zhu-ru</link>
                    <description>
                            <![CDATA[<p>假设目录结构如下，chat服务要导入im组件注入bean。</p><pre><code class="language-">com.jagger├── common          &lt;- 公共模块│   └── im          &lt;- IM 组件└── services        &lt;- 微服务模块    └── chat        &lt;- Chat 微服务</code></pre><p>因为im不属于chat子包层级，spring boot启动时不会默认扫描。这时就需要在im包对外主动暴露自动配置类，chat服务导入时根据配置类进行扫描。</p><h3 id="%E6%B7%BB%E5%8A%A0imautoconfiguration%E7%B1%BB" tabindex="-1">添加<code>IMAutoConfiguration</code>类</h3><pre><code class="language-">import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@Configuration@ComponentScan(&quot;com.jagger.common.im&quot;) // 指定扫描路径public class IMAutoConfiguration {}</code></pre><h3 id="%E6%B7%BB%E5%8A%A0autoconfiguration%E6%96%87%E4%BB%B6" tabindex="-1">添加<code>AutoConfiguration</code>文件</h3><p>如果是spring boot2.7以下创建文件：<code>resources/META-INF/spring.factories</code>，并写入</p><pre><code class="language-">org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.jagger.common.im.IMAutoConfiguration</code></pre><p>如果是spring boot2.7以上，特别是3.x创建文件：<code>/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports</code>，注意这里目录结构不一样是：<font color="red">/resources/META-INF/spring</font></p><pre><code class="language-">com.jagger.common.im.IMAutoConfiguration</code></pre>]]>
                    </description>
                    <pubDate>Wed, 26 Feb 2025 00:55:27 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Spring Boot Gateway 网关配置，Nacos服务发现注册。]]>
                    </title>
                    <link>http://halo.ljdzsk.com/archives/springbootgateway-wang-guan-pei-zhi-nacos-fu-wu-fa-xian-zhu-ce-</link>
                    <description>
                            <![CDATA[<ol><li>添加相关配置</li><li>添加过滤器，路由管理器配置，针对路由进行鉴权、服务转发</li></ol><h2 id="%E6%B7%BB%E5%8A%A0%E7%9B%B8%E5%85%B3%E9%85%8D%E7%BD%AE" tabindex="-1">添加相关配置</h2><p>pom.xml文件添加mvn包</p><pre><code class="language-">        &lt;!-- SpringCloud Alibaba Nacos 服务注册 --&gt;        &lt;dependency&gt;            &lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;            &lt;artifactId&gt;spring-cloud-starter-alibaba-nacos-discovery&lt;/artifactId&gt;        &lt;/dependency&gt;        &lt;!-- SpringCloud Alibaba Nacos 配置中心 --&gt;        &lt;dependency&gt;            &lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;            &lt;artifactId&gt;spring-cloud-starter-alibaba-nacos-config&lt;/artifactId&gt;        &lt;/dependency&gt;        &lt;!-- spring gateway 网关 --&gt;&lt;dependencies&gt;&lt;dependency&gt;&lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;&lt;artifactId&gt;spring-cloud-starter-gateway&lt;/artifactId&gt;&lt;/dependency&gt;&lt;/dependencies&gt;</code></pre><p>bootstrap.yml添加配置</p><pre><code class="language-">server:  port: 8080spring:  application:    name: gateway  profiles:    active: dev  cloud:    nacos:      discovery: # 服务注册、发现        server-addr: 127.0.0.1:8848        username: nacos        password: nacos        service: ${spring.application.name}-${spring.profiles.active}      config:        server-addr: 127.0.0.1:8848        username: nacos        password: nacos        file-extension: yml        shared-configs: # 指定额外的配置文件          - data-id: application.yml    loadbalancer:      nacos:        enabled: true # 开启nacos负载均衡策略    gateway:      routes: # 网关路由配置        - id: auth-service # 路由id，自定义，只要唯一即可          #uri: http://127.0.0.1:8088 # 路由的目标地址 http就是固定地址          uri: lb://auth-${spring.profiles.active} # 路由的目标地址 lb就是负载均衡，后面跟服务名称          predicates: # 路由断言，也就是判断请求是否符合路由规则的条件            - Path=/auth/** # 这个是按照路径匹配，只要以 /auth/ 开头就符合要求          filters: # 路由过滤器，对请求或者响应做出处理            - StripPrefix=1 # 去掉路径一条前缀，这里是: /auth          metadata:            auth-white-list: # 过滤器白名单列表，目前只配置login路由白名单              - /login/**</code></pre><p>此时启动gateway、auth服务，访问http://127.0.0.1:8080/auth/login服务就会自动转发到http://auth-dev/login，auth-dev就是Nacos注册的鉴权服务。<br />auth服务路由代码可以参考这篇文章最后一段Controller代码: <a href="https://halo.ljdzsk.com/archives/springbootswagger3-tong-yi-jie-kou-xiang-ying" target="_blank">https://halo.ljdzsk.com/archives/springbootswagger3-tong-yi-jie-kou-xiang-ying</a></p><h2 id="%E6%B7%BB%E5%8A%A0%E8%BF%87%E6%BB%A4%E5%99%A8%EF%BC%8C%E8%B7%AF%E7%94%B1%E7%AE%A1%E7%90%86%E5%99%A8%E9%85%8D%E7%BD%AE%EF%BC%8C%E9%92%88%E5%AF%B9%E8%B7%AF%E7%94%B1%E8%BF%9B%E8%A1%8C%E9%89%B4%E6%9D%83%E3%80%81%E6%9C%8D%E5%8A%A1%E8%BD%AC%E5%8F%91" tabindex="-1">添加过滤器，路由管理器配置，针对路由进行鉴权、服务转发</h2><pre><code class="language-">import com.fasterxml.jackson.databind.ObjectMapper;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.cloud.gateway.filter.GatewayFilterChain;import org.springframework.cloud.gateway.filter.GlobalFilter;import org.springframework.cloud.gateway.route.Route;import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;import org.springframework.stereotype.Component;import org.springframework.util.AntPathMatcher;import org.springframework.util.PathMatcher;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Mono;import java.util.List;@Slf4j@Component@RequiredArgsConstructorpublic class AuthFilter implements GlobalFilter {    private final RouteMetadataService routeMetadataService;    private final ObjectMapper objectMapper; // JSON 序列化工具    private final PathMatcher pathMatcher = new AntPathMatcher();  // Spring内置的路径匹配器    @Override    public Mono&lt;Void&gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) {        log.info(&quot;开始鉴权&quot;);        Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);        if (route != null) {            // 过滤白名单路由鉴权            List&lt;String&gt; authWhiteList = routeMetadataService.getauthWhiteList(route.getId());            String path = exchange.getRequest().getURI().getPath();            if (authWhiteList.stream().anyMatch(pattern -&gt; pathMatcher.match(pattern, path))) {                return chain.filter(exchange);            }        }        // 从请求头或参数中获取Token        String token = exchange.getRequest().getHeaders().getFirst(&quot;Authorization&quot;);        // 验证Token逻辑（如调用Auth服务）        if (!isValidToken(token)) {            return Result.error(HttpStatus.UNAUTHORIZED).toMono(exchange.getResponse(), objectMapper);        }        return chain.filter(exchange);    }    /***     * 鉴权判断     */    private boolean isValidToken(String token) {        return false;    }}</code></pre><p>路由管理服务</p><pre><code class="language-">import jakarta.annotation.PostConstruct;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.cloud.gateway.route.Route;import org.springframework.cloud.gateway.route.RouteLocator;import org.springframework.stereotype.Component;import reactor.core.publisher.Flux;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.Optional;import java.util.stream.Collectors;/*** * 路由管理 */@Component@RequiredArgsConstructor@Slf4jpublic class RouteMetadataService {    private final RouteLocator routeLocator;    private final Map&lt;String, List&lt;String&gt;&gt; routeExcludePaths = new HashMap&lt;&gt;();    /**     * 加载所有路由的元数据     */    public void loadMetadata() {        Flux&lt;Route&gt; routes = routeLocator.getRoutes();        routes.subscribe(route -&gt; {            // 从元数据获取鉴权白名单，路径在白名单中，无需鉴权            List&lt;String&gt; authWhiteList = Optional.ofNullable(route.getMetadata().get(&quot;auth-white-list&quot;))                    .map(obj -&gt; {                        if (obj instanceof List) {                            // 直接转换为List&lt;String&gt;                            return (List&lt;String&gt;) obj;                        } else if (obj instanceof Map) {                            // 如果是Map，提取所有值组成List                            Map&lt;?, ?&gt; map = (Map&lt;?, ?&gt;) obj;                            return map.values().stream()                                    .filter(String.class::isInstance)                                    .map(String.class::cast)                                    .collect(Collectors.toList());                        } else {                            log.warn(&quot;auth-white-list 类型不匹配: {}&quot;, obj.getClass().getName());                            return List.&lt;String&gt;of();                        }                    })                    .orElse(List.of());  // 默认返回空列表            routeExcludePaths.put(route.getId(), authWhiteList);        });    }    /**     * 获取指定路由的白名单路径列表     */    public List&lt;String&gt; getauthWhiteList(String routeId) {        return routeExcludePaths.getOrDefault(routeId, List.of());    }    /**     * 动态路由更新时调用此方法刷新缓存     */    @PostConstruct    public void refreshMetadata() {        routeExcludePaths.clear();        loadMetadata();    }}</code></pre><p>此时启动gateway、auth服务，访问：<a href="http://127.0.0.1:8080/auth/login%E5%8F%AF%E4%BB%A5%E6%AD%A3%E5%B8%B8%E8%BD%AC%E5%8F%91%EF%BC%8Chttp://127.0.0.1:8080/auth/login1" target="_blank">http://127.0.0.1:8080/auth/login可以正常转发，http://127.0.0.1:8080/auth/login1</a> 会提示没有权限。</p>]]>
                    </description>
                    <pubDate>Tue, 25 Feb 2025 22:30:46 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Swagger Ui 404 问题，更新配置后不显示Controller对应API问题。]]>
                    </title>
                    <link>http://halo.ljdzsk.com/archives/swaggerui404-wen-ti</link>
                    <description>
                            <![CDATA[<ol><li>使用<code>springdoc-openapi</code>时，将<code>spring-boot-starter-web</code> 替换 <code>spring-boot-starter-webflux</code>导致，此时可能忘记替换<code>springdoc-openapi-starter-webflux-ui</code></li><li>配置文件问题，检查<code>path</code>是否正确配置</li><li><code>WebMvcConfigurer</code>、<code>WebFluxConfigurer</code>配置问题</li><li>是否有封装统一全局响应，没有排除swagger api，导致swagger ui api 获取的数据格式不正确。</li><li>浏览器缓存: <font color="red"><strong>一定要强制刷新或者通过无痕浏览器访问配置更新后的swagger ui</strong></font>。</li></ol><h2 id="spring-boot-starter-web-%E6%9B%BF%E6%8D%A2-spring-boot-starter-webflux%E5%AF%BC%E8%87%B4" tabindex="-1"><code>spring-boot-starter-web</code> 替换 <code>spring-boot-starter-webflux</code>导致</h2><ol><li><code>spring-boot-starter-web</code>对应使用<code>springdoc-openapi-starter-webmvc-ui</code>，<code>spring-boot-starter-webflux</code>对应使用<code>springdoc-openapi-starter-webflux-ui</code>，需要一起还</li></ol><h2 id="%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6path%E6%AD%A3%E7%A1%AE%E9%85%8D%E7%BD%AE" tabindex="-1">配置文件<code>path</code>正确配置</h2><p>检查配置文件<code>path</code>是否正确<br />注：<code>webmvc-ui</code>会带有前缀，<code>/swagger-ui</code>，此时访问<code>http://localhost:8080/swagger-ui/index.html#/</code>,才是正确路径<br /><code>webflux-ui</code>会带有前缀，<code>/webjars/swagger-ui</code>，此时访问<code>http://localhost:8080/webjars/swagger-ui/index.html#/</code>,才是正确路径</p><pre><code class="language-">springdoc:  api-docs:    enabled: true    # 是否开启 OpenAPI JSON 接口    path: /v3/api-docs # 默认路径  swagger-ui:    enabled: true    path: /index.html # 自定义 Swagger UI 路径，访问此路径会重定向到/swagger-ui/index.html    disable-swagger-default-url: true # 禁用 swagger 默认自带的示例 ui  webjars:    prefix: # 禁用默认的 /webjars 路径，webflux 会访问到 /webjars/swagger-ui/index.html</code></pre><h2 id="webmvcconfigurer%E3%80%81webfluxconfigurer%E9%85%8D%E7%BD%AE%E9%97%AE%E9%A2%98" tabindex="-1"><code>WebMvcConfigurer</code>、<code>WebFluxConfigurer</code>配置问题</h2><p>检查是否实现<code>WebMvcConfigurer</code>，实现是否有问题，<code>WebFluxConfigurer</code>同理只是继承改成<code>WebFluxConfigurer</code>。</p><pre><code class="language-">@Configurationpublic class WebConfig implements WebMvcConfigurer {    @Override    public void addResourceHandlers(ResourceHandlerRegistry registry) {        registry.addResourceHandler(&quot;/**&quot;).addResourceLocations(&quot;classpath:/static/&quot;);        registry.addResourceHandler(&quot;swagger-ui.html&quot;)            .addResourceLocations(&quot;classpath:/META-INF/resources/&quot;);        registry.addResourceHandler(&quot;/webjars/**&quot;)            .addResourceLocations(&quot;classpath:/META-INF/resources/webjars/&quot;);    }}</code></pre>]]>
                    </description>
                    <pubDate>Fri, 21 Feb 2025 23:18:16 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Netty自定义通信协议，实现IM（即时通讯）]]>
                    </title>
                    <link>http://halo.ljdzsk.com/archives/netty-zi-ding-yi-tong-xin-xie-yi--shi-xian-im-ji-shi-tong-xun-</link>
                    <description>
                            <![CDATA[<ol><li>Netty是什么？</li><li>Netty核心组件</li><li>组件协作流程</li><li>实战代码Demo</li></ol><h3 id="netty%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F" tabindex="-1">Netty是什么？</h3><p>（抄的）Netty是一个高性能、异步的网络应用程序框架，可以轻松地开发基于TCP、UDP和HTTP等协议的网络应用程序。它是基于Java NIO技术实现的，具有较高的性能和可扩展性。Netty不仅可以用于开发网络客户端和服务器端，还可以用于开发其他类型的网络应用程序，如网络代理、网关和中间件等。<br />Netty的主要作用是为开发人员提供一个高效、可靠和可扩展的网络通信框架，从而降低网络应用程序的开发难度和维护成本。它提供了一系列的编解码器、处理器和协议支持，使得开发人员可以更加专注于业务逻辑的实现，而不必关心底层网络通信细节。</p><h3 id="netty%E6%A0%B8%E5%BF%83%E7%BB%84%E4%BB%B6" tabindex="-1">Netty核心组件</h3><h4 id="channel%EF%BC%88%E9%80%9A%E9%81%93%EF%BC%89" tabindex="-1"><strong>Channel（通道）</strong></h4><ul><li><strong>作用</strong>：抽象了网络连接（如 Socket），提供统一的 API 操作（读、写、绑定、连接等），支持多种传输类型（NIO、Epoll、OIO 等）。</li><li><strong>关键实现</strong>：<code>NioSocketChannel</code>（TCP 客户端）、<code>NioServerSocketChannel</code>（TCP 服务端）。</li></ul><h4 id="eventloop-%26-eventloopgroup%EF%BC%88%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF%E5%92%8C%E7%BA%BF%E7%A8%8B%E7%BB%84%EF%BC%89" tabindex="-1"><strong>EventLoop &amp; EventLoopGroup（事件循环和线程组）</strong></h4><ul><li><strong>EventLoop</strong>：<ul><li>每个 <code>EventLoop</code> 绑定一个线程，负责处理多个 <code>Channel</code> 的 I/O 事件和异步任务。</li><li>遵循 <strong>单线程模型</strong>，确保线程安全。</li></ul></li><li><strong>EventLoopGroup</strong>：<ul><li>管理一组 <code>EventLoop</code>，通常分为 <code>bossGroup</code>（接收连接）和 <code>workerGroup</code>（处理 I/O）。</li><li>示例：<code>NioEventLoopGroup</code>。</li></ul></li></ul><h4 id="channelhandler-%26-channelpipeline%EF%BC%88%E5%A4%84%E7%90%86%E5%99%A8%E5%92%8C%E5%A4%84%E7%90%86%E9%93%BE%EF%BC%89" tabindex="-1"><strong>ChannelHandler &amp; ChannelPipeline（处理器和处理链）</strong></h4><ul><li><strong>ChannelHandler</strong>：<ul><li>处理入站/出站事件和数据，用户自定义逻辑的核心扩展点。</li><li>分类：<ul><li><code>ChannelInboundHandler</code>（处理连接建立、数据读取等入站事件）。</li><li><code>ChannelOutboundHandler</code>（处理连接关闭、数据写入等出站操作）。</li><li><code>ChannelDuplexHandler</code>（继承以上两个类，出、入站都会处理）。</li><li><code>ChannelInitializer</code> (继承<code>ChannelInboundHandler</code>，在连接建立时初始化)。</li></ul></li></ul></li><li><strong>ChannelPipeline</strong>：<ul><li>由多个 <code>ChannelHandler</code> 组成的责任链，数据按顺序流经各个处理器。</li><li>支持动态增删处理器。</li></ul></li></ul><h4 id="bytebuf%EF%BC%88%E6%95%B0%E6%8D%AE%E5%AE%B9%E5%99%A8%EF%BC%89" tabindex="-1"><strong>ByteBuf（数据容器）</strong></h4><ul><li><strong>作用</strong>：替代 Java NIO 的 <code>ByteBuffer</code>，提供更高效灵活的数据存储（如池化内存、零拷贝优化）。</li><li><strong>特性</strong>：支持读写索引分离、引用计数、复合缓冲区等。</li></ul><h4 id="bootstrap-%26-serverbootstrap%EF%BC%88%E5%90%AF%E5%8A%A8%E5%BC%95%E5%AF%BC%E7%B1%BB%EF%BC%89" tabindex="-1"><strong>Bootstrap &amp; ServerBootstrap（启动引导类）</strong></h4><ul><li><strong>Bootstrap</strong>：<ul><li>客户端启动类，配置线程组、Channel 类型、处理器等。</li></ul></li><li><strong>ServerBootstrap</strong>：<ul><li>服务端启动类，额外绑定端口并管理 <code>bossGroup</code> 和 <code>workerGroup</code>。</li></ul></li></ul><h4 id="channelfuture%EF%BC%88%E5%BC%82%E6%AD%A5%E7%BB%93%E6%9E%9C%EF%BC%89" tabindex="-1"><strong>ChannelFuture（异步结果）</strong></h4><ul><li><strong>作用</strong>：表示异步 I/O 操作的结果（如连接、写入），通过添加监听器 (<code>ChannelFutureListener</code>) 实现回调逻辑。</li><li><strong>示例</strong>：<code>channel.writeAndFlush(data).addListener(...)</code>。</li></ul><h4 id="%E7%BC%96%E8%A7%A3%E7%A0%81%E5%99%A8%EF%BC%88codec%EF%BC%89" tabindex="-1"><strong>编解码器（Codec）</strong></h4><ul><li><strong>作用</strong>：将原始字节流与业务对象相互转换（如 HTTP 协议解析、Protobuf 序列化）。</li><li><strong>常见实现</strong>：<ul><li><code>MessageToByteEncoder</code>（编码器）、<code>ByteToMessageDecoder</code>（解码器）。</li><li>内置编解码器：<code>StringEncoder/Decoder</code>、<code>ObjectEncoder/Decoder</code> 等。</li></ul></li></ul><h4 id="channelhandlercontext%EF%BC%88%E5%A4%84%E7%90%86%E5%99%A8%E4%B8%8A%E4%B8%8B%E6%96%87%EF%BC%89" tabindex="-1"><strong>ChannelHandlerContext（处理器上下文）</strong></h4><ul><li><strong>作用</strong>：关联 <code>ChannelHandler</code> 与 <code>ChannelPipeline</code>，提供方法触发事件传播（如 <code>ctx.write()</code>）或获取 Channel 信息。</li></ul><h3 id="%E7%BB%84%E4%BB%B6%E5%8D%8F%E4%BD%9C%E6%B5%81%E7%A8%8B" tabindex="-1"><strong>组件协作流程</strong></h3><ol><li>通过 <code>Bootstrap</code>、<code>ServerBootstrap</code> 配置并启动服务端、客户端。</li><li><code>ChannelInitializer</code> 添加连接建立时初始化内容。</li><li>通过添加<code>ChannelInboundHandler</code>、<code>ChannelOutboundHandler</code> 配置数据出、入站处理。</li><li>操作 <code>ByteBuf</code> 数据类或 <code>ChannelFuture</code> 异步回调控制业务逻辑。</li></ol><h3 id="%E5%AE%9E%E6%88%98%E4%BB%A3%E7%A0%81demo" tabindex="-1">实战代码Demo</h3><h4 id="spring-boot-%E6%B7%BB%E5%8A%A0netty%E5%8C%85" tabindex="-1">Spring boot 添加Netty包</h4><pre><code class="language-">        &lt;!-- 通过注解，添加语法糖--&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;            &lt;artifactId&gt;lombok&lt;/artifactId&gt;        &lt;/dependency&gt;        &lt;!-- json解析--&gt;        &lt;dependency&gt;            &lt;groupId&gt;com.alibaba.fastjson2&lt;/groupId&gt;            &lt;artifactId&gt;fastjson2&lt;/artifactId&gt;            &lt;version&gt;2.0.54&lt;/version&gt;        &lt;/dependency&gt;            &lt;!-- NIO框架，主要用于IM实时通讯  --&gt;            &lt;!-- https://mvnrepository.com/artifact/io.netty/netty-all --&gt;            &lt;dependency&gt;                &lt;groupId&gt;io.netty&lt;/groupId&gt;                &lt;artifactId&gt;netty-all&lt;/artifactId&gt;                &lt;version&gt;4.1.118.Final&lt;/version&gt;            &lt;/dependency&gt;                    &lt;!-- 如果使用 webflux 默认集成Netty，就不需要上面的 netty-all 包 --&gt;        &lt;!-- 使用 webflux 响应式流（Reactive Streams）默认通过 Netty 启动排除 Tomcat --&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;            &lt;artifactId&gt;spring-boot-starter-webflux&lt;/artifactId&gt;            &lt;exclusions&gt;                &lt;exclusion&gt;                    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;                    &lt;artifactId&gt;spring-boot-starter-tomcat&lt;/artifactId&gt;                &lt;/exclusion&gt;            &lt;/exclusions&gt;        &lt;/dependency&gt;</code></pre><h4 id="%E8%87%AA%E5%AE%9A%E4%B9%89%E9%85%8D%E7%BD%AE%E9%A1%B9%EF%BC%8C%E6%96%B9%E4%BE%BFbootstrap.yml%E9%85%8D%E7%BD%AE" tabindex="-1">自定义配置项，方便<code>bootstrap.yml</code>配置</h4><pre><code class="language-">import lombok.Data;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Configuration;@Configuration@ConfigurationProperties(prefix = &quot;netty&quot;)@Datapublic class NettyProperties {    /**     * boss线程数量     */    private Integer bossThread = 1;    /**     * worker线程数量     */    private Integer workerThread = 1;    /**     * 连接超时时间     */    private Integer timeout = 30000;    /**     * 服务器主端口     */    private Integer port = 9000;    /**     * 服务器地址 默认为本地     *///    private String host = &quot;127.0.0.1&quot;;}</code></pre><h4 id="%E7%BC%96%E5%86%99%E6%B6%88%E6%81%AF%E7%B1%BB%EF%BC%8C%E7%94%A8%E4%BA%8E%E4%BC%A0%E8%BE%93%E6%95%B0%E6%8D%AE" tabindex="-1">编写消息类，用于传输数据</h4><p>目前暂定只有默认<code>String</code>数据类，<code>ChatMessags</code>IM聊天数据类（可以自行添加model进行编写序列化代码）</p><pre><code class="language-">import com.alibaba.fastjson2.JSONObject;import com.jagger.model.ChatMessage;import lombok.Data;import lombok.Getter;import lombok.extern.slf4j.Slf4j;@Slf4j@Datapublic class Message {    // 消息类型，占用 2 字节    private short type;    // 消息长度，占用 4 字节    private int length;    // 消息内容    private byte[] body;    /***     * {@link Object} 转 {@link Message} 类型     * @return {@link Message}     */    public static Message objectToMessage(Object body) {        Message message = new Message();        message.setType(Message.MessageType.getType(body).getValue());        if (message.getType() == Message.MessageType.STRING.getValue()) {            message.setBody(((String) body).getBytes());        } else {            String bodyJsonStr = JSONObject.toJSONString(body);            message.setBody(bodyJsonStr.getBytes());            message.setLength(message.getBody().length);        }        return message;    }    /***     * {@link Message} 转 {@link Object} 类型     * @return {@link Object}     */    public static Object messageToObject(Message message) {        String bodyStr = new String(message.getBody());        if (message.getType() == Message.MessageType.STRING.getValue()) {            return bodyStr;        } else if (message.getType() == MessageType.CHAT_MESSAGE.getValue()) {            return JSONObject.parseObject(bodyStr, ChatMessage.class);        } else {            throw new RuntimeException(&quot;Message 转 Object 错误，未定义对象类型。&quot;);        }    }    /***     * {@link Message} 转 {@link Object} 类型     * @return {@link Object}     */    public Object toObject() {        return messageToObject(this);    }    /***     * 消息类型     */    @Getter    public enum MessageType {        STRING((short) 0),        CHAT_MESSAGE((short) 1); // 聊天消息        private final short value;        private MessageType(short value) {            this.value = value;        }        public static MessageType getType(Object o) {            if (o instanceof String) {                return STRING;            } else if (o instanceof ChatMessage) {                return CHAT_MESSAGE;            } else {                throw new RuntimeException(&quot;不存在消息类型: &quot; + o);            }        }    }}</code></pre><h4 id="%E5%AE%9A%E4%B9%89%E6%95%B0%E6%8D%AE%E5%87%BA%E3%80%81%E5%85%A5%E7%AB%99%E5%A4%84%E7%90%86%E7%B1%BB" tabindex="-1">定义数据出、入站处理类</h4><ul><li>入站类数据处理，继承实现<code>ChannelInboundHandlerAdapter</code><ul><li><code>MessageDecodeHandler</code>Message数据解码，继承实现<code>ByteToMessageDecoder</code></li><li><code>BusinessHandler</code>业务数据处理</li></ul></li><li>出站类数据处理，继承实现<code>ChannelOutboundHandlerAdapter</code><ul><li><code>ObjectToMessageHandler</code>对象封装，<code>Object</code>转<code>Message</code></li><li><code>MessageEncoderHandler</code>数据编码</li><li><code>PushHandler</code>数据推送前最后操作</li></ul></li><li>出、入站数据处理统一处理，继承实现<code>ChannelDuplexHandler</code><ul><li><code>ExceptionHandler</code>异常数据处理</li></ul></li></ul><h5 id="messagedecodehandlermessage%E6%95%B0%E6%8D%AE%E8%A7%A3%E7%A0%81" tabindex="-1"><code>MessageDecodeHandler</code>Message数据解码</h5><pre><code class="language-">import io.netty.buffer.ByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.handler.codec.ByteToMessageDecoder;import lombok.extern.slf4j.Slf4j;import java.util.List;/*** * 消息解码器 */@Slf4jpublic class MessageDecodeHandler extends ByteToMessageDecoder {    @Override    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List&lt;Object&gt; list) throws Exception {        log.debug(&quot;开始解码&quot;);        Message message = new Message();        message.setType(byteBuf.readShort());        //如果byteBuf剩下的长度还有大于4个字节，说明body不为空        if (byteBuf.readableBytes() &gt; 4) {            message.setLength(byteBuf.readInt());            byte[] contents = new byte[message.getLength()];            byteBuf.readBytes(contents, 0, message.getLength());            message.setBody(contents);            list.add(message);            log.debug(&quot;成功解码，接收消息类型: {}&quot;, message.getType());        } else {            log.warn(&quot;解码失败: 消息内容为空&quot;);        }    }}</code></pre><p><code>BusinessHandler</code>业务数据处理</p><pre><code class="language-">import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;import lombok.extern.slf4j.Slf4j;/*** * 业务处理 */@Slf4jpublic class BusinessHandler extends ChannelInboundHandlerAdapter {    @Override    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {        log.debug(&quot;开始业务处理&quot;);        log.debug(&quot;业务数据: {}&quot;, ((Message) msg).toObject());    }}</code></pre><p><code>ObjectToMessageHandler</code>对象封装</p><pre><code class="language-">import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelOutboundHandlerAdapter;import io.netty.channel.ChannelPromise;import lombok.extern.slf4j.Slf4j;@Slf4jpublic class ObjectToMessageHandler extends ChannelOutboundHandlerAdapter {    @Override    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {        log.debug(&quot;分析数据类型&quot;);        super.write(ctx, Message.objectToMessage(msg), promise);    }}</code></pre><p><code>MessageEncoderHandler</code>Message数据编码</p><pre><code class="language-">import io.netty.buffer.ByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.handler.codec.MessageToByteEncoder;import lombok.extern.slf4j.Slf4j;/*** * 消息编码器 */@Slf4jpublic class MessageEncoderHandler extends MessageToByteEncoder&lt;Message&gt; {    @Override    protected void encode(ChannelHandlerContext channelHandlerContext, Message message, ByteBuf byteBuf) throws Exception {        log.debug(&quot;开始编码&quot;);        byteBuf.writeShort(message.getType());        if (message.getBody() != null) {            byteBuf.writeInt(message.getBody().length);            byteBuf.writeBytes(message.getBody());        }        log.debug(&quot;成功编码&quot;);    }}</code></pre><h5 id="pushhandler%E6%95%B0%E6%8D%AE%E6%8E%A8%E9%80%81%E5%89%8D%E6%9C%80%E5%90%8E%E6%93%8D%E4%BD%9C" tabindex="-1"><code>PushHandler</code>数据推送前最后操作</h5><pre><code class="language-">import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelOutboundHandlerAdapter;import io.netty.channel.ChannelPromise;import lombok.extern.slf4j.Slf4j;/*** * 推送消息时处理 */@Slf4jpublic class PushHandler extends ChannelOutboundHandlerAdapter {    @Override    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {        log.debug(&quot;分析数据类型&quot;);        super.write(ctx, msg, promise);    }}</code></pre><h5 id="exceptionhandler%E5%BC%82%E5%B8%B8%E6%95%B0%E6%8D%AE%E5%A4%84%E7%90%86" tabindex="-1"><code>ExceptionHandler</code>异常数据处理</h5><pre><code class="language-">/*** * ChannelDuplexHandler 它同时实现了 ChannelInboundHandler 和 ChannelOutboundHandler 接口 * ChannelInboundHandler 所有异常会再最后统一处理 * ChannelOutboundHandler 所有异常需要通过 addListener 监听事件才能处理，需要放在第一位先添加监听 * ExceptionHandler 作为全局异常处理，放在最后正好 in 最后处理，out 最先处理 * */@Slf4jpublic class ExceptionHandler extends ChannelDuplexHandler {    /***     * 接收数据异常处理     * @param ctx     * @param cause     * @throws Exception     */    @Override    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {        log.error(&quot;接收消息处理异常: &quot;, cause);    }    /***     * 出站数据异常处理     * @param ctx     * @param msg     * @param promise     */    @Override    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {        ctx.write(msg, promise.addListener(future -&gt; {            if (!future.isSuccess()) {                // 处理异常                log.error(&quot;推送消息异常&quot;, future.cause());            }        }));    }}</code></pre><h4 id="%E7%BC%96%E5%86%99%E6%9C%8D%E5%8A%A1%E7%AB%AF%E7%B1%BBnettyserver%EF%BC%8C%E6%9C%8D%E5%8A%A1%E7%AB%AF%E8%BF%9E%E6%8E%A5%E5%88%9D%E5%A7%8B%E5%8C%96%E7%B1%BBnettyserverinit" tabindex="-1">编写服务端类<code>NettyServer</code>，服务端连接初始化类<code>NettyServerInit</code></h4><p>初始化添加<code>Handler</code>实现要注意顺序，<code>ChannelInboundHandler</code>先添加先调用，<code>ChannelOutboundHandler</code>后添加先调用。</p><pre><code class="language-">import io.netty.bootstrap.ServerBootstrap;import io.netty.channel.*;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioServerSocketChannel;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;@Slf4j@Component@RequiredArgsConstructorpublic class NettyServer {    private final NettyProperties nettyProperties;    private EventLoopGroup bossGroup; // 负责处理连接请求的线程组（NIO 事件循环组）    private EventLoopGroup workerGroup; // 负责处理 I/O 事件的线程组（NIO 事件循环组）    private ChannelFuture channelFuture; // 代表异步操作的结果    public void start() throws InterruptedException {        log.info(&quot;NettyServer 启动&quot;);        bossGroup = new NioEventLoopGroup(nettyProperties.getBossThread()); // 创建 boss 线程组，用于处理连接请求        workerGroup = new NioEventLoopGroup(nettyProperties.getWorkerThread()); // 创建 worker 线程组，用于处理 I/O 事件        ServerBootstrap serverBootstrap = new ServerBootstrap();        serverBootstrap                // 设置线程组                .group(bossGroup, workerGroup)                // 设置为NIO模式                .channel(NioServerSocketChannel.class)                // 设置TCP sync队列大小, 防止洪泛攻击                .childOption(ChannelOption.SO_BACKLOG, 1024)                // 设置初始化类                .childHandler(new NettyServerInit());        channelFuture = serverBootstrap.bind(nettyProperties.getPort()).addListener(future -&gt; {            if (future.isSuccess()) {                log.info(&quot;Netty 服务启动，端口: {}&quot;, nettyProperties.getPort());            } else {                log.error(&quot;启动失败，请检查端口是否被占用: {}&quot;, nettyProperties.getPort(), future.cause());            }        }).sync();    }    /***     * 停止服务     */    public void stop() {        channelFuture.channel().close();        log.info(&quot;Netty 服务端停止&quot;);    }    /**     * 通过 Channel 发送数据     *     * @param body 文本数据     */    public void send(Object body) {        log.debug(&quot;服务端发送数据&quot;);        channelFuture.channel().writeAndFlush(body);    }    /***     * 初始化类     */    private static class NettyServerInit extends ChannelInitializer&lt;SocketChannel&gt; {        @Override        protected void initChannel(SocketChannel socketChannel) throws Exception {            log.info(&quot;新 Netty 连接: {}&quot;, socketChannel.remoteAddress());            // ChannelInboundHandlerAdapter 输入数据Handler实现调用顺序，先 addLast() 先处理            socketChannel.pipeline()                    .addLast(new MessageDecodeHandler())  // 数据解码                    .addLast(new BusinessHandler()) // 业务处理            ;            // ChannelOutboundHandlerAdapter 输出数据Handler实现调用顺序, 后 addLast() 先处理            socketChannel.pipeline()                    .addLast(new PushHandler())  // 数据推送前操作                    .addLast(new MessageEncoderHandler())  // 数据编码                    .addLast(new ObjectToMessageHandler())  // 数据编码            ;            // ChannelDuplexHandler 它同时实现了 ChannelInboundHandler 和 ChannelOutboundHandler 接口            // ChannelInboundHandler 所有异常会再最后统一处理            // ChannelOutboundHandler 所有异常需要通过 addListener 监听事件才能处理，需要放在第一位先添加监听            // ExceptionHandler 作为全局异常处理，放在最后正好 in 最后处理，out 最先处理            socketChannel.pipeline().addLast(new ExceptionHandler()); // 消息接收异常        }    }}</code></pre><h4 id="%E7%BC%96%E5%86%99%E5%AE%A2%E6%88%B7%E7%AB%AF%E7%B1%BBnettyclient%EF%BC%8C%E6%9C%8D%E5%8A%A1%E7%AB%AF%E8%BF%9E%E6%8E%A5%E5%88%9D%E5%A7%8B%E5%8C%96%E7%B1%BBnettyclientinit" tabindex="-1">编写客户端类<code>NettyClient</code>，服务端连接初始化类<code>NettyClientInit</code></h4><p>注意点同服务端。</p><pre><code class="language-">import io.netty.bootstrap.Bootstrap;import io.netty.channel.*;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioSocketChannel;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;@Component@RequiredArgsConstructor@Slf4jpublic class NettyClient {    private final NettyProperties nettyProperties;    private ChannelFuture channelFuture;    Bootstrap bootstrap;    public void start() throws InterruptedException {        log.info(&quot;NettyClient 启动&quot;);        NioEventLoopGroup eventExecutors = new NioEventLoopGroup(nettyProperties.getWorkerThread());        bootstrap = new Bootstrap();        bootstrap.group(eventExecutors)   // 指定线程组                .option(ChannelOption.SO_KEEPALIVE, true)                .channel(NioSocketChannel.class) // 指定通道                .handler(new NettyClientInit()); // 指定处理器        channelFuture = bootstrap.connect(&quot;127.0.0.1&quot;, nettyProperties.getPort()).addListener(future -&gt; {            log.info(&quot;客户端启动成功，并监听端口：{} &quot;, nettyProperties.getPort());        });    }    /**     * 客户端通过 Channel 对象向服务器端发送数据     *     * @param body 文本数据     */    public void send(Object body) {        log.debug(&quot;客户端发送数据&quot;);        channelFuture.channel().writeAndFlush(body);    }    /***     * 停止服务     */    public void stop() {        channelFuture.channel().close();        log.info(&quot;Netty 客户端服务停止&quot;);    }    /***     * 客户端初始化     */    public static class NettyClientInit extends ChannelInitializer&lt;SocketChannel&gt; {        @Override        protected void initChannel(SocketChannel socketChannel) throws Exception {            log.debug(&quot;初始化 Netty 客户端&quot;);            socketChannel.pipeline()                    .addLast(new MessageDecodeHandler())  // 数据解码                    .addLast(new ClientHandler()) // 客户端handler            ;            socketChannel.pipeline()                    .addLast(new PushHandler())  // 数据推送前操作                    .addLast(new MessageEncoderHandler())  // 数据编码                    .addLast(new ObjectToMessageHandler())  // 对象转Message            ;            socketChannel.pipeline().addLast(new ExceptionHandler()); // 消息接收异常        }    }}</code></pre><h4 id="%E7%BC%96%E5%86%99imrunner%E5%AE%9E%E7%8E%B0applicationrunner" tabindex="-1">编写<code>ImRunner</code>实现<code>ApplicationRunner</code></h4><p>在Spring boot启动时统一启动服务端、客户端，这里客户端、服务端都统一放到一个项目里好方便测试</p><pre><code class="language-">import jakarta.annotation.PreDestroy;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.boot.ApplicationArguments;import org.springframework.boot.ApplicationRunner;import org.springframework.stereotype.Component;@Slf4j@Component@RequiredArgsConstructorpublic class ImRunner implements ApplicationRunner {    private final NettyServer nettyServer;    private final NettyClient nettyClient;    @Override    public void run(ApplicationArguments args) throws Exception {        log.debug(&quot;启动IM服务&quot;);        nettyServer.start();        log.debug(&quot;启动 netty 客户端&quot;);        nettyClient.start();    }    @PreDestroy    public void destroy() {        log.debug(&quot;退出IM服务&quot;);        nettyServer.stop();        nettyClient.stop();    }}</code></pre><h5 id="%E6%B7%BB%E5%8A%A0controller%E6%B5%8B%E8%AF%95" tabindex="-1">添加<code>Controller</code>测试</h5><p>这里通过调用<code>NettyClient</code>模拟客户端发送数据，测试数据<code>ChatMessage</code>为自定义model</p><pre><code class="language-">@Slf4j@RestController@RequiredArgsConstructorpublic class ChatController implements ChatApi {    private final NettyClient nettyClient;        @Override    public List&lt;ChatMessage&gt; getMessages(int chatRoomId, int serialNumber, int messagesNumber) {        nettyClient.send(&quot;测试数据&quot;);        nettyClient.send(new ChatMessage());        return null;    }}</code></pre><p>可以看到后台日志正常输出。<br /><img src="/upload/2025/02/image.png" alt="image" /></p>]]>
                    </description>
                    <pubDate>Thu, 20 Feb 2025 22:41:42 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Spring boot、Swagger 3 统一接口响应，处理异常统一返回。]]>
                    </title>
                    <link>http://halo.ljdzsk.com/archives/springbootswagger3-tong-yi-jie-kou-xiang-ying</link>
                    <description>
                            <![CDATA[<ol><li>统一配置</li><li>定义统一响应类 <code>Result&lt;T&gt;</code></li><li>WebMvc配置<br />3.1. 安装依赖，添加配置<br />3.2. 自定义<code>@RestControllerAdvice</code> + <code>ResponseBodyAdvice</code>自动<code>Result&lt;T&gt;</code>包装返回值</li><li>WebFlux配置<br />4.1. 安装依赖，添加配置<br />4.2. 自定义<code>GlobalResponseHandler</code>自动<code>Result&lt;T&gt;</code>包装返回值<br />4.3. 自定义<code>WebFluxConfiguration</code>并注入<code>GlobalResponseHandler</code></li><li>自定义<code>GlobalExceptionHandler</code>监听全局异常WebMvc、WebFlux通用</li><li>自定义<code>ModelConverter</code>修改Swagger Ui 显示Api文档，使其匹配<code>Result&lt;T&gt;</code>包装的返回值</li><li>同步Spring Boot和Swagger Ui的<code>ObjectMapper</code>，统一修改返回值驼峰格式为蛇形格式（下划线）</li></ol><h3 id="1.-%E5%AE%89%E8%A3%85%E4%BE%9D%E8%B5%96" tabindex="-1">1. 安装依赖</h3><pre><code class="language-">&lt;!-- 通过注解，添加语法糖--&gt;&lt;dependency&gt;    &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;    &lt;artifactId&gt;lombok&lt;/artifactId&gt;&lt;/dependency&gt;</code></pre><p>添加配置到<code>bootstrap.yml</code></p><pre><code class="language-">springdoc:  api-docs:    enabled: true    # 是否开启 OpenAPI JSON 接口    path: /v3/api-docs # 默认路径  swagger-ui:    enabled: true    path: /index.html # 自定义 Swagger UI 路径，访问此路径会重定向到/swagger-ui/index.html    disable-swagger-default-url: true # 禁用 swagger 默认自带的示例 ui  webjars:    prefix: # 修改默认的 /webjars 路径为空  </code></pre><p>此时启动服务可以访问<code>http://127.0.0.1:8080/swagger-ui/index.html</code></p><h3 id="2.-%E5%AE%9A%E4%B9%89%E7%BB%9F%E4%B8%80%E5%93%8D%E5%BA%94%E7%B1%BB-result%3Ct%3E%EF%BC%8Chttp%E7%8A%B6%E6%80%81httpstatus" tabindex="-1">2. 定义统一响应类 <code>Result&lt;T&gt;</code>，http状态<code>HttpStatus</code></h3><p>坑点：<font color="red">Schema 泛型不能使用name属性，swagger-ui通过name区分唯一，会导致多个不同泛型api文档一样</font></p><pre><code class="language-">import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.ObjectMapper;import com.jagger.ai.common.core.constant.HttpStatus;import io.swagger.v3.oas.annotations.media.Schema;import lombok.Data;import lombok.extern.slf4j.Slf4j;import org.springframework.core.io.buffer.DataBuffer;import org.springframework.http.HttpStatusCode;import org.springframework.http.MediaType;import org.springframework.http.server.reactive.ServerHttpResponse;import reactor.core.publisher.Mono;@Slf4j@Data@Schema(description = &quot;Http统一响应&quot;) // 统一响应类(泛型) 不能使用 @Schema(name = &quot;&quot;)，swagger-ui通过name区分，导致多个泛型api文档一样public class Result&lt;T&gt; {    @Schema(description = &quot;状态码&quot;)    private int code;    @Schema(description = &quot;消息&quot;)    private String msg;    @Schema(description = &quot;数据&quot;)    private T data;    public Result(HttpStatus httpStatus, String msg, T data) {        this.code = httpStatus.value();        this.msg = msg;        this.data = data;    }    // 成功响应（无数据）    public static &lt;T&gt; Result&lt;T&gt; success() {        return Result.success(null);    }    // 成功响应（带数据）    public static &lt;T&gt; Result&lt;T&gt; success(T data) {        return new Result&lt;&gt;(HttpStatus.OK, HttpStatus.OK.getReasonPhrase(), data);    }    // 错误响应    public static &lt;T&gt; Result&lt;T&gt; error(HttpStatus httpStatus) {        return Result.error(httpStatus, httpStatus.getReasonPhrase());    }    public static &lt;T&gt; Result&lt;T&gt; error(HttpStatus httpStatus, String msg) {        return Result.error(httpStatus, msg, null);    }    public static &lt;T&gt; Result&lt;T&gt; error(HttpStatus httpStatus, String msg, T data) {        return new Result&lt;&gt;(httpStatus, msg, data);    }    /***     * webflux 专用     */    public Mono&lt;Void&gt; toMono(ServerHttpResponse response, ObjectMapper objectMapper) {        response.setStatusCode(HttpStatusCode.valueOf(code));        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);        byte[] bytes = null;        try {            bytes = objectMapper.writeValueAsBytes(this);        } catch (JsonProcessingException e) {            log.error(e.getMessage(), e);            throw new RuntimeException(e);        }        DataBuffer buffer = response.bufferFactory().wrap(bytes);        return response.writeWith(Mono.just(buffer));    }}</code></pre><p>HttpStatus 定义返回值</p><pre><code class="language-">import org.springframework.lang.Nullable;/*** * 抄 {@link org.springframework.http.HttpStatus}，改汉化的 */public enum HttpStatus {    CONTINUE(100, HttpStatus.Series.INFORMATIONAL, &quot;继续&quot;),    SWITCHING_PROTOCOLS(101, HttpStatus.Series.INFORMATIONAL, &quot;切换协议&quot;),    PROCESSING(102, HttpStatus.Series.INFORMATIONAL, &quot;处理中&quot;),    EARLY_HINTS(103, HttpStatus.Series.INFORMATIONAL, &quot;早期提示&quot;),    OK(200, HttpStatus.Series.SUCCESSFUL, &quot;成功&quot;),    CREATED(201, HttpStatus.Series.SUCCESSFUL, &quot;已创建&quot;),    ACCEPTED(202, HttpStatus.Series.SUCCESSFUL, &quot;已接受&quot;),    NON_AUTHORITATIVE_INFORMATION(203, HttpStatus.Series.SUCCESSFUL, &quot;非权威信息&quot;),    NO_CONTENT(204, HttpStatus.Series.SUCCESSFUL, &quot;无内容&quot;),    RESET_CONTENT(205, HttpStatus.Series.SUCCESSFUL, &quot;重置内容&quot;),    PARTIAL_CONTENT(206, HttpStatus.Series.SUCCESSFUL, &quot;部分内容&quot;),    MULTI_STATUS(207, HttpStatus.Series.SUCCESSFUL, &quot;多状态&quot;),    ALREADY_REPORTED(208, HttpStatus.Series.SUCCESSFUL, &quot;已报告&quot;),    IM_USED(226, HttpStatus.Series.SUCCESSFUL, &quot;IM 已使用&quot;),    MULTIPLE_CHOICES(300, HttpStatus.Series.REDIRECTION, &quot;多种选择&quot;),    MOVED_PERMANENTLY(301, HttpStatus.Series.REDIRECTION, &quot;永久移动&quot;),    FOUND(302, HttpStatus.Series.REDIRECTION, &quot;已找到&quot;),    /** @deprecated */    @Deprecated    MOVED_TEMPORARILY(302, HttpStatus.Series.REDIRECTION, &quot;临时移动&quot;),    SEE_OTHER(303, HttpStatus.Series.REDIRECTION, &quot;查看其他&quot;),    NOT_MODIFIED(304, HttpStatus.Series.REDIRECTION, &quot;未修改&quot;),    /** @deprecated */    @Deprecated    USE_PROXY(305, HttpStatus.Series.REDIRECTION, &quot;使用代理&quot;),    TEMPORARY_REDIRECT(307, HttpStatus.Series.REDIRECTION, &quot;临时重定向&quot;),    PERMANENT_REDIRECT(308, HttpStatus.Series.REDIRECTION, &quot;永久重定向&quot;),    BAD_REQUEST(400, HttpStatus.Series.CLIENT_ERROR, &quot;错误请求&quot;),    UNAUTHORIZED(401, HttpStatus.Series.CLIENT_ERROR, &quot;鉴权不通过&quot;),    PAYMENT_REQUIRED(402, HttpStatus.Series.CLIENT_ERROR, &quot;需要付款&quot;),    FORBIDDEN(403, HttpStatus.Series.CLIENT_ERROR, &quot;禁止访问&quot;),    NOT_FOUND(404, HttpStatus.Series.CLIENT_ERROR, &quot;未找到&quot;),    METHOD_NOT_ALLOWED(405, HttpStatus.Series.CLIENT_ERROR, &quot;方法不允许&quot;),    NOT_ACCEPTABLE(406, HttpStatus.Series.CLIENT_ERROR, &quot;不可接受&quot;),    PROXY_AUTHENTICATION_REQUIRED(407, HttpStatus.Series.CLIENT_ERROR, &quot;需要代理认证&quot;),    REQUEST_TIMEOUT(408, HttpStatus.Series.CLIENT_ERROR, &quot;请求超时&quot;),    CONFLICT(409, HttpStatus.Series.CLIENT_ERROR, &quot;冲突&quot;),    GONE(410, HttpStatus.Series.CLIENT_ERROR, &quot;已删除&quot;),    LENGTH_REQUIRED(411, HttpStatus.Series.CLIENT_ERROR, &quot;需要长度&quot;),    PRECONDITION_FAILED(412, HttpStatus.Series.CLIENT_ERROR, &quot;前提条件失败&quot;),    PAYLOAD_TOO_LARGE(413, HttpStatus.Series.CLIENT_ERROR, &quot;负载过大&quot;),    /** @deprecated */    @Deprecated    REQUEST_ENTITY_TOO_LARGE(413, HttpStatus.Series.CLIENT_ERROR, &quot;请求实体过大&quot;),    URI_TOO_LONG(414, HttpStatus.Series.CLIENT_ERROR, &quot;URI 过长&quot;),    /** @deprecated */    @Deprecated    REQUEST_URI_TOO_LONG(414, HttpStatus.Series.CLIENT_ERROR, &quot;请求 URI 过长&quot;),    UNSUPPORTED_MEDIA_TYPE(415, HttpStatus.Series.CLIENT_ERROR, &quot;不支持的媒体类型&quot;),    REQUESTED_RANGE_NOT_SATISFIABLE(416, HttpStatus.Series.CLIENT_ERROR, &quot;请求范围无法满足&quot;),    EXPECTATION_FAILED(417, HttpStatus.Series.CLIENT_ERROR, &quot;期望失败&quot;),    I_AM_A_TEAPOT(418, HttpStatus.Series.CLIENT_ERROR, &quot;我是一个茶壶&quot;),    /** @deprecated */    @Deprecated    INSUFFICIENT_SPACE_ON_RESOURCE(419, HttpStatus.Series.CLIENT_ERROR, &quot;资源空间不足&quot;),    /** @deprecated */    @Deprecated    METHOD_FAILURE(420, HttpStatus.Series.CLIENT_ERROR, &quot;方法失败&quot;),    /** @deprecated */    @Deprecated    DESTINATION_LOCKED(421, HttpStatus.Series.CLIENT_ERROR, &quot;目标锁定&quot;),    UNPROCESSABLE_ENTITY(422, HttpStatus.Series.CLIENT_ERROR, &quot;无法处理的实体&quot;),    LOCKED(423, HttpStatus.Series.CLIENT_ERROR, &quot;锁定&quot;),    FAILED_DEPENDENCY(424, HttpStatus.Series.CLIENT_ERROR, &quot;依赖失败&quot;),    TOO_EARLY(425, HttpStatus.Series.CLIENT_ERROR, &quot;过早&quot;),    UPGRADE_REQUIRED(426, HttpStatus.Series.CLIENT_ERROR, &quot;需要升级&quot;),    PRECONDITION_REQUIRED(428, HttpStatus.Series.CLIENT_ERROR, &quot;需要前提条件&quot;),    TOO_MANY_REQUESTS(429, HttpStatus.Series.CLIENT_ERROR, &quot;请求过多&quot;),    REQUEST_HEADER_FIELDS_TOO_LARGE(431, HttpStatus.Series.CLIENT_ERROR, &quot;请求头字段过大&quot;),    UNAVAILABLE_FOR_LEGAL_REASONS(451, HttpStatus.Series.CLIENT_ERROR, &quot;因法律原因不可用&quot;),    INTERNAL_SERVER_ERROR(500, HttpStatus.Series.SERVER_ERROR, &quot;服务器错误&quot;),    NOT_IMPLEMENTED(501, HttpStatus.Series.SERVER_ERROR, &quot;未实现&quot;),    BAD_GATEWAY(502, HttpStatus.Series.SERVER_ERROR, &quot;网关错误&quot;),    SERVICE_UNAVAILABLE(503, HttpStatus.Series.SERVER_ERROR, &quot;服务不可用&quot;),    GATEWAY_TIMEOUT(504, HttpStatus.Series.SERVER_ERROR, &quot;网关超时&quot;),    HTTP_VERSION_NOT_SUPPORTED(505, HttpStatus.Series.SERVER_ERROR, &quot;HTTP 版本不支持&quot;),    VARIANT_ALSO_NEGOTIATES(506, HttpStatus.Series.SERVER_ERROR, &quot;变体也协商&quot;),    INSUFFICIENT_STORAGE(507, HttpStatus.Series.SERVER_ERROR, &quot;存储不足&quot;),    LOOP_DETECTED(508, HttpStatus.Series.SERVER_ERROR, &quot;检测到循环&quot;),    BANDWIDTH_LIMIT_EXCEEDED(509, HttpStatus.Series.SERVER_ERROR, &quot;带宽限制超出&quot;),    NOT_EXTENDED(510, HttpStatus.Series.SERVER_ERROR, &quot;未扩展&quot;),    NETWORK_AUTHENTICATION_REQUIRED(511, HttpStatus.Series.SERVER_ERROR, &quot;需要网络认证&quot;);    private static final HttpStatus[] VALUES = values();    private final int value;    private final HttpStatus.Series series;    private final String reasonPhrase;    private HttpStatus(int value, HttpStatus.Series series, String reasonPhrase) {        this.value = value;        this.series = series;        this.reasonPhrase = reasonPhrase;    }    public int value() {        return this.value;    }    public HttpStatus.Series series() {        return this.series;    }    public String getReasonPhrase() {        return this.reasonPhrase;    }    /***     * 转 org.springframework.http.HttpStatus     * @return     */    public org.springframework.http.HttpStatus toSpringHttpStatus(){        return org.springframework.http.HttpStatus.valueOf(this.value);    }    public boolean is1xxInformational() {        return this.series() == HttpStatus.Series.INFORMATIONAL;    }    public boolean is2xxSuccessful() {        return this.series() == HttpStatus.Series.SUCCESSFUL;    }    public boolean is3xxRedirection() {        return this.series() == HttpStatus.Series.REDIRECTION;    }    public boolean is4xxClientError() {        return this.series() == HttpStatus.Series.CLIENT_ERROR;    }    public boolean is5xxServerError() {        return this.series() == HttpStatus.Series.SERVER_ERROR;    }    public boolean isError() {        return this.is4xxClientError() || this.is5xxServerError();    }    public String toString() {        int var10000 = this.value;        return var10000 + &quot; &quot; + this.name();    }    public static HttpStatus valueOf(int statusCode) {        HttpStatus status = resolve(statusCode);        if (status == null) {            throw new IllegalArgumentException(&quot;No matching constant for [&quot; + statusCode + &quot;]&quot;);        } else {            return status;        }    }    @Nullable    public static HttpStatus resolve(int statusCode) {        for(HttpStatus status : VALUES) {            if (status.value == statusCode) {                return status;            }        }        return null;    }    public static enum Series {        INFORMATIONAL(1),        SUCCESSFUL(2),        REDIRECTION(3),        CLIENT_ERROR(4),        SERVER_ERROR(5);        private final int value;        private Series(int value) {            this.value = value;        }        public int value() {            return this.value;        }        /** @deprecated */        @Deprecated        public static HttpStatus.Series valueOf(HttpStatus status) {            return status.series;        }        public static HttpStatus.Series valueOf(int statusCode) {            HttpStatus.Series series = resolve(statusCode);            if (series == null) {                throw new IllegalArgumentException(&quot;No matching constant for [&quot; + statusCode + &quot;]&quot;);            } else {                return series;            }        }        @Nullable        public static HttpStatus.Series resolve(int statusCode) {            int seriesCode = statusCode / 100;            for(HttpStatus.Series series : values()) {                if (series.value == seriesCode) {                    return series;                }            }            return null;        }    }}</code></pre><h2 id="3.-webmvc%E9%85%8D%E7%BD%AE" tabindex="-1">3. WebMvc配置</h2><h3 id="3.1-%E6%B7%BB%E5%8A%A0-webmvc-%E6%89%80%E9%9C%80mvn-%E5%8C%85" tabindex="-1">3.1 添加 WebMvc 所需mvn 包</h3><pre><code class="language-">&lt;!-- webmvc 依赖于 spring-boot-starter-web --&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;!-- springboot 官方 api 文档，集成Swagger v3 --&gt;&lt;!-- https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-ui --&gt;&lt;dependency&gt;    &lt;groupId&gt;org.springdoc&lt;/groupId&gt;    &lt;artifactId&gt;springdoc-openapi-starter-webmvc-ui&lt;/artifactId&gt;    &lt;version&gt;2.8.3&lt;/version&gt;&lt;/dependency&gt;</code></pre><h3 id="3.2-%E8%87%AA%E5%AE%9A%E4%B9%89%40restcontrolleradvice%E8%87%AA%E5%8A%A8%E4%BD%BF%E7%94%A8result%3Ct%3E%E5%8C%85%E8%A3%85%E8%BF%94%E5%9B%9E%E5%80%BC" tabindex="-1">3.2 自定义<code>@RestControllerAdvice</code>自动使用<code>Result&lt;T&gt;</code>包装返回值</h3><p>注: <font color="red">使用<code>RestControllerAdvice</code>封装返回值不会修改swagger-ui显示，需要而外添加swagger ui api封装，参考文章后续方案</font></p><p>如果未生效，启动类添加注解<code>@ComponentScan(basePackages = {&quot;com.xxx.xxx&quot;})</code>确定扫描包范围（但是会导致spring扫描bean注入有问题，idea报错一些spring类无法找到注入，要把需要扫描的所有包都加进去）</p><pre><code class="language-">@SpringBootApplication@ComponentScan(basePackages = {       &quot;com.xxx.xxx&quot;,    // 主模块包路径})public class AuthApplication {    public static void main(String[] args) {        SpringApplication.run(AuthApplication.class, args);    }}</code></pre><p>封装正常返回</p><pre><code class="language-">import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.ObjectMapper;import com.jagger.ai.common.core.domain.Result;import org.springframework.core.MethodParameter;import org.springframework.http.MediaType;import org.springframework.http.server.ServerHttpRequest;import org.springframework.http.server.ServerHttpResponse;import org.springframework.web.bind.annotation.RestControllerAdvice;import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;/*** * 所有返回值统一封装{@link Result} */@RestControllerAdvice(basePackages = &quot;com.xx.xx&quot;) // 扫描包范围public class GlobalResponseHandler implements ResponseBodyAdvice&lt;Object&gt; {    /***     * 判断是否需要处理返回值     */    @Override    public boolean supports(MethodParameter returnType, Class converterType) {        // 排除已包装的 Result 类型        return !returnType.getParameterType().isAssignableFrom(Result.class);    }    /***     * 统一包装返回值     */    @Override    public Object beforeBodyWrite(Object body, MethodParameter returnType,                                  MediaType mediaType, Class converterType,                                  ServerHttpRequest request, ServerHttpResponse response) {        // 如果返回值是 String 类型，手动转为 JSON 字符串        // 在 Spring 的 统一响应处理中，只有 String 类型需要单独处理，而其他类型（如 int、对象、集合等）不需要特殊处理。        // 这是因为 Spring 对 String 类型的返回值有特殊的 StringHttpMessageConverter 默认行为，而其他类型会通过 JSON 序列化正常处理。        Result&lt;?&gt; result = Result.success(body);        if (body instanceof String) {            try {                return new ObjectMapper().writeValueAsString(result);            } catch (JsonProcessingException e) {                throw new RuntimeException(e);            }        }        return Result.success(body);    }}</code></pre><h2 id="4.-webflux%E9%85%8D%E7%BD%AE" tabindex="-1">4. WebFlux配置</h2><h3 id="4.1.-%E5%AE%89%E8%A3%85%E4%BE%9D%E8%B5%96%EF%BC%8C%E6%B7%BB%E5%8A%A0%E9%85%8D%E7%BD%AE" tabindex="-1">4.1. 安装依赖，添加配置</h3><pre><code class="language-">&lt;!-- 使用 webflux 响应式流（Reactive Streams）默认通过 Netty 启动排除 Tomcat --&gt;&lt;dependency&gt;    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;    &lt;artifactId&gt;spring-boot-starter-webflux&lt;/artifactId&gt;&lt;/dependency&gt;&lt;!-- springboot 官方 api 文档，支持集成Swagger --&gt;&lt;!-- https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-ui --&gt;&lt;dependency&gt;    &lt;groupId&gt;org.springdoc&lt;/groupId&gt;    &lt;artifactId&gt;springdoc-openapi-starter-webflux-ui&lt;/artifactId&gt;&lt;/dependency&gt;</code></pre><p>自定义<code>WebProperties</code>配置控制全局封装过滤路由</p><pre><code class="language-">import lombok.Data;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.stereotype.Component;import java.util.ArrayList;import java.util.List;@Component@ConfigurationProperties(prefix = &quot;jagger.web&quot;)@Datapublic class WebProperties {    // 需要无视的路由    private List&lt;String&gt; ignoreRoute = new ArrayList&lt;&gt;();    // 统一封装所有响应返回    private Boolean unifiedResponse = true;}</code></pre><p>application.yml文件添加配置</p><pre><code class="language-">jagger:  web:    ignore-route:    # 过滤 swagger api 路由      - ${springdoc.api-docs.path}/** </code></pre><h3 id="4.2.-%E8%87%AA%E5%AE%9A%E4%B9%89globalresponsehandler%E8%87%AA%E5%8A%A8result%3Ct%3E%E5%8C%85%E8%A3%85%E8%BF%94%E5%9B%9E%E5%80%BC" tabindex="-1">4.2. 自定义<code>GlobalResponseHandler</code>自动<code>Result&lt;T&gt;</code>包装返回值</h3><pre><code class="language-">import org.springframework.core.MethodParameter;import org.springframework.core.ResolvableType;import org.springframework.http.codec.HttpMessageWriter;import org.springframework.util.AntPathMatcher;import org.springframework.util.PathMatcher;import org.springframework.web.reactive.HandlerResult;import org.springframework.web.reactive.accept.RequestedContentTypeResolver;import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Flux;import reactor.core.publisher.Mono;import java.lang.reflect.Method;import java.util.ArrayList;import java.util.List;/*** * 所有返回值统一封装{@link Result} */public class GlobalResponseHandler extends ResponseBodyResultHandler {    // 需要无视的路由配置，避免封装导致api返回异常，例如: swagger api的路径    private final List&lt;String&gt; ignoreRoute;    public GlobalResponseHandler(List&lt;String&gt; ignoreRoute, List&lt;HttpMessageWriter&lt;?&gt;&gt; writers, RequestedContentTypeResolver resolver) {        super(writers, resolver);        this.ignoreRoute = ignoreRoute;        setOrder(getOrder() - 1); // 修改调用优先级使其高于 ResponseBodyResultHandler。    }    private static MethodParameter param;    static {        try {            Method method = GlobalResponseHandler.class.getDeclaredMethod(&quot;methodForParams&quot;);            param = new MethodParameter(method, -1);            param = param.nestedIfOptional();        } catch (NoSuchMethodException e) {            throw new IllegalStateException(&quot;Failed to find methodForParams method&quot;, e);        }    }    // 虚拟方法用于初始化MethodParameter    private static Result&lt;?&gt; methodForParams() {        return null;    }    private final PathMatcher pathMatcher = new AntPathMatcher();  // Spring内置的路径匹配器    /***     * 判断是否是需要忽视的路由     */    private boolean isIgnoreRoute(String route) {        return ignoreRoute.stream()                .anyMatch(pattern -&gt; pathMatcher.match(pattern, route));    }    /***     * 判断是否需要执行封装操作     */    @Override    public boolean supports(HandlerResult result) {        // 使用 ResolvableType 解析返回值类型        ResolvableType returnType = result.getReturnType();        // 检查是否包含泛型参数        if (returnType.getGenerics().length == 0) {            // 无泛型参数，需要封装            return true;        }        Class&lt;?&gt; rawClass = returnType.getRawClass();        if (rawClass == null) {            return true;        }        // 判断泛型是否是 Result 类型        return !Result.class.isAssignableFrom(rawClass);    }    @Override    public Mono&lt;Void&gt; handleResult(ServerWebExchange exchange, HandlerResult result) {        // 跳过需要忽视路由，避免第三方包api调用异常，例如：Swagger Api        if (isIgnoreRoute(exchange.getRequest().getURI().getPath())) {            return super.handleResult(exchange, result);        }        Object returnValue = result.getReturnValue();        if (returnValue instanceof Mono) {            // 处理Mono&lt;T&gt;，将其映射为Mono&lt;Result&lt;T&gt;&gt;            return ((Mono&lt;?&gt;) returnValue)                    .map(item -&gt; {                        ResolvableType resolvableType = ResolvableType.forType(item.getClass());                        Class&lt;?&gt; rawClass = resolvableType.getRawClass();                        // 非Result类型，包装成Result                        if (rawClass != null &amp;&amp; Result.class.isAssignableFrom(rawClass)) {                            // 已经是Result类型，直接返回                            return (Result&lt;?&gt;) item;                        } else {                            // 非Result类型，包装成Result                            return Result.success(item);                        }                    })                    .defaultIfEmpty(Result.success(null))   // 处理空 Mono（如 Mono.empty()）                    .flatMap(body -&gt; writeBody(body, param, exchange));        } else if (returnValue instanceof Flux) {            // 处理Flux&lt;T&gt;，转换为Mono&lt;Result&lt;List&lt;T&gt;&gt;&gt;            return ((Flux&lt;?&gt;) returnValue).collectList()                    .map(items -&gt; {                        if (items.isEmpty()) {                            return Result.success(items);                        } else {                            List&lt;Object&gt; resultList = new ArrayList&lt;&gt;();                            for (Object item : items) {                                ResolvableType resolvableType = ResolvableType.forType(item.getClass());                                Class&lt;?&gt; rawClass = resolvableType.getRawClass();                                if (rawClass != null &amp;&amp; Result.class.isAssignableFrom(rawClass)) {                                    resultList.add(((Result&lt;?&gt;) item).getData());                                } else {                                    resultList.add(item);                                }                            }                            return Result.success(resultList);                        }                    })                    .defaultIfEmpty(Result.success(null))                    .flatMap(body -&gt; writeBody(body, param, exchange));        } else {            // 调用父类方法处理包装后的返回值            return writeBody(Result.success(returnValue), param, exchange);        }    }}</code></pre><h3 id="4.3.-%E8%87%AA%E5%AE%9A%E4%B9%89webfluxconfiguration%E5%B9%B6%E6%B3%A8%E5%85%A5globalresponsehandler" tabindex="-1">4.3. 自定义<code>WebFluxConfiguration</code>并注入<code>GlobalResponseHandler</code></h3><pre><code class="language-">import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.http.codec.ServerCodecConfigurer;import org.springframework.web.reactive.accept.RequestedContentTypeResolver;import org.springframework.web.reactive.config.WebFluxConfigurer;@Configuration@RequiredArgsConstructor@Slf4jpublic class WebFluxConfiguration implements WebFluxConfigurer {    private final WebProperties webProperties;    @Bean    public GlobalResponseHandler responseWrapper(ServerCodecConfigurer serverCodecConfigurer,                                                 RequestedContentTypeResolver requestedContentTypeResolver) {        if (webProperties.getUnifiedResponse()) {            log.info(&quot;全局响应自动封装，忽略路由: {}&quot;, webProperties.getIgnoreRoute());            return new GlobalResponseHandler(webProperties.getIgnoreRoute(), serverCodecConfigurer.getWriters(), requestedContentTypeResolver);        }        return null;    }}</code></pre><h2 id="5.-%E8%87%AA%E5%AE%9A%E4%B9%89globalexceptionhandler%E7%9B%91%E5%90%AC%E5%85%A8%E5%B1%80%E5%BC%82%E5%B8%B8webmvc%E3%80%81webflux%E9%80%9A%E7%94%A8" tabindex="-1">5. 自定义<code>GlobalExceptionHandler</code>监听全局异常WebMvc、WebFlux通用</h2><p>定义异常</p><pre><code class="language-">import lombok.Getter;@Getterpublic class AuthException extends RuntimeException {    private final HttpStatus httpStatus;    public AuthException(HttpStatus httpStatus) {        super(httpStatus.getReasonPhrase());        this.httpStatus = httpStatus;    }}</code></pre><p>异常处理统一返回</p><pre><code class="language-">import lombok.extern.slf4j.Slf4j;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;@Slf4j@RestControllerAdvice(basePackages = &quot;com.jagger.ai&quot;)public class GlobalExceptionHandler {    @ExceptionHandler(AuthException.class)    public ResponseEntity&lt;Result&lt;Void&gt;&gt; authException(AuthException e) {        // 从异常中动态获取HTTP状态码        return ResponseEntity                .status(e.getHttpStatus().toSpringHttpStatus())                .body(Result.error(e.getHttpStatus(), e.getMessage()));    }    // 处理其他未捕获异常    @ExceptionHandler(Exception.class)    public ResponseEntity&lt;Result&lt;Void&gt;&gt; handleException(Exception e) {        log.error(e.getMessage(), e);        return ResponseEntity                .status(HttpStatus.INTERNAL_SERVER_ERROR.toSpringHttpStatus())                .body(Result.error(HttpStatus.INTERNAL_SERVER_ERROR,e.getMessage()));    }}</code></pre><p>此时定义的一个<code>Controller</code>，返回<code>String</code>会自动加一层封装返回<code>Result&lt;T&gt;</code>，以下是测试路由</p><pre><code class="language-">public interface LoginApi {@Slf4j@RestController@RequiredArgsConstructorpublic class LoginController implements LoginApi {    @Override    public String login() {        throw new AuthException(HttpStatus.INTERNAL_SERVER_ERROR);    }    @GetMapping(&quot;/login2&quot;)    public Mono&lt;Result&lt;String&gt;&gt; login2() {        return Mono.create(sink -&gt; sink.success(Result.success(&quot;success&quot;)));    }    @GetMapping(&quot;/login3&quot;)    public Mono&lt;Result&lt;Result&lt;String&gt;&gt;&gt; login3() {        return Mono.create(sink -&gt; sink.success(Result.success(Result.success(&quot;success&quot;))));    }    @GetMapping(&quot;/login4&quot;)    public Flux&lt;Result&lt;String&gt;&gt; login4() {        return Flux.create((t) -&gt; {            t.next(Result.success(&quot;1&quot;));            t.next(Result.success(&quot;2&quot;));            t.complete();        });    }    @GetMapping(&quot;/login5&quot;)    public List&lt;String&gt; login5() {        return List.of(&quot;1&quot;, &quot;2&quot;, &quot;3&quot;);    }}}</code></pre><p>所有响应都会返回<code>Result&lt;T&gt;</code>，但是swagger-ui接口文档显示不正确</p><p>接口 /login 返回文档</p><pre><code class="language-">string</code></pre><p>接口 /login2 返回文档</p><pre><code class="language-">{  &quot;code&quot;: 200,  &quot;msg&quot;: &quot;Success&quot;,  &quot;data&quot;: &quot;string&quot;}</code></pre><h2 id="6.-%E8%87%AA%E5%AE%9A%E4%B9%89resultwrappercustomizer%E4%BF%AE%E6%94%B9swagger-ui-%E6%98%BE%E7%A4%BAapi%E6%96%87%E6%A1%A3%EF%BC%8C%E4%BD%BF%E5%85%B6%E5%8C%B9%E9%85%8Dresult%3Ct%3E%E5%8C%85%E8%A3%85%E7%9A%84%E8%BF%94%E5%9B%9E%E5%80%BC" tabindex="-1">6. 自定义<code>ResultWrapperCustomizer</code>修改Swagger Ui 显示Api文档，使其匹配<code>Result&lt;T&gt;</code>包装的返回值</h2><pre><code class="language-">import io.swagger.v3.oas.models.OpenAPI;import io.swagger.v3.oas.models.media.*;import io.swagger.v3.oas.models.responses.ApiResponse;import io.swagger.v3.oas.models.responses.ApiResponses;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;import org.springdoc.core.customizers.OpenApiCustomizer;/*** * swagger ui api显示所有返回值封装一层{@link Result} */@Slf4j@Componentpublic class ResultWrapperCustomizer implements OpenApiCustomizer {    @Override    public void customise(OpenAPI openApi) {        openApi.getPaths().forEach((path, pathItem) -&gt; {            log.debug(path);            pathItem.readOperations().forEach(operation -&gt; {                ApiResponses responses = operation.getResponses();                responses.values().forEach(apiResponse -&gt; processApiResponse(apiResponse));            });        });    }    /***     * 处理api docs返回值显示内容     * @param apiResponse     */    private void processApiResponse(ApiResponse apiResponse) {        Content content = apiResponse.getContent();        if (content == null) {            // 针对无响应体的接口（如返回 void），创建默认的 content 和 schema            content = new Content();            MediaType mediaType = new MediaType();            mediaType.setSchema(createWrappedSchema(null)); // 传入 null 表示 data 为 null            content.addMediaType(&quot;application/json&quot;, mediaType);            apiResponse.setContent(content);            return;        }        content.forEach((mediaType, mediaTypeItem) -&gt; {            Schema originalSchema = mediaTypeItem.getSchema();            if (originalSchema != null &amp;&amp; !isResultWrapper(originalSchema)) {                Schema&lt;?&gt; wrappedSchema = createWrappedSchema(originalSchema);                mediaTypeItem.setSchema(wrappedSchema);            }        });    }    /***     * 判断返回类型是否是Result     */    private boolean isResultWrapper(Schema&lt;?&gt; schema) {        if (schema.get$ref() != null) {            return schema.get$ref().startsWith(&quot;#/components/schemas/&quot; + Result.class.getSimpleName());        }        return false;    }    /***     * 封装一层Result     */    private Schema&lt;?&gt; createWrappedSchema(Schema&lt;?&gt; originalSchema) {        return new ObjectSchema()                .addProperty(&quot;code&quot;, new IntegerSchema().example(200))                .addProperty(&quot;msg&quot;, new StringSchema().example(&quot;Success&quot;))                .addProperty(&quot;data&quot;, originalSchema);    }}</code></pre><p>至此swagger-ui显示的接口文档，返回值也是正确的<br />接口 /login 返回文档</p><pre><code class="language-">{  &quot;code&quot;: 200,  &quot;msg&quot;: &quot;Success&quot;,  &quot;data&quot;: &quot;string&quot;}</code></pre><p>接口 /login2 返回文档</p><pre><code class="language-">{  &quot;code&quot;: 200,  &quot;msg&quot;: &quot;Success&quot;,  &quot;data&quot;: &quot;string&quot;}</code></pre><h2 id="7.-%E5%90%8C%E6%AD%A5spring-boot%E5%92%8Cswagger-ui%E7%9A%84objectmapper%EF%BC%8C%E7%BB%9F%E4%B8%80%E4%BF%AE%E6%94%B9%E8%BF%94%E5%9B%9E%E5%80%BC%E9%A9%BC%E5%B3%B0%E6%A0%BC%E5%BC%8F%E4%B8%BA%E8%9B%87%E5%BD%A2%E6%A0%BC%E5%BC%8F%EF%BC%88%E4%B8%8B%E5%88%92%E7%BA%BF%EF%BC%89" tabindex="-1">7. 同步Spring Boot和Swagger Ui的<code>ObjectMapper</code>，统一修改返回值驼峰格式为蛇形格式（下划线）</h2><p>application.yml 添加配置</p><pre><code class="language-">spring:  jackson:    property-naming-strategy: SNAKE_CASE</code></pre><p>添加<code>ModelResolver</code>子类通过<code>@Component</code>注入 Spring</p><pre><code class="language-">/*** * 通过 @Component 注入 CustomModelResolver 覆盖 ModelResolver，将spring ObjectMapper注入CustomModelResolver。 * 否则ModelResolver会使用自定义的ObjectMapper，无法与spring ObjectMapper保持同步配置，例如：swagger ui 返回值格式修改 SNAKE_CASE * 注：这里不能使用@bean注入ModelResolver实现，否则ModelResolver不会加载自定义ModelConverter，具体原因未知。 */@Componentpublic class CustomModelResolver extends ModelResolver {    public CustomModelResolver(ObjectMapper mapper) {        super(mapper);    }}</code></pre><p>此时返回值都会是下划线格式</p>]]>
                    </description>
                    <pubDate>Tue, 18 Feb 2025 21:52:17 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Nacos 2023.0.1.2 之后版本不稳定，配置有改动不能直接2023.0.1.2升级到2023.0.1.3]]>
                    </title>
                    <link>http://halo.ljdzsk.com/archives/nacos2023012-zhi-hou-ban-ben-bu-wen-ding--pei-zhi-you-gai-dong-bu-neng-zhi-jie-2023012-sheng-ji-dao-2023013</link>
                    <description>
                            <![CDATA[<p>参考：<a href="https://github.com/alibaba/spring-cloud-alibaba/pull/2349" target="_blank">https://github.com/alibaba/spring-cloud-alibaba/pull/2349</a></p>]]>
                    </description>
                    <pubDate>Thu, 13 Feb 2025 18:19:30 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Git 代理配置，方便push代码到GitHub]]>
                    </title>
                    <link>http://halo.ljdzsk.com/archives/git-dai-li-pei-zhi--fang-bian-push-dai-ma-dao-github</link>
                    <description>
                            <![CDATA[<h1 id="git%E4%BB%A3%E7%90%86%E9%85%8D%E7%BD%AE%E5%8E%9F%E5%9B%A0" tabindex="-1">git代理配置原因</h1><p>就算代理设置了全局代理，git上传时走的也是hosts文件配置。因此就会出现网页可以访问github，但是git push到github就会提示链接失败。</p><h1 id="%E4%BB%A3%E7%90%86%E9%85%8D%E7%BD%AE" tabindex="-1">代理配置</h1><p>我们这里假设本地代理为<code>http://127.0.0.1:1080</code>。</p><h3 id="%E5%85%A8%E5%B1%80%E9%85%8D%E7%BD%AE" tabindex="-1">全局配置</h3><p>全局配置会影响所有的git地址，这里建议针对网址进行局部配置</p><pre><code class="language-">git config --global http.proxy http://127.0.0.1:1080git config --global https.proxy https://127.0.0.1:1080</code></pre><h3 id="%E5%B1%80%E9%83%A8%E9%85%8D%E7%BD%AE" tabindex="-1">局部配置</h3><p>针对github.com单独进行代理配置</p><pre><code class="language-">git config --global http.https://github.com.proxy https://127.0.0.1:1080git config --global https.https://github.com.proxy https://127.0.0.1:1080</code></pre><p>如果在配置局部代理之前已经配置了全局代理，我们可以取消全局代理的配置</p><pre><code class="language-">git config --global --unset http.proxygit config --global --unset https.proxy</code></pre><h3 id="http%E5%92%8Csock5%E9%85%8D%E7%BD%AE%E5%8C%BA%E5%88%AB" tabindex="-1">http和sock5配置区别</h3><p>如果是sock5也是一样的配置，我们在这里找到sock5的端口为1086（windows一般是1080,mac是1086）<br /><img src="/upload/2024/03/image.png" alt="image" /><br />然后进行局部配置</p><pre><code class="language-">git config --global http.https://github.com.proxy socks5://127.0.0.1:1086git config --global https.https://github.com.proxy socks5://127.0.0.1:1086</code></pre>]]>
                    </description>
                    <pubDate>Sat, 02 Mar 2024 04:25:58 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[SSH 手册]]>
                    </title>
                    <link>http://halo.ljdzsk.com/archives/ssh-shou-ce</link>
                    <description>
                            <![CDATA[<h1 id="%E5%85%8D%E5%AF%86%E7%99%BB%E5%BD%95" tabindex="-1">免密登录</h1><h2 id="mac-%E5%85%8D%E5%AF%86%E7%99%BB%E5%BD%95" tabindex="-1">Mac 免密登录</h2><p><font color="red">Mac的坑点：多了个<code>ssh-add</code>命令需要执行，否则还是无法进行免密登录</font></p><pre><code class="language-"># 更换目录到 ~/.sshcd ~/.ssh  # ssh-keygen -t rsa -f [秘钥文件名称]# 创建秘钥文件，我这里直接使用服务器ip作为秘钥文件名称，如果不写默认名称为：id_rsa# 执行命令后会生成公钥和私钥2个文件： [秘钥文件名称]  和  [秘钥文件名称].pubssh-keygen -t rsa -f 192.168.31.128# ssh-copy-id -i [秘钥文件名称].pub root@[服务器IP]# 上传公钥到服务器中，此时会在服务器的authorized_keys文件后追加公钥内容ssh-copy-id -i 192.168.31.128.pub root@192.168.31.128# Mac的坑点# ssh-add -K [你的私钥文件，就是那个不加.pub结尾的文件] ssh-add -K 192.168.31.128</code></pre><h2 id="linux%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%85%8D%E5%AF%86%E7%99%BB%E5%BD%95" tabindex="-1">Linux服务器免密登录</h2><pre><code class="language-"># 更换目录到 ~/.sshcd ~/.ssh  # ssh-keygen -t rsa -f [秘钥文件名称]# 创建秘钥文件，我这里直接使用服务器ip作为秘钥文件名称，如果不写默认名称为：id_rsa# 执行命令后会生成公钥和私钥2个文件： [秘钥文件名称]  和  [秘钥文件名称].pubssh-keygen -t rsa -f 192.168.31.128# ssh-copy-id -i [秘钥文件名称].pub root@[服务器IP]# 上传公钥到服务器中，此时会在服务器的authorized_keys文件后追加公钥内容ssh-copy-id -i 192.168.31.128.pub root@192.168.31.128</code></pre>]]>
                    </description>
                    <pubDate>Wed, 10 Jan 2024 17:18:52 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Redis 避坑指南]]>
                    </title>
                    <link>http://halo.ljdzsk.com/archives/redis-bi-keng-zhi-nan</link>
                    <description>
                            <![CDATA[<h1 id="%E8%A6%86%E7%9B%96%E5%80%BC%E6%97%B6%EF%BC%8C%E4%BC%9A%E5%86%B2%E8%BF%87%E6%9C%9F%E6%97%B6%E9%97%B4%E4%B8%BA%E6%B0%B8%E4%B9%85%E4%B8%8D%E8%BF%87%E6%9C%9F%EF%BC%88-1%EF%BC%89" tabindex="-1">覆盖值时，会冲过期时间为永久不过期（-1）</h1><h3 id="string%E7%B1%BB%E5%9E%8B" tabindex="-1">String类型</h3><ul><li>set、setex修改值（覆盖）会重置过期时间为永久不过期（-1），需要在覆盖修改后重新设定过期时间。</li><li>append、incr、decr等命令在原有值上修改的操作，不会重置过期时间。</li></ul><h3 id="hash%E3%80%81set%E3%80%81zset%E3%80%81list%E7%B1%BB%E5%9E%8B" tabindex="-1">Hash、Set、Zset、List类型</h3><ul><li>单纯修改所属值不会重置过期时间，但是如果提取出所有值导致值为空移除了指定key，再次重新写入新的值，过期时间就会变成永不过期（-1）</li></ul>]]>
                    </description>
                    <pubDate>Fri, 17 Nov 2023 06:35:40 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Minio 文件系统部署与使用]]>
                    </title>
                    <link>http://halo.ljdzsk.com/archives/minio-wen-jian-xi-tong-bu-shu-yu-shi-yong</link>
                    <description>
                            <![CDATA[<h1 id="%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99" tabindex="-1">参考资料</h1><p>Docker部署: <a href="https://hub.docker.com/r/minio/minio" target="_blank">https://hub.docker.com/r/minio/minio</a><br />Java官方教程: <a href="https://min.io/docs/minio/linux/developers/java/minio-java.html" target="_blank">https://min.io/docs/minio/linux/developers/java/minio-java.html</a><br />其他语言访问教程: <a href="https://min.io/docs/minio/linux/developers/minio-drivers.html?ref=docs" target="_blank">https://min.io/docs/minio/linux/developers/minio-drivers.html?ref=docs</a><br />Minio Nginx转发配置：<a href="https://min.io/docs/minio/linux/integrations/setup-nginx-proxy-with-minio.html" target="_blank">https://min.io/docs/minio/linux/integrations/setup-nginx-proxy-with-minio.html</a><br />Minio 可视化控制台配置：<a href="https://min.io/docs/minio/linux/administration/minio-console.html" target="_blank">https://min.io/docs/minio/linux/administration/minio-console.html</a></p><h1 id="%E9%81%87%E5%88%B0%E7%9A%84%E5%9D%91" tabindex="-1">遇到的坑</h1><h3 id="minio%E7%9A%84api%E6%8E%A5%E5%8F%A3%EF%BC%8C%E4%B8%8D%E8%83%BD%E4%BD%BF%E7%94%A8nginx%E4%BA%8C%E7%BA%A7%E7%9B%AE%E5%BD%95%E8%BD%AC%E5%8F%91%EF%BC%88%E5%8F%AA%E8%83%BD%E9%85%8D%E7%BD%AE%E8%BD%AC%E5%8F%91%E6%8E%A7%E5%88%B6%E5%8F%B0%E7%9A%849001%E7%AB%AF%E5%8F%A3%EF%BC%89" tabindex="-1"><font color="red">Minio的API接口，不能使用Nginx二级目录转发（只能配置转发控制台的9001端口）</font></h3><p>这里使用Java代码访问，如图直接报错地址格式错误。<br /><img src="/upload/2023/11/image.png" alt="image" /></p><h1 id="docker%E9%83%A8%E7%BD%B2" tabindex="-1">docker部署</h1><h3 id="%E5%91%BD%E4%BB%A4%E8%A1%8C%E9%83%A8%E7%BD%B2" tabindex="-1">命令行部署</h3><pre><code class="language-shell">docker run -p 9000:9000 -p 9001:9001 --name minio \  -d --restart=always \  -e &quot;MINIO_ACCESS_KEY=admin&quot; \  -e &quot;MINIO_SECRET_KEY=admin&quot; \      # 如果要配置Nginx二级域名转发，必须要写清楚Minio控制台域名路径。  -e &quot;MINIO_BROWSER_REDIRECT_URL=https://domain.com/minio/&quot; \  -v /data/minio/data:/data \  -v /data/minio/config:/root/.minio \  minio/minio:RELEASE.2023-11-01T18-37-25Z.fips \  server /data --console-address &quot;:9090&quot;</code></pre><h3 id="docker-compose.yml%E6%96%87%E4%BB%B6%E9%83%A8%E7%BD%B2" tabindex="-1">docker-compose.yml文件部署</h3><p><code>docker-compose -f .\docker-compose.yml up -d</code></p><pre><code class="language-yml">version : &#39;3.8&#39;services:  minio:    container_name: minio    image: minio/minio:RELEASE.2023-11-01T18-37-25Z.fips    ports:      - &quot;9000:9000&quot;      - &quot;9001:9001&quot;    environment:      - MINIO_ACCESS_KEY=admin      - MINIO_SECRET_KEY=admin      # 如果要配置Nginx二级域名转发，必须要写清楚Minio控制台域名路径。      - MINIO_BROWSER_REDIRECT_URL=https://domain.com/minio/    volumes:      - ./minio/data:/data      - ./minio/config:/root/.minio    command: server /data --console-address &quot;:9001&quot;</code></pre><h1 id="9000%E5%92%8C9001%E7%AB%AF%E5%8F%A3%E4%BD%9C%E7%94%A8" tabindex="-1">9000和9001端口作用</h1><p>9000: 提供API访问<br />9001: 提供可视化页面使用</p><h1 id="minio%E6%8E%A7%E5%88%B6%E5%8F%B0%EF%BC%8C%E5%8F%AF%E8%A7%86%E5%8C%96%E7%95%8C%E9%9D%A2%E8%AE%BF%E9%97%AE" tabindex="-1">Minio控制台，可视化界面访问</h1><ol><li>访问<code>http://IP:9001</code>。</li><li>创建Access Key提供API进行访问。<br /><img src="/upload/2023/11/1699381738201.png" alt="1699381738201" /></li></ol><h3 id="%E9%85%8D%E7%BD%AEnginx%E8%BD%AC%E5%8F%91%E5%8F%AF%E8%A7%86%E5%8C%96%E7%95%8C%E9%9D%A2" tabindex="-1">配置Nginx转发可视化界面</h3><pre><code class="language-"># 需要转发的二级目录   location /minio/ {      rewrite ^/minio/(.*) /$1 break;      proxy_set_header Host $http_host;      proxy_set_header X-Real-IP $remote_addr;      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;      proxy_set_header X-Forwarded-Proto $scheme;      proxy_set_header X-NginX-Proxy true;      # This is necessary to pass the correct IP to be hashed      real_ip_header X-Real-IP;      proxy_connect_timeout 300;      # To support websockets in MinIO versions released after January 2023      proxy_http_version 1.1;      proxy_set_header Upgrade $http_upgrade;      proxy_set_header Connection &quot;upgrade&quot;;      # Some environments may encounter CORS errors (Kubernetes + Nginx Ingress)      # Uncomment the following line to set the Origin request to an empty string      # proxy_set_header Origin &#39;&#39;;      chunked_transfer_encoding off; # 对应的docker容器名称与域名      proxy_pass http://minio:9001;   }</code></pre><h1 id="%E4%BB%A3%E7%A0%81%E8%AE%BF%E9%97%AE" tabindex="-1">代码访问</h1><h3 id="java%E4%BB%A3%E7%A0%81%E8%AE%BF%E9%97%AE" tabindex="-1">Java代码访问</h3><pre><code class="language-Java">import io.minio.BucketExistsArgs;import io.minio.MakeBucketArgs;import io.minio.MinioClient;import io.minio.UploadObjectArgs;import io.minio.errors.*;import org.testng.annotations.Test;import java.io.IOException;import java.security.InvalidKeyException;import java.security.NoSuchAlgorithmException;public class MinioTest {    @Test()    public void test2() throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {        // 使用MinIO服务的URL，端口，Access key和Secret key创建一个MinioClient对象        MinioClient minioClient = MinioClient.builder()                .endpoint(&quot;http://ip:9000&quot;)                // 这里写之前可视化页面创建的 Access Key                .credentials(&quot;wrYc3jGugGwhJn8sOkdK&quot;,                        &quot;8mByhjkpOmP1LC0NLyN8Yccvohwdd7E9kb4gGeDs&quot;)                .build();        // 如果桶不存在就新建        boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(&quot;asiatrip&quot;).build());        if (!found) {            minioClient.makeBucket(MakeBucketArgs.builder().bucket(&quot;asiatrip&quot;).build());        } else {            System.out.println(&quot;&#39;asiatrip桶已存在&quot;);        }        // 上传文件        minioClient.uploadObject(                UploadObjectArgs.builder()                        .bucket(&quot;asiatrip&quot;)                        .object(&quot;test.zip&quot;)                        .filename(&quot;D:\\下载\\test.zip&quot;)                        .build());        System.out.println(&quot;上传成功&quot;);    }}</code></pre>]]>
                    </description>
                    <pubDate>Wed, 08 Nov 2023 02:37:31 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Java Spring 定时任务]]>
                    </title>
                    <link>http://halo.ljdzsk.com/archives/javaspring-ding-shi-ren-wu</link>
                    <description>
                            <![CDATA[<p>添加</p><pre><code class="language-">@EnableScheduling // 在spring容器中启用定时任务的执行public class GameApplication {    public static void main(String[] args) {        SpringApplication.run(GameApplication.class, args);    }}</code></pre><p>编写定时任务类，通过注解@Scheduled设定触发时间</p><pre><code class="language-">import com.alibaba.fastjson2.JSONArray;import com.douyin.game.dto.DouyinMsgDto;import com.douyin.game.service.DouyinApiService;import com.douyin.game.service.DouyinGameCacheService;import com.ruoyi.common.core.constant.DouyinConstants;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.cloud.context.config.annotation.RefreshScope;import org.springframework.scheduling.annotation.Scheduled;import org.springframework.stereotype.Component;import java.util.List;@Component@Slf4j// 定时任务 @Scheduled 不能与 @RefreshScope 同时使用，当属性刷新时会导致定时任务失效（可以解决：需要触发刷新的时候重新装载定时任务）// @RefreshScopepublic class DouyinTask {    /**     * cron配置每30分钟执行一次     */    @Scheduled(cron = &quot;* */30 * * * ?&quot;)    // 上一次任务开始触发，进入倒计时5000毫秒后再次触发任务    // @Scheduled(fixedRate = 5000)    // 上一次任务执行完毕，进入倒计时5000毫秒后再次触发任务    // @Scheduled(fixedDelay = 5000)    public void updateAccessToken() {        log.info(&quot;定时任务：更新access_token&quot;);    }}</code></pre>]]>
                    </description>
                    <pubDate>Thu, 02 Nov 2023 18:40:08 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Nginx 防御]]>
                    </title>
                    <link>http://halo.ljdzsk.com/archives/nginx-fang-yu</link>
                    <description>
                            <![CDATA[<h1 id="%E8%A2%AB%E6%81%B6%E6%84%8F%E6%89%AB%E6%8F%8F%EF%BC%8C%E6%88%96%E8%80%85%E4%B8%8B%E8%BD%BD%E6%96%87%E4%BB%B6" tabindex="-1">被恶意扫描，或者下载文件</h1><p>配置conf</p><pre><code class="language-">    # 限制IP访问,只允许域名访问    server {        listen  80  default_server;        listen  443 default_server;        #SSL相关配置，请勿删除或修改，必须带有证书才能配置443        ssl_certificate     /etc/letsencrypt/live/test/fullchain.pem; // 这里要改成自己不用的证书        ssl_certificate_key /etc/letsencrypt/live/test/privkey.pem; // 这里要改成自己不用的证书        #SSL-END        server_name _;        location / {            # 专制恶意扫描，所有请求跳转到10GB大文件慢慢下去            rewrite \.*$ http://lg-hkg.fdcservers.net/10GBtest.zip;            # 只匹配扫描对应文件后缀才下载            # rewrite \.(rar|zip|tar|sql|gz|7z)/?$ http://lg-hkg.fdcservers.net/10GBtest.zip;            return  403;        }    }</code></pre><h1 id="%E6%97%A5%E5%BF%97%E5%88%86%E6%9E%90" tabindex="-1">日志分析</h1><p>访问者IP请求次数排名</p><pre><code class="language-">awk &#39;{print $1}&#39; /usr/local/nginx/log/access.log |sort |uniq -c|sort -n</code></pre><p>带有指定字段的IP，请求次数排名，这里是过滤尝试代理访问百度的请求。</p><pre><code class="language-">cat access.log | grep &#39;baidu&#39; | awk &#39;{print $1}&#39;  | sort | uniq -c | sort -nr -k1 | head -n 10</code></pre><p>访问IP的请求次数</p><pre><code class="language-">awk &#39;{print $1}&#39;  access.log|sort | uniq -c |wc -l</code></pre><p>查询访问最频繁的URL</p><pre><code class="language-">awk &#39;{print $7}&#39; access.log|sort | uniq -c |sort -n -k 1 -r|more</code></pre><h1 id="%E9%85%8D%E7%BD%AEningx%EF%BC%8C%E5%B1%8F%E8%94%BDip" tabindex="-1">配置Ningx，屏蔽IP</h1><p>将以下配置写入conf中，可应用作用域： http / server / location / limit_except</p><pre><code class="language-">http {# 全局屏蔽单个 IPdeny IP;     server {        # 特定服务屏蔽单个IP        deny IP;                  location /api {            # 特定路由屏蔽单个IP            deny IP;          }    }    # 允许单个ip访问    allow IP;     # 屏蔽所有ip访问    deny all;     # 允许所有ip访问    allow all;     # 屏蔽整个段即从123.0.0.1到123.255.255.254访问的命令    deny 123.0.0.0/8    # 屏蔽IP段即从123.45.0.1到123.45.255.254访问的命令    deny 124.45.0.0/16    # 屏蔽IP段即从123.45.6.1到123.45.6.254访问的命令    deny 123.45.6.0/24    # 如果你想实现这样的应用，除了几个IP外，其他全部拒绝，    # 那需要你在guolv_ip.conf中这样写    allow 1.1.1.1;     allow 1.1.1.2;    deny all;}</code></pre><h3 id="%E9%85%8D%E7%BD%AE%E4%BC%98%E5%85%88%E7%BA%A7" tabindex="-1">配置优先级</h3><p>作用范围和配置的顺序有关系,先配置的优先级高，会覆盖和后一个配置重合的部分,<br />可以添加多个allow和多个deny：</p><pre><code class="language-"># 这个配置127.0.0.1可以通过访问。allow 127.0.0.1;deny all;# 这个配置127.0.0.1无法通过访问。deny all;allow 127.0.0.1;# 这个配置127.0.0.1无法通过访问。deny 127.0.0.1;allow all;# 这个配置127.0.0.1可以通过访问。allow all;deny 127.0.0.1;</code></pre>]]>
                    </description>
                    <pubDate>Thu, 02 Nov 2023 18:30:10 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[C# 避坑指南]]>
                    </title>
                    <link>http://halo.ljdzsk.com/archives/c-bi-keng-zhi-nan</link>
                    <description>
                            <![CDATA[<h1 id="%E5%A7%94%E6%89%98-delegate%E3%80%81event-%E5%8C%BA%E5%88%AB" tabindex="-1">委托 Delegate、Event 区别</h1><h3 id="delegate%E7%89%B9%E6%80%A7" tabindex="-1">Delegate特性</h3><p>Delegate官方翻译是委托 。delegate 本质上是一个用来存放函数的容器。众所周知，在面向对象的语言中，变量可以是值类型，也可以是引用类型。而 C# 则用 delegate 的机制将函数引用存储起来，并且可以在运行时被改变。delegate 特别用于实现事件和回调方法，一种委托将提前约定好存放的函数的返回类型和参数列表。</p><p>delegate 可以被初始化成一个函数的引用，也可以通过 +=，-= 等运算符进行存放多个函数引用。存放多个函数时，调用 delegate 将会调用所有的函数引用，这种行为叫做 multicast delegate，即委托的多播。</p><p><font color="red"><code>=</code>会取消所有订阅，只保留<code>=</code>之后的订阅</font></p><pre><code class="language-C#">//定义一个无返回值的，带一个int参数的委托public delegate void myDelegate(int num);// 定义一个委托，并添加订阅public myDelegate m_delegate;m_delegate += MyFun;public void MyFun(int num){  Debug.Log(&quot;my func: &quot; + num);}// 此时 如果再使用 &#96;=&#96; 会取消前面所有的订阅，只保留 &#96;=&#96; 后的新订阅m_delegate = MyFun1;  //MyFun订阅被取消，只有MyFun1在订阅中</code></pre><h3 id="event%E7%89%B9%E6%80%A7" tabindex="-1">Event特性</h3><p><code>Event</code>外部调用只能使用<code>+=</code>、<code>-=</code>增减委托，而不能直接使用<code>=</code>，避免出现委托因为误操作(或恶意)取消所有订阅。发布者是可以是用<code>=</code>。</p><pre><code class="language-">public class A{    public delegate void ActionTest();    public event ActionTest m_event;    public void T()    {        m_event = MyFun; // 不报错    }    private void MyFun()    {        throw new NotImplementedException();    }}public class B{    public A a;    public void T()    {        a.m_event = MyFun; // 报错        a.m_event += MyFun; // 不报错        a.m_event -= MyFun; // 不报错    }    private void MyFun()    {        throw new NotImplementedException();    }}</code></pre><p><img src="/upload/2023/10/image-1697456375667.png" alt="image-1697456375667" /></p>]]>
                    </description>
                    <pubDate>Mon, 16 Oct 2023 19:39:45 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Unity 小技巧]]>
                    </title>
                    <link>http://halo.ljdzsk.com/archives/unity-xiao-ji-qiao</link>
                    <description>
                            <![CDATA[<h1 id="%E5%8A%A8%E7%94%BB%E6%95%88%E6%9E%9C%E5%8D%95%E7%8B%AC%E5%81%9A%E4%B8%80%E4%B8%AA%E5%AD%90gameobject%E8%BF%9B%E8%A1%8C%E4%BD%BF%E7%94%A8" tabindex="-1">动画效果单独做一个子GameObject进行使用</h1><p>很多动画做出来，朝向方向不是统一的，特别是使用第三方资源时。此时我们将动画放置子GameObject中，通过修正Transform.Rotation角度指向x方位。后续通过修改父 GameObject Transform.Rotation 调整角色转向、拐弯等方向时可用有效避免而外角度换算。<br /><img src="/upload/2023/10/image-1697370844758.png" alt="image-1697370844758" /></p>]]>
                    </description>
                    <pubDate>Sun, 15 Oct 2023 19:54:14 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Unity 对象池优化性能]]>
                    </title>
                    <link>http://halo.ljdzsk.com/archives/unity-dui-xiang-chi-you-hua-xing-neng</link>
                    <description>
                            <![CDATA[<h1 id="%E9%80%9A%E8%BF%87%E5%AF%B9%E8%B1%A1%E6%B1%A0%E5%87%8F%E5%B0%91%E5%AF%B9%E8%B1%A1%E7%9A%84%E5%88%9B%E5%BB%BA%E4%BC%98%E5%8C%96%E6%80%A7%E8%83%BD%EF%BC%9A%E7%A9%BA%E9%97%B4%E6%8D%A2%E6%97%B6%E9%97%B4" tabindex="-1">通过对象池减少对象的创建优化性能：空间换时间</h1><p>常用切重复的对象可以通过对象池管理进行复用，减少重复创建。<br />例：同一个技能释放每次使用的都是预制体，只是技能伤害可能有所变化。技能每次释放结束通过<code>gameObject.SetActive(false)</code>隐藏对象，再次释放同一技能修改其伤害数值并<code>gameObject.SetActive(true)</code>展示对象。就可以避免大量的重复技能创建节约CPU运算性能。</p><h3 id="%E7%A4%BA%E4%BE%8B%E4%BB%A3%E7%A0%81" tabindex="-1">示例代码</h3><p>对象池代码</p><pre><code class="language-">using System.Collections.Generic;using UnityEngine;public class GameObjectPool{    public GameObject gameObjectPool;    private Dictionary&lt;string, Queue&lt;GameObject&gt;&gt; _pool;    public GameObjectPool(string name)    {        _pool = new Dictionary&lt;string, Queue&lt;GameObject&gt;&gt;();        gameObjectPool = new GameObject(name);    }    // 获取对象，通过预制体资源地址，分别获取、存储不同的队列中。    public GameObject GetGameObject(string key)    {        if (!_pool.ContainsKey(key))        {            return null;        }        if (_pool[key].Count &gt; 0)        {            return _pool[key].Dequeue();        }        else        {            return null;        }    }    // 回收对象    public void RecoveryGameObject(string key, GameObject gameObject)    {        if (!_pool.ContainsKey(key))        {            _pool[key] = new Queue&lt;GameObject&gt;();        }        gameObject.SetActive(false);        gameObject.transform.SetParent(gameObjectPool.transform);        _pool[key].Enqueue(gameObject);    }}</code></pre><p>对象池的使用</p><pre><code class="language-">using System.Collections.Generic;using System.Linq;using UnityEngine;using UnityEngine.AddressableAssets;using UnityEngine.ResourceManagement.AsyncOperations;public class SkillManager : MonoBehaviour{    public SkillEventSO skillManagerSO;    private AsyncOperationHandle&lt;GameObject&gt; _gameObjectHandle;    private GameObjectPool _bulletPool;    #region Unity生命周期    private void Awake()    {        _bulletPool = new GameObjectPool(&quot;BulletGameObjectPool&quot;);        _bulletPool.gameObjectPool.transform.SetParent(transform);    }    #endregion    // 释放技能    public void CastSkill(SkillDTO skillDTO)    {        string prefabAddress = skillDTO.prefabAddress;        // 对象池获取对象，如果池中存在对象就直接拿来用，如果没有就是实例化一个新的        GameObject bullet = _bulletPool.GetGameObject(prefabAddress);        if (bullet != null)        {            bullet.transform.SetParent(transform);            bullet.GetComponent&lt;BulletController&gt;().skillDTO = skillDTO;            bullet.SetActive(true);        }        else        {            _gameObjectHandle = Addressables.LoadAssetAsync&lt;GameObject&gt;(prefabAddress);            _gameObjectHandle.Completed += (handle) =&gt;            {                if (handle.Status == AsyncOperationStatus.Succeeded)                {                    bullet = Instantiate(handle.Result, transform);                    bullet.transform.SetParent(transform);                    bullet.GetComponent&lt;BulletController&gt;().skillDTO = skillDTO;                    bullet.GetComponent&lt;BulletController&gt;().skillManager = this;                }                else                {                    Debug.LogError(&quot;Asset load failed: &quot; + prefabAddress);                }            };        }    }    // 回收技能    public void RecoveryBulletGameObject(GameObject bulletGameObject)    {        _bulletPool.RecoveryGameObject(bulletGameObject.GetComponent&lt;BulletController&gt;().skillDTO.prefabAddress, bulletGameObject);    }}</code></pre>]]>
                    </description>
                    <pubDate>Sun, 15 Oct 2023 19:47:19 CST</pubDate>
                </item>
    </channel>
</rss>