Jekyll2021-04-23T08:37:06+08:00https://xiaoym.gitee.io/八一菜刀萧明的个人博客肖玉民Spring Security框架中踢人下线技术探索2021-04-20T00:00:00+08:002021-04-20T00:00:00+08:00https://xiaoym.gitee.io/2021/04/20/spring-security-out-session<h2 id="1背景">1.背景</h2>
<p>在某次项目的开发中,使用到了Spring Security权限框架进行后端权限开发的权限校验,底层集成Spring Session组件,非常方便的集成Redis进行分布式Session的会话集群部署。系统正式上线后,各个部署节点能够非常方便的进行集群部署,用户的Session会话信息全部保存在Redis中间件库中,开发者不用关心具体的实现,Spring Session组件已经全部集成好了。</p>
<p>但是在系统的用户管理模块中,提供了对系统用户账号的<strong>删除</strong>功能以及<strong>禁用</strong>功能,针对这两个功能,需求方给出的具体要求是:</p>
<ul>
<li><strong>删除</strong>:当管理员删除当前用户账号时,如果当前账号已经登录系统,则需要剔除下线,并且不可登录</li>
<li><strong>禁用</strong>:当管理员对当前账号禁用操作时,如果当前账号已经登录系统,则需要剔除下线,并且登录时,提示当前账号已禁用</li>
</ul>
<h2 id="2需求分析">2.需求分析</h2>
<p>从上面的需求来看,不管是<strong>删除</strong>还是<strong>禁用</strong>功能,都需要实现,如果当前账号已经登录系统,则需要剔除下线,而<strong>禁用</strong>操作只需要再登录时给出提示信息即可,这个在业务登录方法中就可以实现,不必从底层框架进行考虑。</p>
<p>因此,从底层技术测进行考虑时,我们需要探索如何在Spring Security权限框架中实现踢人下线的功能。</p>
<p>既然需求已经明确,从功能的实现可能性方面入手,我们则需要从几个方面进行考虑:</p>
<ul>
<li>1)、在Spring Security框架中,用户登录的Session会话信息存储在哪里?</li>
<li>2)、在Spring Security框架中,Session会话如何存储,主要存储哪些信息?</li>
<li>3)、如何根据账号收集当前该账号登录的所有Session会话信息?</li>
<li>4)、如何在服务端主动销毁Session对象?</li>
</ul>
<p>1)、在Spring Security框架中,用户登录的Session会话信息存储在哪里?</p>
<p>如果我们不考虑分布式Session会话的情况,单体Spring Boot项目中,服务端Session会话肯定存储在内存中,这样的弊端是如果当前应用需要做负载均衡进行部署时,用户请求服务端接口时,会存在Session会话丢失的情况,因为用户登录的会话信息都存在JVM内存中,没有进程之间的共享互通。</p>
<p>为了解决分布式应用Session会话不丢失的问题,Spring Session组件发布了,该组件提供了基于JDBC\Redis等中间件的方式,将用户端的Session会话存储在中间件中,这样分布式应用获取用户会话时,都会从中间件去获取会话Session,这样也保证了服务可以做负载部署以保证Session会话不丢失。本文主要讨论的也是这种情况,集成Redis中间件用来保存用户会话信息。</p>
<p>2)、在Spring Security框架中,Session会话如何存储,主要存储哪些信息?</p>
<p>由于我们使用了Redis中间件,所以,在Spring Security权限框架中产生的Session会话信息,肯定存储与Redis中,这点毫无疑问,那么存储了哪些信息呢?我会在接下来的源码分析中进行介绍</p>
<p>3)、如何根据账号收集当前该账号登录的所有Session会话信息?</p>
<p>我们从上面的需求分析中已经得知Session会话已经存储在Redis中,那么我们是否可以做这样的假设,我们只需要根据Spring Security中在Redis中存储的键值,找到和登录用户名相关的Redis缓存数据,就可以通过调用Security封装的方法进行获取,得到当前登录账号的会话信息呢?这个我们需要在源码中去找到答案</p>
<p>4)、如何在服务端主动销毁Session对象?</p>
<p>如果是单体的Spring Boot应用,Session信息肯定存储在JVM的内存中,服务端要主动销毁Session对象只需要找到Security权限框架如何存储的就可以进行删除。</p>
<p>在分布式的Spring Boot应用中,我们从上面已经得知Session会话信息以及存储在Redis中间件中,那么我们只需要得到当前登录的Session在Redis中的键值,就可以调用方法进行删除操作,从而主动在服务端销毁Session会话</p>
<h2 id="3源码分析">3.源码分析</h2>
<p><strong>在上面的需求分析中,我们已经提出了假设,并且根据假设,做出来技术性的判断,接下来我们需要从Spring Security以及Spring Session组件的源码中,去寻找我们需要的答案。</strong></p>
<p>首先,我们在源码分析前,我们需要找到入口,也就是我们在使用Spring Security框架,并且使用Spring Session组件时,我们如何使用的。</p>
<p>在<code class="highlighter-rouge">pom.xml</code>文件中引入组件的依赖是必不可少的,如下:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!--Spring Security组件--></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-starter-security<span class="nt"></artifactId></span>
<span class="nt"></dependency></span>
<span class="c"><!--Spring针对Redis操作组件--></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-starter-data-redis<span class="nt"></artifactId></span>
<span class="nt"></dependency></span>
<span class="c"><!--Spring Session集成Redis分布式Session会话--></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.springframework.session<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-session-data-redis<span class="nt"></artifactId></span>
<span class="nt"></dependency></span>
</code></pre></div></div>
<p>接下来,我们在Spring Boot项目中,需要添加<code class="highlighter-rouge">@EnableRedisHttpSession</code>注解,以开启Redis组件对Session会话的支持,该注解我们需要制定Spring Session在Redis中存储的Redis命名空间,已经Session会话的时效性,示例代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@SpringBootApplication</span>
<span class="nd">@EnableRedisHttpSession</span><span class="o">(</span><span class="n">redisNamespace</span> <span class="o">=</span> <span class="s">"fish-admin:session"</span><span class="o">,</span><span class="n">maxInactiveIntervalInSeconds</span> <span class="o">=</span> <span class="mi">7200</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">FishAdminApplication</span> <span class="o">{</span>
<span class="kd">static</span> <span class="n">Logger</span> <span class="n">logger</span><span class="o">=</span> <span class="n">LoggerFactory</span><span class="o">.</span><span class="na">getLogger</span><span class="o">(</span><span class="n">FishAdminApplication</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="n">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">UnknownHostException</span> <span class="o">{</span>
<span class="n">ConfigurableApplicationContext</span> <span class="n">application</span><span class="o">=</span><span class="n">SpringApplication</span><span class="o">.</span><span class="na">run</span><span class="o">(</span><span class="n">FishAdminApplication</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="n">args</span><span class="o">);</span>
<span class="n">Environment</span> <span class="n">env</span> <span class="o">=</span> <span class="n">application</span><span class="o">.</span><span class="na">getEnvironment</span><span class="o">();</span>
<span class="n">String</span> <span class="n">host</span><span class="o">=</span> <span class="n">InetAddress</span><span class="o">.</span><span class="na">getLocalHost</span><span class="o">().</span><span class="na">getHostAddress</span><span class="o">();</span>
<span class="n">String</span> <span class="n">port</span><span class="o">=</span><span class="n">env</span><span class="o">.</span><span class="na">getProperty</span><span class="o">(</span><span class="s">"server.port"</span><span class="o">);</span>
<span class="n">logger</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"\n----------------------------------------------------------\n\t"</span> <span class="o">+</span>
<span class="s">"Application '{}' is running! Access URLs:\n\t"</span> <span class="o">+</span>
<span class="s">"Local: \t\thttp://localhost:{}\n\t"</span> <span class="o">+</span>
<span class="s">"External: \thttp://{}:{}\n\t"</span><span class="o">+</span>
<span class="s">"Doc: \thttp://{}:{}/doc.html\n\t"</span><span class="o">+</span>
<span class="s">"----------------------------------------------------------"</span><span class="o">,</span>
<span class="n">env</span><span class="o">.</span><span class="na">getProperty</span><span class="o">(</span><span class="s">"spring.application.name"</span><span class="o">),</span>
<span class="n">env</span><span class="o">.</span><span class="na">getProperty</span><span class="o">(</span><span class="s">"server.port"</span><span class="o">),</span>
<span class="n">host</span><span class="o">,</span><span class="n">port</span><span class="o">,</span>
<span class="n">host</span><span class="o">,</span><span class="n">port</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在上面的代码中,我们指定Redis的命名空间是<code class="highlighter-rouge">fish-admin:session</code>,默认最大失效<code class="highlighter-rouge">7200</code>秒。</p>
<p>如果开发者默认不指定这两个属性的话,命名空间默认值是<code class="highlighter-rouge">spring:session</code>,默认最大时效则是<code class="highlighter-rouge">1800</code>秒</p>
<p>在上面我们已经说过了,既然是看源码,我们需要找到入口,这是看源码最好的方式,我们在使用Spring Session组件时,需要使用<code class="highlighter-rouge">@EnableRedisHttpSession</code>注解,那么该注解就是我们需要重点关注的对象,我们需要搞清楚,该注解的作用是什么?</p>
<p><code class="highlighter-rouge">EnableRedisHttpSession.java</code>部分源码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Retention</span><span class="o">(</span><span class="n">RetentionPolicy</span><span class="o">.</span><span class="na">RUNTIME</span><span class="o">)</span>
<span class="nd">@Target</span><span class="o">(</span><span class="n">ElementType</span><span class="o">.</span><span class="na">TYPE</span><span class="o">)</span>
<span class="nd">@Documented</span>
<span class="nd">@Import</span><span class="o">(</span><span class="n">RedisHttpSessionConfiguration</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="nd">@Configuration</span><span class="o">(</span><span class="n">proxyBeanMethods</span> <span class="o">=</span> <span class="kc">false</span><span class="o">)</span>
<span class="kd">public</span> <span class="nd">@interface</span> <span class="n">EnableRedisHttpSession</span> <span class="o">{</span>
<span class="c1">//more property.. </span>
<span class="o">}</span>
</code></pre></div></div>
<p>在该注解中,我们可以看到,最关键的是在该注解之上,使用<code class="highlighter-rouge">@Import</code>注解导入了<code class="highlighter-rouge">RedisHttpSessionConfiguration.java</code>配置类,如果你经常翻看Spring Boot相关的源码,你会敏锐的察觉到,该配置类就是我们最终要找的类</p>
<p>先来看该类的UML图,如下:</p>
<p><img src="/assets/images/springboot/security-out-session/RedisHttpSessionConfiguration.png" alt="RedisHttpSessionConfiguration" /></p>
<p>该类实现了Spring框架中很多<code class="highlighter-rouge">Aware</code>类型接口,<code class="highlighter-rouge">Aware</code>类型的接口我们都知道,Spring容器在启动创建实体Bean后,会调用<code class="highlighter-rouge">Aware</code>系列的set方法传参赋值</p>
<p>当然,最核心的,我们从源码中可以看到,是Spring Session组件会向Spring容器中注入两个实体<code class="highlighter-rouge">Bean</code>,代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Bean</span>
<span class="kd">public</span> <span class="n">RedisIndexedSessionRepository</span> <span class="nf">sessionRepository</span><span class="o">()</span> <span class="o">{</span>
<span class="n">RedisTemplate</span><span class="o"><</span><span class="n">Object</span><span class="o">,</span> <span class="n">Object</span><span class="o">></span> <span class="n">redisTemplate</span> <span class="o">=</span> <span class="n">createRedisTemplate</span><span class="o">();</span>
<span class="n">RedisIndexedSessionRepository</span> <span class="n">sessionRepository</span> <span class="o">=</span> <span class="k">new</span> <span class="n">RedisIndexedSessionRepository</span><span class="o">(</span><span class="n">redisTemplate</span><span class="o">);</span>
<span class="n">sessionRepository</span><span class="o">.</span><span class="na">setApplicationEventPublisher</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">applicationEventPublisher</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">indexResolver</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="n">sessionRepository</span><span class="o">.</span><span class="na">setIndexResolver</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">indexResolver</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">if</span> <span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">defaultRedisSerializer</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="n">sessionRepository</span><span class="o">.</span><span class="na">setDefaultSerializer</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">defaultRedisSerializer</span><span class="o">);</span>
<span class="o">}</span>
<span class="n">sessionRepository</span><span class="o">.</span><span class="na">setDefaultMaxInactiveInterval</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">maxInactiveIntervalInSeconds</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">StringUtils</span><span class="o">.</span><span class="na">hasText</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">redisNamespace</span><span class="o">))</span> <span class="o">{</span>
<span class="n">sessionRepository</span><span class="o">.</span><span class="na">setRedisKeyNamespace</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">redisNamespace</span><span class="o">);</span>
<span class="o">}</span>
<span class="n">sessionRepository</span><span class="o">.</span><span class="na">setFlushMode</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">flushMode</span><span class="o">);</span>
<span class="n">sessionRepository</span><span class="o">.</span><span class="na">setSaveMode</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">saveMode</span><span class="o">);</span>
<span class="kt">int</span> <span class="n">database</span> <span class="o">=</span> <span class="n">resolveDatabase</span><span class="o">();</span>
<span class="n">sessionRepository</span><span class="o">.</span><span class="na">setDatabase</span><span class="o">(</span><span class="n">database</span><span class="o">);</span>
<span class="k">this</span><span class="o">.</span><span class="na">sessionRepositoryCustomizers</span>
<span class="o">.</span><span class="na">forEach</span><span class="o">((</span><span class="n">sessionRepositoryCustomizer</span><span class="o">)</span> <span class="o">-></span> <span class="n">sessionRepositoryCustomizer</span><span class="o">.</span><span class="na">customize</span><span class="o">(</span><span class="n">sessionRepository</span><span class="o">));</span>
<span class="k">return</span> <span class="n">sessionRepository</span><span class="o">;</span>
<span class="o">}</span>
<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="n">RedisMessageListenerContainer</span> <span class="nf">springSessionRedisMessageListenerContainer</span><span class="o">(</span>
<span class="n">RedisIndexedSessionRepository</span> <span class="n">sessionRepository</span><span class="o">)</span> <span class="o">{</span>
<span class="n">RedisMessageListenerContainer</span> <span class="n">container</span> <span class="o">=</span> <span class="k">new</span> <span class="n">RedisMessageListenerContainer</span><span class="o">();</span>
<span class="n">container</span><span class="o">.</span><span class="na">setConnectionFactory</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">redisConnectionFactory</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">redisTaskExecutor</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="n">container</span><span class="o">.</span><span class="na">setTaskExecutor</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">redisTaskExecutor</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">if</span> <span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">redisSubscriptionExecutor</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="n">container</span><span class="o">.</span><span class="na">setSubscriptionExecutor</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">redisSubscriptionExecutor</span><span class="o">);</span>
<span class="o">}</span>
<span class="n">container</span><span class="o">.</span><span class="na">addMessageListener</span><span class="o">(</span><span class="n">sessionRepository</span><span class="o">,</span>
<span class="n">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="k">new</span> <span class="n">ChannelTopic</span><span class="o">(</span><span class="n">sessionRepository</span><span class="o">.</span><span class="na">getSessionDeletedChannel</span><span class="o">()),</span>
<span class="k">new</span> <span class="nf">ChannelTopic</span><span class="o">(</span><span class="n">sessionRepository</span><span class="o">.</span><span class="na">getSessionExpiredChannel</span><span class="o">())));</span>
<span class="n">container</span><span class="o">.</span><span class="na">addMessageListener</span><span class="o">(</span><span class="n">sessionRepository</span><span class="o">,</span>
<span class="n">Collections</span><span class="o">.</span><span class="na">singletonList</span><span class="o">(</span><span class="k">new</span> <span class="n">PatternTopic</span><span class="o">(</span><span class="n">sessionRepository</span><span class="o">.</span><span class="na">getSessionCreatedChannelPrefix</span><span class="o">()</span> <span class="o">+</span> <span class="s">"*"</span><span class="o">)));</span>
<span class="k">return</span> <span class="n">container</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p><code class="highlighter-rouge">RedisIndexedSessionRepository</code>以及<code class="highlighter-rouge">RedisMessageListenerContainer</code>的实体Bean</p>
<ul>
<li><code class="highlighter-rouge">RedisMessageListenerContainer</code>:该类是Redis的消息通知回调机制实体类,Redis提供了针对不同Key的操作回调消息通知,比如常见的删除key、key过期等事件的回调,在Spring Session组件中注入该实体Bean,从代码中也可以看出是用来监听处理Session会话的过期以及删除事件</li>
<li><code class="highlighter-rouge">RedisIndexedSessionRepository</code>:该类是Spring Session组件提供基于Redis的针对Session会话一系列操作的具体实现类,是我们接下来源码分析的重点。</li>
</ul>
<p>先来看<code class="highlighter-rouge">RedisIndexedSessionRepository</code>类的UML类图结构,如下图:</p>
<p><img src="/assets/images/springboot/security-out-session/RedisIndexedSessionRepository.png" alt="RedisIndexedSessionRepository" /></p>
<p><code class="highlighter-rouge">RedisIndexedSessionRepository</code>实现了<code class="highlighter-rouge">FindByIndexNameSessionRepository</code>接口,而<code class="highlighter-rouge">FindByIndexNameSessionRepository</code>接口又继承Spring Security权限框架提供的顶级<code class="highlighter-rouge">SessionRepository</code>接口,UML类图中,我们可以得到几个重要的信息:</p>
<ul>
<li><code class="highlighter-rouge">RedisIndexedSessionRepository</code>拥有创建Session会话、销毁删除Session会话的能力</li>
<li><code class="highlighter-rouge">RedisIndexedSessionRepository</code>由于实现自<code class="highlighter-rouge">FindByIndexNameSessionRepository</code>接口,而该接口提供了根据<code class="highlighter-rouge">PrincipalName</code>查找Session会话的能力</li>
<li>拥有Redis回调事件的处理消息能力,因为实现了<code class="highlighter-rouge">MessageListener</code>接口</li>
</ul>
<p><code class="highlighter-rouge">SessionRepository</code>是Spring Security提供的顶级接口,源码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">SessionRepository</span><span class="o"><</span><span class="n">S</span> <span class="kd">extends</span> <span class="n">Session</span><span class="o">></span> <span class="o">{</span>
<span class="cm">/**
* Creates a new {@link Session} that is capable of being persisted by this
* {@link SessionRepository}.
*
* <p>
* This allows optimizations and customizations in how the {@link Session} is
* persisted. For example, the implementation returned might keep track of the changes
* ensuring that only the delta needs to be persisted on a save.
* </p>
* @return a new {@link Session} that is capable of being persisted by this
* {@link SessionRepository}
*/</span>
<span class="n">S</span> <span class="nf">createSession</span><span class="o">();</span>
<span class="cm">/**
* Ensures the {@link Session} created by
* {@link org.springframework.session.SessionRepository#createSession()} is saved.
*
* <p>
* Some implementations may choose to save as the {@link Session} is updated by
* returning a {@link Session} that immediately persists any changes. In this case,
* this method may not actually do anything.
* </p>
* @param session the {@link Session} to save
*/</span>
<span class="kt">void</span> <span class="nf">save</span><span class="o">(</span><span class="n">S</span> <span class="n">session</span><span class="o">);</span>
<span class="cm">/**
* Gets the {@link Session} by the {@link Session#getId()} or null if no
* {@link Session} is found.
* @param id the {@link org.springframework.session.Session#getId()} to lookup
* @return the {@link Session} by the {@link Session#getId()} or null if no
* {@link Session} is found.
*/</span>
<span class="n">S</span> <span class="nf">findById</span><span class="o">(</span><span class="n">String</span> <span class="n">id</span><span class="o">);</span>
<span class="cm">/**
* Deletes the {@link Session} with the given {@link Session#getId()} or does nothing
* if the {@link Session} is not found.
* @param id the {@link org.springframework.session.Session#getId()} to delete
*/</span>
<span class="kt">void</span> <span class="nf">deleteById</span><span class="o">(</span><span class="n">String</span> <span class="n">id</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>该接口提供四个方法:</p>
<ul>
<li><code class="highlighter-rouge">createSession</code>:创建Session会话</li>
<li><code class="highlighter-rouge">save</code>:保存Session会话</li>
<li><code class="highlighter-rouge">findById</code>:根据<code class="highlighter-rouge">SessionId</code>查找获取Session会话对象信息</li>
<li><code class="highlighter-rouge">deleteById</code>:根据<code class="highlighter-rouge">SessionId</code>进行删除</li>
</ul>
<p><code class="highlighter-rouge">FindByIndexNameSessionRepository</code>源码主要是提供根据账号名称进行查询的功能,如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">FindByIndexNameSessionRepository</span><span class="o"><</span><span class="n">S</span> <span class="kd">extends</span> <span class="n">Session</span><span class="o">></span> <span class="kd">extends</span> <span class="n">SessionRepository</span><span class="o"><</span><span class="n">S</span><span class="o">></span> <span class="o">{</span>
<span class="cm">/**
* 当前存储的用户名前缀,使用Redis进行存储时,存储的key值是:redisNamespace+
*/</span>
<span class="n">String</span> <span class="n">PRINCIPAL_NAME_INDEX_NAME</span> <span class="o">=</span> <span class="n">FindByIndexNameSessionRepository</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getName</span><span class="o">()</span>
<span class="o">.</span><span class="na">concat</span><span class="o">(</span><span class="s">".PRINCIPAL_NAME_INDEX_NAME"</span><span class="o">);</span>
<span class="cm">/**
* Find a {@link Map} of the session id to the {@link Session} of all sessions that
* contain the specified index name index value.
* @param indexName the name of the index (i.e.
* {@link FindByIndexNameSessionRepository#PRINCIPAL_NAME_INDEX_NAME})
* @param indexValue the value of the index to search for.
* @return a {@code Map} (never {@code null}) of the session id to the {@code Session}
* of all sessions that contain the specified index name and index value. If no
* results are found, an empty {@code Map} is returned.
*/</span>
<span class="n">Map</span><span class="o"><</span><span class="n">String</span><span class="o">,</span> <span class="n">S</span><span class="o">></span> <span class="nf">findByIndexNameAndIndexValue</span><span class="o">(</span><span class="n">String</span> <span class="n">indexName</span><span class="o">,</span> <span class="n">String</span> <span class="n">indexValue</span><span class="o">);</span>
<span class="cm">/**
* Find a {@link Map} of the session id to the {@link Session} of all sessions that
* contain the index with the name
* {@link FindByIndexNameSessionRepository#PRINCIPAL_NAME_INDEX_NAME} and the
* specified principal name.
* @param principalName the principal name
* @return a {@code Map} (never {@code null}) of the session id to the {@code Session}
* of all sessions that contain the specified principal name. If no results are found,
* an empty {@code Map} is returned.
* @since 2.1.0
*/</span>
<span class="k">default</span> <span class="n">Map</span><span class="o"><</span><span class="n">String</span><span class="o">,</span> <span class="n">S</span><span class="o">></span> <span class="nf">findByPrincipalName</span><span class="o">(</span><span class="n">String</span> <span class="n">principalName</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="nf">findByIndexNameAndIndexValue</span><span class="o">(</span><span class="n">PRINCIPAL_NAME_INDEX_NAME</span><span class="o">,</span> <span class="n">principalName</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>该接口最核心的功能是提供了根据用户名查找获取Session会话的接口,这对我们后面实现踢人功能很有帮助。</p>
<p>通过查看<code class="highlighter-rouge">SessionRepository</code>接口以及<code class="highlighter-rouge">FindByIndexNameSessionRepository</code>接口的源码我们得知:</p>
<ul>
<li>Redis的实现最终实现了这两个接口,因此获得了基于Redis中间件创建及销毁Session会话的能力</li>
<li>根据账号去查找当前的所有登录会话Session符合我们最终需要服务端主动踢人下线的功能需求。</li>
</ul>
<p>接下来我们只需要关注<code class="highlighter-rouge">RedisIndexedSessionRepository</code>的实现即可。首先来看<code class="highlighter-rouge">findByPrincipalName</code>方法,源码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Override</span>
<span class="kd">public</span> <span class="n">Map</span><span class="o"><</span><span class="n">String</span><span class="o">,</span> <span class="n">RedisSession</span><span class="o">></span> <span class="nf">findByIndexNameAndIndexValue</span><span class="o">(</span><span class="n">String</span> <span class="n">indexName</span><span class="o">,</span> <span class="n">String</span> <span class="n">indexValue</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">//如果名称不匹配,则直接反馈空集合Map</span>
<span class="k">if</span> <span class="o">(!</span><span class="n">PRINCIPAL_NAME_INDEX_NAME</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">indexName</span><span class="o">))</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">Collections</span><span class="o">.</span><span class="na">emptyMap</span><span class="o">();</span>
<span class="o">}</span>
<span class="c1">//获取拼装的Key值</span>
<span class="n">String</span> <span class="n">principalKey</span> <span class="o">=</span> <span class="n">getPrincipalKey</span><span class="o">(</span><span class="n">indexValue</span><span class="o">);</span>
<span class="c1">//从Redis中获取该Key值的成员数</span>
<span class="n">Set</span><span class="o"><</span><span class="n">Object</span><span class="o">></span> <span class="n">sessionIds</span> <span class="o">=</span> <span class="k">this</span><span class="o">.</span><span class="na">sessionRedisOperations</span><span class="o">.</span><span class="na">boundSetOps</span><span class="o">(</span><span class="n">principalKey</span><span class="o">).</span><span class="na">members</span><span class="o">();</span>
<span class="c1">//初始化Map集合</span>
<span class="n">Map</span><span class="o"><</span><span class="n">String</span><span class="o">,</span> <span class="n">RedisSession</span><span class="o">></span> <span class="n">sessions</span> <span class="o">=</span> <span class="k">new</span> <span class="n">HashMap</span><span class="o"><>(</span><span class="n">sessionIds</span><span class="o">.</span><span class="na">size</span><span class="o">());</span>
<span class="c1">//循环遍历</span>
<span class="k">for</span> <span class="o">(</span><span class="n">Object</span> <span class="n">id</span> <span class="o">:</span> <span class="n">sessionIds</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">//根据id查找Session会话</span>
<span class="n">RedisSession</span> <span class="n">session</span> <span class="o">=</span> <span class="n">findById</span><span class="o">((</span><span class="n">String</span><span class="o">)</span> <span class="n">id</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">session</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="n">sessions</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="n">session</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="n">session</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="k">return</span> <span class="n">sessions</span><span class="o">;</span>
<span class="o">}</span>
<span class="n">String</span> <span class="nf">getPrincipalKey</span><span class="o">(</span><span class="n">String</span> <span class="n">principalName</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="k">this</span><span class="o">.</span><span class="na">namespace</span> <span class="o">+</span> <span class="s">"index:"</span> <span class="o">+</span> <span class="n">FindByIndexNameSessionRepository</span><span class="o">.</span><span class="na">PRINCIPAL_NAME_INDEX_NAME</span> <span class="o">+</span> <span class="s">":"</span>
<span class="o">+</span> <span class="n">principalName</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>接下来我们看删除Session会话的方法实现:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">deleteById</span><span class="o">(</span><span class="n">String</span> <span class="n">sessionId</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">//根据sessionId获取Session会话</span>
<span class="n">RedisSession</span> <span class="n">session</span> <span class="o">=</span> <span class="n">getSession</span><span class="o">(</span><span class="n">sessionId</span><span class="o">,</span> <span class="kc">true</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">session</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span><span class="o">;</span>
<span class="o">}</span>
<span class="c1">//从Redis中移除所有存储的针对principal的key值</span>
<span class="n">cleanupPrincipalIndex</span><span class="o">(</span><span class="n">session</span><span class="o">);</span>
<span class="c1">//Redis中删除SessionId所对应的key值</span>
<span class="k">this</span><span class="o">.</span><span class="na">expirationPolicy</span><span class="o">.</span><span class="na">onDelete</span><span class="o">(</span><span class="n">session</span><span class="o">);</span>
<span class="c1">//移除Session会话创建时,存储的过期key值</span>
<span class="n">String</span> <span class="n">expireKey</span> <span class="o">=</span> <span class="n">getExpiredKey</span><span class="o">(</span><span class="n">session</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
<span class="k">this</span><span class="o">.</span><span class="na">sessionRedisOperations</span><span class="o">.</span><span class="na">delete</span><span class="o">(</span><span class="n">expireKey</span><span class="o">);</span>
<span class="c1">//设置当前session会话最大存活时间为0</span>
<span class="n">session</span><span class="o">.</span><span class="na">setMaxInactiveInterval</span><span class="o">(</span><span class="n">Duration</span><span class="o">.</span><span class="na">ZERO</span><span class="o">);</span>
<span class="c1">//执行save方法</span>
<span class="n">save</span><span class="o">(</span><span class="n">session</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>从上面的代码中,我们已经知道了Spring Session组件对于Session相关的处理方法,其实我们基于上面的两个核心方法,我们已经获得了踢人下线的能力,但是,既然<code class="highlighter-rouge">RedisIndexedSessionRepository</code>实现了<code class="highlighter-rouge">MessageListener</code>接口,我们需要继续跟踪一下该接口的具体实现方法,我们直接来看<code class="highlighter-rouge">onMessage</code>方法,代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">onMessage</span><span class="o">(</span><span class="n">Message</span> <span class="n">message</span><span class="o">,</span> <span class="kt">byte</span><span class="o">[]</span> <span class="n">pattern</span><span class="o">)</span> <span class="o">{</span>
<span class="kt">byte</span><span class="o">[]</span> <span class="n">messageChannel</span> <span class="o">=</span> <span class="n">message</span><span class="o">.</span><span class="na">getChannel</span><span class="o">();</span>
<span class="kt">byte</span><span class="o">[]</span> <span class="n">messageBody</span> <span class="o">=</span> <span class="n">message</span><span class="o">.</span><span class="na">getBody</span><span class="o">();</span>
<span class="n">String</span> <span class="n">channel</span> <span class="o">=</span> <span class="k">new</span> <span class="n">String</span><span class="o">(</span><span class="n">messageChannel</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">channel</span><span class="o">.</span><span class="na">startsWith</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">sessionCreatedChannelPrefix</span><span class="o">))</span> <span class="o">{</span>
<span class="c1">// TODO: is this thread safe?</span>
<span class="nd">@SuppressWarnings</span><span class="o">(</span><span class="s">"unchecked"</span><span class="o">)</span>
<span class="n">Map</span><span class="o"><</span><span class="n">Object</span><span class="o">,</span> <span class="n">Object</span><span class="o">></span> <span class="n">loaded</span> <span class="o">=</span> <span class="o">(</span><span class="n">Map</span><span class="o"><</span><span class="n">Object</span><span class="o">,</span> <span class="n">Object</span><span class="o">>)</span> <span class="k">this</span><span class="o">.</span><span class="na">defaultSerializer</span><span class="o">.</span><span class="na">deserialize</span><span class="o">(</span><span class="n">message</span><span class="o">.</span><span class="na">getBody</span><span class="o">());</span>
<span class="n">handleCreated</span><span class="o">(</span><span class="n">loaded</span><span class="o">,</span> <span class="n">channel</span><span class="o">);</span>
<span class="k">return</span><span class="o">;</span>
<span class="o">}</span>
<span class="n">String</span> <span class="n">body</span> <span class="o">=</span> <span class="k">new</span> <span class="n">String</span><span class="o">(</span><span class="n">messageBody</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(!</span><span class="n">body</span><span class="o">.</span><span class="na">startsWith</span><span class="o">(</span><span class="n">getExpiredKeyPrefix</span><span class="o">()))</span> <span class="o">{</span>
<span class="k">return</span><span class="o">;</span>
<span class="o">}</span>
<span class="kt">boolean</span> <span class="n">isDeleted</span> <span class="o">=</span> <span class="n">channel</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">sessionDeletedChannel</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">isDeleted</span> <span class="o">||</span> <span class="n">channel</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">sessionExpiredChannel</span><span class="o">))</span> <span class="o">{</span>
<span class="kt">int</span> <span class="n">beginIndex</span> <span class="o">=</span> <span class="n">body</span><span class="o">.</span><span class="na">lastIndexOf</span><span class="o">(</span><span class="s">":"</span><span class="o">)</span> <span class="o">+</span> <span class="mi">1</span><span class="o">;</span>
<span class="kt">int</span> <span class="n">endIndex</span> <span class="o">=</span> <span class="n">body</span><span class="o">.</span><span class="na">length</span><span class="o">();</span>
<span class="n">String</span> <span class="n">sessionId</span> <span class="o">=</span> <span class="n">body</span><span class="o">.</span><span class="na">substring</span><span class="o">(</span><span class="n">beginIndex</span><span class="o">,</span> <span class="n">endIndex</span><span class="o">);</span>
<span class="n">RedisSession</span> <span class="n">session</span> <span class="o">=</span> <span class="n">getSession</span><span class="o">(</span><span class="n">sessionId</span><span class="o">,</span> <span class="kc">true</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">session</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="n">logger</span><span class="o">.</span><span class="na">warn</span><span class="o">(</span><span class="s">"Unable to publish SessionDestroyedEvent for session "</span> <span class="o">+</span> <span class="n">sessionId</span><span class="o">);</span>
<span class="k">return</span><span class="o">;</span>
<span class="o">}</span>
<span class="k">if</span> <span class="o">(</span><span class="n">logger</span><span class="o">.</span><span class="na">isDebugEnabled</span><span class="o">())</span> <span class="o">{</span>
<span class="n">logger</span><span class="o">.</span><span class="na">debug</span><span class="o">(</span><span class="s">"Publishing SessionDestroyedEvent for session "</span> <span class="o">+</span> <span class="n">sessionId</span><span class="o">);</span>
<span class="o">}</span>
<span class="n">cleanupPrincipalIndex</span><span class="o">(</span><span class="n">session</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">isDeleted</span><span class="o">)</span> <span class="o">{</span>
<span class="n">handleDeleted</span><span class="o">(</span><span class="n">session</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">else</span> <span class="o">{</span>
<span class="n">handleExpired</span><span class="o">(</span><span class="n">session</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="kt">void</span> <span class="nf">handleDeleted</span><span class="o">(</span><span class="n">RedisSession</span> <span class="n">session</span><span class="o">)</span> <span class="o">{</span>
<span class="n">publishEvent</span><span class="o">(</span><span class="k">new</span> <span class="n">SessionDeletedEvent</span><span class="o">(</span><span class="k">this</span><span class="o">,</span> <span class="n">session</span><span class="o">));</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="kt">void</span> <span class="nf">handleExpired</span><span class="o">(</span><span class="n">RedisSession</span> <span class="n">session</span><span class="o">)</span> <span class="o">{</span>
<span class="n">publishEvent</span><span class="o">(</span><span class="k">new</span> <span class="n">SessionExpiredEvent</span><span class="o">(</span><span class="k">this</span><span class="o">,</span> <span class="n">session</span><span class="o">));</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="kt">void</span> <span class="nf">publishEvent</span><span class="o">(</span><span class="n">ApplicationEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span>
<span class="k">try</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">eventPublisher</span><span class="o">.</span><span class="na">publishEvent</span><span class="o">(</span><span class="n">event</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">catch</span> <span class="o">(</span><span class="n">Throwable</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
<span class="n">logger</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="s">"Error publishing "</span> <span class="o">+</span> <span class="n">event</span> <span class="o">+</span> <span class="s">"."</span><span class="o">,</span> <span class="n">ex</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在<code class="highlighter-rouge">onMessage</code>方法中,最核心的是最后一个判断,分别执行<code class="highlighter-rouge">handleDeleted</code>和<code class="highlighter-rouge">handleExpired</code>方法,从源码中我们可以看到,当当前Session会话被删除或者失效时,Spring Session会通过<code class="highlighter-rouge">ApplicationEventPublisher</code>广播一个事件,分别处理<code class="highlighter-rouge">SessionExpiredEvent</code>和<code class="highlighter-rouge">SessionDeletedEvent</code>事件</p>
<p>这是Spring Session组件为开发者预留的针对Session会话的Event事件,如果开发者对于当前的<code class="highlighter-rouge">Sesssion</code>会话的删除或者失效有特殊的处理需求,则可以通过监听该事件进行处理。</p>
<p>例如,开发者针对Session会话的操作都需要做业务操作,记录日志保存到DB数据库中,此时,开发者只需要使用Spring提供的<code class="highlighter-rouge">EventListener</code>实现就可以很轻松的实现,示例代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SecuritySessionEventListener</span> <span class="o">{</span>
<span class="nd">@EventListener</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">sessionDestroyed</span><span class="o">(</span><span class="n">SessionDestroyedEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">//session销毁事件处理方法...</span>
<span class="o">}</span>
<span class="nd">@EventListener</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">sessionCreated</span><span class="o">(</span><span class="n">SessionCreatedEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">//session创建会话事件处理方法...</span>
<span class="o">}</span>
<span class="nd">@EventListener</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">sessionExired</span><span class="o">(</span><span class="n">SessionExpiredEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">//session会话过期事件处理方法...</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<h2 id="4解决方案">4.解决方案</h2>
<p>我们分析了Spring Session针对Session基于Redis的实现,接下来,我们从源码中已经知道了该如何查找Session会话以及销毁会话的方法,此时,我们可以来改造我们的框架代码了</p>
<p>创建<code class="highlighter-rouge">SessionService</code>接口,代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">SessionService</span> <span class="o">{</span>
<span class="cm">/**
*
* @param account
* @return
*/</span>
<span class="kt">boolean</span> <span class="nf">hasLogin</span><span class="o">(</span><span class="n">String</span> <span class="n">account</span><span class="o">);</span>
<span class="cm">/**
* 根據账号查找当前session会话
* @param account 账号
* @return
*/</span>
<span class="n">Map</span><span class="o"><</span><span class="n">String</span><span class="o">,</span> <span class="o">?</span> <span class="kd">extends</span> <span class="n">Session</span><span class="o">></span> <span class="nf">loadByAccount</span><span class="o">(</span><span class="n">String</span> <span class="n">account</span><span class="o">);</span>
<span class="cm">/**
* 销毁当前session会话
* @param account
*/</span>
<span class="kt">void</span> <span class="nf">destroySession</span><span class="o">(</span><span class="n">String</span> <span class="n">account</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>声明该接口主要包含3个方法:</p>
<ul>
<li>hasLogin:通过传递登录账号,判断该账号是否已经登录过,该方法是一个业务的延伸,比如我们对当前账号判断是否已经登录过,如果登录则提示需要退出才能继续登录的操作等</li>
<li>loadByAccount:根据登录账号获取当前已经登录的Session会话Map集合</li>
<li>destroySession:根据登录账号销毁当前所有该账号的Session会话信息,此接口和产品经理要求的踢人下线操作一致</li>
</ul>
<p>接下来就是实现类,由于我们是基于Redis来处理,因此,我们需要将源码分析中的<code class="highlighter-rouge">RedisIndexedSessionRepository</code>实体Bean进行引入,借助该类实现该接口方法</p>
<p><code class="highlighter-rouge">RedisSessionService</code>方法实现如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
* SpringSession集成底层Redis实现,如果底层分布式会话保持方式不是基于Redis,则该类无法正常使用
* @author <a href="mailto:xiaoymin@foxmail.com">xiaoymin@foxmail.com</a>
* 2021/04/20 16:23
* @since:fish 1.0
*/</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">RedisSessionService</span> <span class="kd">implements</span> <span class="n">SessionService</span> <span class="o">{</span>
<span class="n">Logger</span> <span class="n">logger</span><span class="o">=</span> <span class="n">LoggerFactory</span><span class="o">.</span><span class="na">getLogger</span><span class="o">(</span><span class="n">RedisSessionService</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
<span class="kd">final</span> <span class="n">RedisIndexedSessionRepository</span> <span class="n">redisIndexedSessionRepository</span><span class="o">;</span>
<span class="kd">final</span> <span class="n">ApplicationEventPublisher</span> <span class="n">applicationEventPublisher</span><span class="o">;</span>
<span class="kd">public</span> <span class="nf">RedisSessionService</span><span class="o">(</span><span class="n">RedisIndexedSessionRepository</span> <span class="n">redisIndexedSessionRepository</span><span class="o">,</span> <span class="n">ApplicationEventPublisher</span> <span class="n">applicationEventPublisher</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">redisIndexedSessionRepository</span> <span class="o">=</span> <span class="n">redisIndexedSessionRepository</span><span class="o">;</span>
<span class="k">this</span><span class="o">.</span><span class="na">applicationEventPublisher</span> <span class="o">=</span> <span class="n">applicationEventPublisher</span><span class="o">;</span>
<span class="o">}</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">hasLogin</span><span class="o">(</span><span class="n">String</span> <span class="n">account</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">CollectionUtil</span><span class="o">.</span><span class="na">isNotEmpty</span><span class="o">(</span><span class="n">loadByAccount</span><span class="o">(</span><span class="n">account</span><span class="o">));</span>
<span class="o">}</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="n">Map</span><span class="o"><</span><span class="n">String</span><span class="o">,</span> <span class="o">?</span> <span class="kd">extends</span> <span class="n">Session</span><span class="o">></span> <span class="nf">loadByAccount</span><span class="o">(</span><span class="n">String</span> <span class="n">account</span><span class="o">)</span> <span class="o">{</span>
<span class="n">logger</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"收集当前登录会话session,账号:{}"</span><span class="o">,</span><span class="n">account</span><span class="o">);</span>
<span class="k">return</span> <span class="n">redisIndexedSessionRepository</span><span class="o">.</span><span class="na">findByIndexNameAndIndexValue</span><span class="o">(</span><span class="n">FindByIndexNameSessionRepository</span><span class="o">.</span><span class="na">PRINCIPAL_NAME_INDEX_NAME</span><span class="o">,</span><span class="n">account</span><span class="o">);</span>
<span class="o">}</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">destroySession</span><span class="o">(</span><span class="n">String</span> <span class="n">account</span><span class="o">)</span> <span class="o">{</span>
<span class="n">logger</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"销毁当前登录session会话,账号:{}"</span><span class="o">,</span><span class="n">account</span><span class="o">);</span>
<span class="n">Map</span><span class="o"><</span><span class="n">String</span><span class="o">,?</span> <span class="kd">extends</span> <span class="n">Session</span><span class="o">></span> <span class="n">sessionMap</span><span class="o">=</span><span class="n">loadByAccount</span><span class="o">(</span><span class="n">account</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">CollectionUtil</span><span class="o">.</span><span class="na">isNotEmpty</span><span class="o">(</span><span class="n">sessionMap</span><span class="o">)){</span>
<span class="n">logger</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"当前登录会话size:{}"</span><span class="o">,</span><span class="n">sessionMap</span><span class="o">.</span><span class="na">size</span><span class="o">());</span>
<span class="k">for</span> <span class="o">(</span><span class="n">Map</span><span class="o">.</span><span class="na">Entry</span><span class="o"><</span><span class="n">String</span><span class="o">,?</span> <span class="kd">extends</span> <span class="n">Session</span><span class="o">></span> <span class="nl">sessionEntry:</span><span class="n">sessionMap</span><span class="o">.</span><span class="na">entrySet</span><span class="o">()){</span>
<span class="n">String</span> <span class="n">key</span><span class="o">=</span><span class="n">sessionEntry</span><span class="o">.</span><span class="na">getKey</span><span class="o">();</span>
<span class="n">Session</span> <span class="n">session</span><span class="o">=</span><span class="n">sessionEntry</span><span class="o">.</span><span class="na">getValue</span><span class="o">();</span>
<span class="n">logger</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"destroy session key:{}"</span><span class="o">,</span><span class="n">key</span><span class="o">);</span>
<span class="c1">//删除</span>
<span class="n">redisIndexedSessionRepository</span><span class="o">.</span><span class="na">deleteById</span><span class="o">(</span><span class="n">session</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
<span class="c1">//广播Session会话销毁事件</span>
<span class="n">applicationEventPublisher</span><span class="o">.</span><span class="na">publishEvent</span><span class="o">(</span><span class="k">new</span> <span class="n">SessionDestroyedEvent</span><span class="o">(</span><span class="n">redisIndexedSessionRepository</span><span class="o">,</span><span class="n">session</span><span class="o">));</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在<code class="highlighter-rouge">destroySession</code>方法实现中,首先根据账号获取当前所有登录会话信息,如果会话不为空,则遍历会话Map集合,执行删除会话操作,并且通过<code class="highlighter-rouge">applicationEventPublisher</code>广播一个会话被销毁的事件。该广播事件非必须,但是从代码的全局进行考虑,还是需要加上</p>
<p>接下来,我们就可以将该类注入到Spring的容器中的,注入实体Bean代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Bean</span>
<span class="kd">public</span> <span class="n">RedisSessionService</span> <span class="nf">sessionService</span><span class="o">(</span><span class="n">RedisIndexedSessionRepository</span> <span class="n">redisIndexedSessionRepository</span><span class="o">,</span> <span class="n">ApplicationEventPublisher</span> <span class="n">applicationEventPublisher</span><span class="o">){</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">RedisSessionService</span><span class="o">(</span><span class="n">redisIndexedSessionRepository</span><span class="o">,</span><span class="n">applicationEventPublisher</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<blockquote>
<p><strong>PS:</strong>我们为什么需要创建接口而不是直接创建class的方式通过<code class="highlighter-rouge">@Service</code>等注解进行注入,而是通过抽象接口实现类的方式,最终通过JavaConfig的方式进行注入呢?从代码的耦合度上来看,由于Spring Session提供处理基于Redis的能力处理Session会话之外,还提供了诸如JDBC\mongo等多元化的扩展方式,因此,为了代码解耦,通过抽象接口的方式是更合理的。</p>
</blockquote>
<p>接下来,我们在我们的用户管理的业务Service方法中就可以进行操作了</p>
<p><strong>删除用户的业务Service方法</strong></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
* 根据主键id删除用户管理
* @param id 主键id
* @return 是否删除成功
*/</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="n">RestfulMessage</span><span class="o"><</span><span class="n">String</span><span class="o">></span> <span class="nf">delete</span><span class="o">(</span><span class="n">Integer</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
<span class="n">logger</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"根据主键id删除用户管理,id:{}"</span><span class="o">,</span><span class="n">id</span><span class="o">);</span>
<span class="n">FishUserInfo</span> <span class="n">fishUserInfo</span><span class="o">=</span><span class="n">fishUserInfoMapper</span><span class="o">.</span><span class="na">selectByPrimaryKey</span><span class="o">(</span><span class="n">id</span><span class="o">);</span>
<span class="n">assertArgumentNotEmpty</span><span class="o">(</span><span class="n">fishUserInfo</span><span class="o">,</span><span class="s">"请求数据非法"</span><span class="o">);</span>
<span class="kt">int</span> <span class="n">ret</span><span class="o">=</span><span class="n">fishUserInfoMapper</span><span class="o">.</span><span class="na">deleteByPrimaryKey</span><span class="o">(</span><span class="n">id</span><span class="o">);</span>
<span class="c1">//删除成功,如果该角色在线,则强制剔除下线</span>
<span class="k">if</span> <span class="o">(</span><span class="n">ret</span><span class="o">></span><span class="mi">0</span><span class="o">){</span>
<span class="n">logger</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"用户会话剔除下线"</span><span class="o">);</span>
<span class="n">sessionService</span><span class="o">.</span><span class="na">destroySession</span><span class="o">(</span><span class="n">fishUserInfo</span><span class="o">.</span><span class="na">getAccount</span><span class="o">());</span>
<span class="o">}</span>
<span class="k">return</span> <span class="n">ret</span><span class="o">></span><span class="mi">0</span><span class="o">?</span><span class="n">RestfulMessage</span><span class="o">.</span><span class="na">success</span><span class="o">(</span><span class="s">"删除成功"</span><span class="o">):</span><span class="n">RestfulMessage</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="s">"删除失败"</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p><strong>禁用用户</strong></p>
<blockquote>
<p>禁用用户其实操作方法和删除一样,区别在于禁用操作只是将用户在数据库中的状态进行变更,而删除则是将该用户的数据从数据库DB中进行删除。更新库的用户状态后,调用destroySession删除该账号的所有Session会话操作即可</p>
</blockquote>肖玉民1.背景Spring Boot框架中使用Jackson的处理总结2021-03-26T00:00:00+08:002021-03-26T00:00:00+08:00https://xiaoym.gitee.io/2021/03/26/spring-boot-code-action-jackson<h2 id="1前言">1.前言</h2>
<p>通常我们在使用Spring Boot框架时,如果没有特别指定接口的序列化类型,则会使用Spring Boot框架默认集成的Jackson框架进行处理,通过Jackson框架将服务端响应的数据序列化成JSON格式的数据。</p>
<p>本文主要针对在Spring Boot框架中使用Jackson进行处理的经验进行总结,同时也结合在实际开发场景中碰到的问题以及解决方案进行陈述。</p>
<p>本文涉及到的源码地址:<a href="https://gitee.com/dt_research_institute/code-in-action">https://gitee.com/dt_research_institute/code-in-action</a></p>
<blockquote>
<p>PS:目前市面上针对JSON序列化的框架很多,比较出名的就是<a href="https://github.com/FasterXML/jackson">Jackson</a>、<a href="https://github.com/google/gson">Gson</a>、<a href="https://github.com/alibaba/fastjson">FastJson</a>。如果开发者对序列化框架没有特别的要求的情况下,个人建议是直接使用Spring Boot框架默认集成的Jackson,<strong>没有必要</strong>进行更换。</p>
</blockquote>
<h2 id="2统一序列化时间格式">2.统一序列化时间格式</h2>
<p>在我们的接口中,针对时间类型的字段序列化是最常见的需求之一,一般前后端开发人员会针对时间字段统一进行约束,这样有助于在编码开发时,统一编码规范。</p>
<p>在Spring Boot框架中,如果使用Jackson处理框架,并且没有任何配置的情况下,Jackson针对不同时间类型字段,序列化的格式也会不尽相同。</p>
<p>先来看一个简单示例,<code class="highlighter-rouge">User.java</code>实体类编码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">User</span> <span class="o">{</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">Integer</span> <span class="n">age</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">LocalDateTime</span> <span class="n">birthday</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">Date</span> <span class="n">studyDate</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">LocalDate</span> <span class="n">workDate</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">Calendar</span> <span class="n">firstWorkDate</span><span class="o">;</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="n">User</span> <span class="nf">buildOne</span><span class="o">(){</span>
<span class="n">User</span> <span class="n">user</span><span class="o">=</span><span class="k">new</span> <span class="n">User</span><span class="o">();</span>
<span class="n">LocalDateTime</span> <span class="n">now</span><span class="o">=</span><span class="n">LocalDateTime</span><span class="o">.</span><span class="na">now</span><span class="o">();</span>
<span class="n">user</span><span class="o">.</span><span class="na">setWorkDate</span><span class="o">(</span><span class="n">now</span><span class="o">.</span><span class="na">plusYears</span><span class="o">(</span><span class="mi">25</span><span class="o">).</span><span class="na">toLocalDate</span><span class="o">());</span>
<span class="n">user</span><span class="o">.</span><span class="na">setStudyDate</span><span class="o">(</span><span class="n">Date</span><span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">now</span><span class="o">.</span><span class="na">plusYears</span><span class="o">(</span><span class="mi">5</span><span class="o">).</span><span class="na">atZone</span><span class="o">(</span><span class="n">ZoneId</span><span class="o">.</span><span class="na">systemDefault</span><span class="o">()).</span><span class="na">toInstant</span><span class="o">()));</span>
<span class="n">user</span><span class="o">.</span><span class="na">setName</span><span class="o">(</span><span class="s">"姓名-"</span><span class="o">+</span><span class="n">RandomUtil</span><span class="o">.</span><span class="na">randomString</span><span class="o">(</span><span class="mi">5</span><span class="o">));</span>
<span class="n">user</span><span class="o">.</span><span class="na">setAge</span><span class="o">(</span><span class="n">RandomUtil</span><span class="o">.</span><span class="na">randomInt</span><span class="o">(</span><span class="mi">0</span><span class="o">,</span><span class="mi">100</span><span class="o">));</span>
<span class="n">user</span><span class="o">.</span><span class="na">setBirthday</span><span class="o">(</span><span class="n">now</span><span class="o">);</span>
<span class="n">user</span><span class="o">.</span><span class="na">setFirstWorkDate</span><span class="o">(</span><span class="n">Calendar</span><span class="o">.</span><span class="na">getInstance</span><span class="o">());</span>
<span class="k">return</span> <span class="n">user</span><span class="o">;</span>
<span class="o">}</span>
<span class="c1">//getter and setter...</span>
<span class="o">}</span>
</code></pre></div></div>
<p>接口代码层也很简单,返回一个User的实体对象即可,代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@RestController</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserApplication</span> <span class="o">{</span>
<span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/queryOne"</span><span class="o">)</span>
<span class="kd">public</span> <span class="n">ResponseEntity</span><span class="o"><</span><span class="n">User</span><span class="o">></span> <span class="nf">queryOne</span><span class="o">(){</span>
<span class="k">return</span> <span class="n">ResponseEntity</span><span class="o">.</span><span class="na">ok</span><span class="o">(</span><span class="n">User</span><span class="o">.</span><span class="na">buildOne</span><span class="o">());</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>如果我们对框架代码没有任何的配置,此时我们通过调用接口<code class="highlighter-rouge">/queryOne</code>,拿到的返回结果数据如下图:</p>
<p><img src="/assets/images/springboot/code-action-jackson/image-20210312085839202.png" alt="image-20210312085839202" /></p>
<p>Jackson序列化框架针对四个不同的时间类型字段,序列化处理的操作是不同的,如果我们对时间字段有格式化的要求时,我们应该如何处理呢?</p>
<h3 id="21-通过jsonformat注解">2.1 通过<code class="highlighter-rouge">@JsonFormat</code>注解</h3>
<p>最直接也是最简单的一种方式,是我们通过使用Jackson提供的<code class="highlighter-rouge">@JsonFormat</code>注解,对需要格式化处理的时间字段进行标注,在<code class="highlighter-rouge">@JsonFormat</code>注解中写上我们的时间格式化字符,<code class="highlighter-rouge">User.java</code>代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">User</span> <span class="o">{</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">Integer</span> <span class="n">age</span><span class="o">;</span>
<span class="nd">@JsonFormat</span><span class="o">(</span><span class="n">pattern</span> <span class="o">=</span> <span class="s">"yyyy-MM-dd HH:mm:ss"</span><span class="o">)</span>
<span class="kd">private</span> <span class="n">LocalDateTime</span> <span class="n">birthday</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">Date</span> <span class="n">studyDate</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">LocalDate</span> <span class="n">workDate</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">Calendar</span> <span class="n">firstWorkDate</span><span class="o">;</span>
<span class="c1">//getter and setter...</span>
<span class="o">}</span>
</code></pre></div></div>
<p>此时,我们再通过调用接口,拿到的返回结果如下图:</p>
<p><img src="/assets/images/springboot/code-action-jackson/image-20210312090417967.png" alt="image-20210312090417967" /></p>
<p>通过对<code class="highlighter-rouge">birthday</code>字段标注<code class="highlighter-rouge">@JsonFormat</code>注解,最终Jackson框架会将该字段序列化为我们标注的格式类型。</p>
<h3 id="22-配置全局applicationyml">2.2 配置全局<code class="highlighter-rouge">application.yml</code></h3>
<p>通过<code class="highlighter-rouge">@JsonFormat</code>注解的方式虽然能解决问题,但是我们在实际的开发当中,涉及到的时间字段会非常多,如果全部都用注解的方式对项目中的时间字段进行标注,那开发的工作量也会很大,并且多团队一起协同编码时,难免会存在遗漏的情况,因此,<code class="highlighter-rouge">@JsonFormat</code>注解只适用于针对特定的接口,特定的场景下,对序列化响应的时间字段进行约束,而在全局的角度来看,开发者应该考虑通过在<code class="highlighter-rouge">application.yml</code>配置文件中进行全局配置</p>
<p>针对Spring Boot框架中Jackson的全局配置,我们在<code class="highlighter-rouge">application.yml</code>进行配置时,IDEA等编辑器会给出相应的提示,包含的属性如下图:</p>
<p><img src="/assets/images/springboot/code-action-jackson/image-20210312092003557.png" alt="image-20210312092003557" /></p>
<p>开发者可以通过<code class="highlighter-rouge">org.springframework.boot.autoconfigure.jackson.JacksonProperties.java</code>查看所有配置的源码信息</p>
<table>
<thead>
<tr>
<th>配置属性</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="highlighter-rouge">date-format</code></td>
<td>日期字段格式化,例如:<code class="highlighter-rouge">yyyy-MM-dd HH:mm:ss</code></td>
</tr>
</tbody>
</table>
<p>针对日期字段的格式化处理,我们只需要使用<code class="highlighter-rouge">date-format</code>属性进行配置即可,<code class="highlighter-rouge">application.yml</code>配置如下:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">spring</span><span class="pi">:</span>
<span class="na">jackson</span><span class="pi">:</span>
<span class="na">date-format</span><span class="pi">:</span> <span class="s">yyyy-MM-dd HH:mm:ss</span>
</code></pre></div></div>
<p>当然,如果有必要的话,还需要配置<code class="highlighter-rouge">time-zone</code>时区属性,不过该属性不配置的情况下,Jackson会使用系统默认时区。</p>
<p>我们从Spring Boot的源码中可以看到对Jackson的时间处理逻辑,<code class="highlighter-rouge">JacksonAutoConfiguration.java</code>中部分代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">void</span> <span class="nf">configureDateFormat</span><span class="o">(</span><span class="n">Jackson2ObjectMapperBuilder</span> <span class="n">builder</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// We support a fully qualified class name extending DateFormat or a date</span>
<span class="c1">// pattern string value</span>
<span class="n">String</span> <span class="n">dateFormat</span> <span class="o">=</span> <span class="k">this</span><span class="o">.</span><span class="na">jacksonProperties</span><span class="o">.</span><span class="na">getDateFormat</span><span class="o">();</span>
<span class="k">if</span> <span class="o">(</span><span class="n">dateFormat</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="k">try</span> <span class="o">{</span>
<span class="n">Class</span><span class="o"><?></span> <span class="n">dateFormatClass</span> <span class="o">=</span> <span class="n">ClassUtils</span><span class="o">.</span><span class="na">forName</span><span class="o">(</span><span class="n">dateFormat</span><span class="o">,</span> <span class="kc">null</span><span class="o">);</span>
<span class="n">builder</span><span class="o">.</span><span class="na">dateFormat</span><span class="o">((</span><span class="n">DateFormat</span><span class="o">)</span> <span class="n">BeanUtils</span><span class="o">.</span><span class="na">instantiateClass</span><span class="o">(</span><span class="n">dateFormatClass</span><span class="o">));</span>
<span class="o">}</span>
<span class="k">catch</span> <span class="o">(</span><span class="n">ClassNotFoundException</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
<span class="n">SimpleDateFormat</span> <span class="n">simpleDateFormat</span> <span class="o">=</span> <span class="k">new</span> <span class="n">SimpleDateFormat</span><span class="o">(</span><span class="n">dateFormat</span><span class="o">);</span>
<span class="c1">// Since Jackson 2.6.3 we always need to set a TimeZone (see</span>
<span class="c1">// gh-4170). If none in our properties fallback to the Jackson's</span>
<span class="c1">// default</span>
<span class="n">TimeZone</span> <span class="n">timeZone</span> <span class="o">=</span> <span class="k">this</span><span class="o">.</span><span class="na">jacksonProperties</span><span class="o">.</span><span class="na">getTimeZone</span><span class="o">();</span>
<span class="k">if</span> <span class="o">(</span><span class="n">timeZone</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="n">timeZone</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ObjectMapper</span><span class="o">().</span><span class="na">getSerializationConfig</span><span class="o">().</span><span class="na">getTimeZone</span><span class="o">();</span>
<span class="o">}</span>
<span class="n">simpleDateFormat</span><span class="o">.</span><span class="na">setTimeZone</span><span class="o">(</span><span class="n">timeZone</span><span class="o">);</span>
<span class="n">builder</span><span class="o">.</span><span class="na">dateFormat</span><span class="o">(</span><span class="n">simpleDateFormat</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>从上面的代码中,我们可以看到的处理逻辑:</p>
<ul>
<li>从yml配置文件中拿到<code class="highlighter-rouge">dateFormat</code>属性字段</li>
<li>首先通过<code class="highlighter-rouge">ClassUtils.forName</code>方法来判断开发者配置的是否是格式化类,如果配置的是格式化类,则直接配置<code class="highlighter-rouge">dateFormat</code>属性</li>
<li>类找不到的情况下,捕获<code class="highlighter-rouge">ClassNotFoundException</code>异常,默认使用JDK自带的<code class="highlighter-rouge">SimpleDateFormat</code>类进行初始化</li>
</ul>
<p>最终,我们在<code class="highlighter-rouge">application.yml</code>配置文件中配置了全局的Jackson针对日期处理的格式化信息,此时我们再看<code class="highlighter-rouge">/queryOne</code>接口响应的内容是什么情况呢?如下图:</p>
<p><img src="/assets/images/springboot/code-action-jackson/image-20210312094014588.png" alt="image-20210312094014588" /></p>
<p>从图中我们可以发现,除了<code class="highlighter-rouge">LocalDate</code>类型的字段,包含时分秒类型的日期类型:<code class="highlighter-rouge">LocalDateTime</code>、<code class="highlighter-rouge">Date</code>、<code class="highlighter-rouge">Calendar</code>全部按照我们的要求将日期序列化成了<code class="highlighter-rouge">yyyy-MM-dd HH:mm:ss</code>格式,达到了我们的要求。</p>
<h2 id="3jackson在spring-boot框架中的配置选项">3.Jackson在Spring Boot框架中的配置选项</h2>
<p>在上面的时间字段序列化处理,我们已经知道了如何配置,那么在Spring Boot的框架中,针对Jackson的各个配置项主要包含哪些呢?我们通过IDEA的提示可以看到,配置如下图:</p>
<p><img src="/assets/images/springboot/code-action-jackson/image-20210312092003557.png" alt="image-20210312092003557" /></p>
<p>在上面的12个属性中,每个属性的配置都会对Jackson产生不同的效果,接下来,我们逐一详解每个属性配置的作用</p>
<h3 id="31-date-format日期格式化">3.1 date-format日期格式化</h3>
<p><code class="highlighter-rouge">date-format</code>在前面我们已经知道了该属性的作用,主要是针对日期字段的格式化</p>
<h3 id="32-time-zone时区">3.2 time-zone时区</h3>
<p><code class="highlighter-rouge">time-zone</code>字段也是和日期字段类型,使用不同的时区,最终日期类型字段响应的结果会不一样</p>
<p>时区的表示方法有两种:</p>
<ul>
<li>指定时区的名称,例如:<code class="highlighter-rouge">Asia/Shanghai</code>,<code class="highlighter-rouge">America/Los_Angeles</code></li>
<li>通过格林威治平时<code class="highlighter-rouge">GMT</code>针对时分秒做<code class="highlighter-rouge">+</code>或者<code class="highlighter-rouge">-</code>自定义操作</li>
</ul>
<p>通过指定时区的名称,假设我们指定当前的项目是<code class="highlighter-rouge">America/Los_Angeles</code>,那么接口响应的数据是什么效果呢?</p>
<blockquote>
<p>PS:时区名称如果不是很清楚的话,一般在Linux服务器的<code class="highlighter-rouge">/usr/share/zoneinfo</code>目录可以进行查看,如下图:</p>
<p><img src="/assets/images/springboot/code-action-jackson/image-20210312131802521.png" alt="image-20210312131802521" /></p>
</blockquote>
<p><code class="highlighter-rouge">application.yml</code>:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">spring</span><span class="pi">:</span>
<span class="na">jackson</span><span class="pi">:</span>
<span class="na">date-format</span><span class="pi">:</span> <span class="s">yyyy-MM-dd HH:mm:ss</span>
<span class="na">time-zone</span><span class="pi">:</span> <span class="s">America/Los_Angeles</span>
</code></pre></div></div>
<p>效果图如下:</p>
<p><img src="/assets/images/springboot/code-action-jackson/image-20210312130547087.png" alt="image-20210312130547087" /></p>
<p>我们在结合代码来分析:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//User.java</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="n">User</span> <span class="nf">buildOne</span><span class="o">(){</span>
<span class="n">User</span> <span class="n">user</span><span class="o">=</span><span class="k">new</span> <span class="n">User</span><span class="o">();</span>
<span class="n">LocalDateTime</span> <span class="n">now</span><span class="o">=</span><span class="n">LocalDateTime</span><span class="o">.</span><span class="na">now</span><span class="o">();</span>
<span class="n">user</span><span class="o">.</span><span class="na">setWorkDate</span><span class="o">(</span><span class="n">now</span><span class="o">.</span><span class="na">plusYears</span><span class="o">(</span><span class="mi">25</span><span class="o">).</span><span class="na">toLocalDate</span><span class="o">());</span>
<span class="n">user</span><span class="o">.</span><span class="na">setStudyDate</span><span class="o">(</span><span class="n">Date</span><span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">now</span><span class="o">.</span><span class="na">plusYears</span><span class="o">(</span><span class="mi">5</span><span class="o">).</span><span class="na">atZone</span><span class="o">(</span><span class="n">ZoneId</span><span class="o">.</span><span class="na">systemDefault</span><span class="o">()).</span><span class="na">toInstant</span><span class="o">()));</span>
<span class="n">user</span><span class="o">.</span><span class="na">setName</span><span class="o">(</span><span class="s">"姓名-"</span><span class="o">+</span><span class="n">RandomUtil</span><span class="o">.</span><span class="na">randomString</span><span class="o">(</span><span class="mi">5</span><span class="o">));</span>
<span class="n">user</span><span class="o">.</span><span class="na">setAge</span><span class="o">(</span><span class="n">RandomUtil</span><span class="o">.</span><span class="na">randomInt</span><span class="o">(</span><span class="mi">0</span><span class="o">,</span><span class="mi">100</span><span class="o">));</span>
<span class="n">user</span><span class="o">.</span><span class="na">setBirthday</span><span class="o">(</span><span class="n">now</span><span class="o">);</span>
<span class="n">user</span><span class="o">.</span><span class="na">setFirstWorkDate</span><span class="o">(</span><span class="n">Calendar</span><span class="o">.</span><span class="na">getInstance</span><span class="o">());</span>
<span class="k">return</span> <span class="n">user</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>由于洛杉矶时区与上海时区相差16个小时,因此,Jackson框架针对日期的序列化时,分别做了不同类型的处理,但我们也能看出差别</p>
<ul>
<li><code class="highlighter-rouge">LocalDateTime</code>、<code class="highlighter-rouge">LocalDate</code>类型的字段,Jackson的时区设置不会对该字段产生影响(因为这两个日期类型自带时区属性)</li>
<li><code class="highlighter-rouge">Date</code>、<code class="highlighter-rouge">Calendar</code>类型的字段受Jackson序列化框架的时区设置影响</li>
</ul>
<p>另外一种方式是通过格林威治平时(GMT)做加减法,主要有两种格式支持:</p>
<ul>
<li><code class="highlighter-rouge">GMT+HHMM</code>或者<code class="highlighter-rouge">GMT-HHMM</code>或者<code class="highlighter-rouge">GMT+H</code>:其中HH代表的是小时数,MM代表的是分钟数,取值范围是0-9,例如我们常见的GMT+8代表东八区,也就是北京时间</li>
<li><code class="highlighter-rouge">GMT+HH:MM</code>或者<code class="highlighter-rouge">GMT-HH:MM</code>:其中HH代表的是小时数,MM代表的是分钟数,取值范围是0-9,和上面意思差不多</li>
</ul>
<p>可以自己写测试代码进行测试,示例如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">TimeTest</span> <span class="o">{</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="n">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
<span class="n">LocalDateTime</span> <span class="n">localDateTime</span><span class="o">=</span><span class="n">LocalDateTime</span><span class="o">.</span><span class="na">now</span><span class="o">();</span>
<span class="n">DateTimeFormatter</span> <span class="n">dateTimeFormatter</span><span class="o">=</span><span class="n">DateTimeFormatter</span><span class="o">.</span><span class="na">ofPattern</span><span class="o">(</span><span class="s">"yyyy-MM-dd HH:mm:ss"</span><span class="o">);</span>
<span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">localDateTime</span><span class="o">.</span><span class="na">format</span><span class="o">(</span><span class="n">dateTimeFormatter</span><span class="o">));</span>
<span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">LocalDateTime</span><span class="o">.</span><span class="na">now</span><span class="o">(</span><span class="n">ZoneId</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"GMT+0901"</span><span class="o">)).</span><span class="na">format</span><span class="o">(</span><span class="n">dateTimeFormatter</span><span class="o">));</span>
<span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">LocalDateTime</span><span class="o">.</span><span class="na">now</span><span class="o">(</span><span class="n">ZoneId</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"GMT+09:01"</span><span class="o">)).</span><span class="na">format</span><span class="o">(</span><span class="n">dateTimeFormatter</span><span class="o">));</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<h3 id="33-locale本地化">3.3 locale本地化</h3>
<p>JSON序列化时Locale的变量设置</p>
<h3 id="34-visibility访问级别">3.4 visibility访问级别</h3>
<p>Jackson支持从私有字段中读取值,但是默认情况下不这样做,如果我们的项目中存在不同的序列化反序列化需求,那么我们可以在配置文件中对<code class="highlighter-rouge">visibility</code>进行配置</p>
<p>我们将上面<code class="highlighter-rouge">User.java</code>代码中的name属性的get方法修饰符从<code class="highlighter-rouge">public</code>变更为<code class="highlighter-rouge">private</code>,其他字段保持不变</p>
<p>代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">User</span> <span class="o">{</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">Integer</span> <span class="n">age</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">Date</span> <span class="n">nowDate</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">LocalDateTime</span> <span class="n">birthday</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">Date</span> <span class="n">studyDate</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">LocalDate</span> <span class="n">workDate</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">Calendar</span> <span class="n">firstWorkDate</span><span class="o">;</span>
<span class="c1">//getter方法修饰符从public修改为private</span>
<span class="kd">private</span> <span class="n">String</span> <span class="nf">getName</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">name</span><span class="o">;</span>
<span class="o">}</span>
<span class="c1">//other setter and getter</span>
<span class="o">}</span>
</code></pre></div></div>
<p>此时,我们通过调用<code class="highlighter-rouge">/queryOne</code>接口响应结果如下:</p>
<p><img src="/assets/images/springboot/code-action-jackson/image-20210314191124147.png" alt="image-20210314191124147" /></p>
<p>从结果中我们可以看到,由于我们将name属性的<code class="highlighter-rouge">getter</code>方法设置为了<code class="highlighter-rouge">private</code>,因此jackson在序列化时,没有拿到该字段</p>
<p>此时,我们再修改<code class="highlighter-rouge">application.yml</code>的配置,如下:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">spring</span><span class="pi">:</span>
<span class="na">jackson</span><span class="pi">:</span>
<span class="na">visibility</span><span class="pi">:</span>
<span class="na">getter</span><span class="pi">:</span> <span class="s">any</span>
</code></pre></div></div>
<p>我们通过将<code class="highlighter-rouge">getter</code>设置为<code class="highlighter-rouge">any</code>级别的类型,再调用<code class="highlighter-rouge">/queryOne</code>接口,响应结果如下:</p>
<p><img src="/assets/images/springboot/code-action-jackson/image-20210314191405490.png" alt="image-20210314191405490" /></p>
<p>从图中可以看出,jackson序列化结果中又出现了name属性,这代表即使name字段的属性和<code class="highlighter-rouge">getter</code>方法都是<code class="highlighter-rouge">private</code>,但是jackson还是获取到了该成员变量的值,并且进行了序列化处理。</p>
<p>通过设置<code class="highlighter-rouge">visibility</code>属性即可达到上面的效果。开发者根据自己的需要自行进行选择。</p>
<h3 id="35-property-naming-strategy属性命名策略">3.5 property-naming-strategy属性命名策略</h3>
<p>通常比较常见的我们针对java代码中的实体类属性一般都是<a href="https://baike.baidu.com/item/%E9%AA%86%E9%A9%BC%E5%91%BD%E5%90%8D%E6%B3%95/7794053?fromtitle=%E9%A9%BC%E5%B3%B0%E5%91%BD%E5%90%8D%E6%B3%95&fromid=7560610&fr=aladdin">驼峰命名法(Camel-Case)</a>,但是Jackson序列化框架也提供了更多的序列化策略,而<code class="highlighter-rouge">property-naming-strategy</code>就是配置该属性的。</p>
<p>先来看Spring Boot框架如何配置jackson的命名策略</p>
<p><code class="highlighter-rouge">JacksonAutoConfiguration.java</code></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">void</span> <span class="nf">configurePropertyNamingStrategyField</span><span class="o">(</span><span class="n">Jackson2ObjectMapperBuilder</span> <span class="n">builder</span><span class="o">,</span> <span class="n">String</span> <span class="n">fieldName</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// Find the field (this way we automatically support new constants</span>
<span class="c1">// that may be added by Jackson in the future)</span>
<span class="n">Field</span> <span class="n">field</span> <span class="o">=</span> <span class="n">ReflectionUtils</span><span class="o">.</span><span class="na">findField</span><span class="o">(</span><span class="n">PropertyNamingStrategy</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="n">fieldName</span><span class="o">,</span>
<span class="n">PropertyNamingStrategy</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
<span class="n">Assert</span><span class="o">.</span><span class="na">notNull</span><span class="o">(</span><span class="n">field</span><span class="o">,</span> <span class="o">()</span> <span class="o">-></span> <span class="s">"Constant named '"</span> <span class="o">+</span> <span class="n">fieldName</span> <span class="o">+</span> <span class="s">"' not found on "</span>
<span class="o">+</span> <span class="n">PropertyNamingStrategy</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getName</span><span class="o">());</span>
<span class="k">try</span> <span class="o">{</span>
<span class="n">builder</span><span class="o">.</span><span class="na">propertyNamingStrategy</span><span class="o">((</span><span class="n">PropertyNamingStrategy</span><span class="o">)</span> <span class="n">field</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="kc">null</span><span class="o">));</span>
<span class="o">}</span>
<span class="k">catch</span> <span class="o">(</span><span class="n">Exception</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalStateException</span><span class="o">(</span><span class="n">ex</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>通过反射,直接获取<code class="highlighter-rouge">PropertyNamingStrategy</code>类中的成员变量的值</p>
<p><code class="highlighter-rouge">PropertyNamingStrategy</code>定义了Jackson(<code class="highlighter-rouge">2.11.4</code>)框架中的命名策略常量成员变量</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="n">com</span><span class="o">.</span><span class="na">fasterxml</span><span class="o">.</span><span class="na">jackson</span><span class="o">.</span><span class="na">databind</span><span class="o">;</span>
<span class="c1">//other import</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">PropertyNamingStrategy</span> <span class="c1">// NOTE: was abstract until 2.7</span>
<span class="kd">implements</span> <span class="n">java</span><span class="o">.</span><span class="na">io</span><span class="o">.</span><span class="na">Serializable</span>
<span class="o">{</span>
<span class="cm">/**
* Naming convention used in languages like C, where words are in lower-case
* letters, separated by underscores.
* See {@link SnakeCaseStrategy} for details.
*
* @since 2.7 (was formerly called {@link #CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES})
*/</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="n">PropertyNamingStrategy</span> <span class="n">SNAKE_CASE</span> <span class="o">=</span> <span class="k">new</span> <span class="n">SnakeCaseStrategy</span><span class="o">();</span>
<span class="cm">/**
* Naming convention used in languages like Pascal, where words are capitalized
* and no separator is used between words.
* See {@link PascalCaseStrategy} for details.
*
* @since 2.7 (was formerly called {@link #PASCAL_CASE_TO_CAMEL_CASE})
*/</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="n">PropertyNamingStrategy</span> <span class="n">UPPER_CAMEL_CASE</span> <span class="o">=</span> <span class="k">new</span> <span class="n">UpperCamelCaseStrategy</span><span class="o">();</span>
<span class="cm">/**
* Naming convention used in Java, where words other than first are capitalized
* and no separator is used between words. Since this is the native Java naming convention,
* naming strategy will not do any transformation between names in data (JSON) and
* POJOS.
*
* @since 2.7 (was formerly called {@link #PASCAL_CASE_TO_CAMEL_CASE})
*/</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="n">PropertyNamingStrategy</span> <span class="n">LOWER_CAMEL_CASE</span> <span class="o">=</span> <span class="k">new</span> <span class="n">PropertyNamingStrategy</span><span class="o">();</span>
<span class="cm">/**
* Naming convention in which all words of the logical name are in lower case, and
* no separator is used between words.
* See {@link LowerCaseStrategy} for details.
*
* @since 2.4
*/</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="n">PropertyNamingStrategy</span> <span class="n">LOWER_CASE</span> <span class="o">=</span> <span class="k">new</span> <span class="n">LowerCaseStrategy</span><span class="o">();</span>
<span class="cm">/**
* Naming convention used in languages like Lisp, where words are in lower-case
* letters, separated by hyphens.
* See {@link KebabCaseStrategy} for details.
*
* @since 2.7
*/</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="n">PropertyNamingStrategy</span> <span class="n">KEBAB_CASE</span> <span class="o">=</span> <span class="k">new</span> <span class="n">KebabCaseStrategy</span><span class="o">();</span>
<span class="cm">/**
* Naming convention widely used as configuration properties name, where words are in
* lower-case letters, separated by dots.
* See {@link LowerDotCaseStrategy} for details.
*
* @since 2.10
*/</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="n">PropertyNamingStrategy</span> <span class="n">LOWER_DOT_CASE</span> <span class="o">=</span> <span class="k">new</span> <span class="n">LowerDotCaseStrategy</span><span class="o">();</span>
<span class="c1">//others...</span>
<span class="o">}</span>
</code></pre></div></div>
<p>从源码中我们可以看到,有六种策略供我们进行配置,配置示例如下:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">spring</span><span class="pi">:</span>
<span class="na">jackson</span><span class="pi">:</span>
<span class="na">date-format</span><span class="pi">:</span> <span class="s">yyyy-MM-dd HH:mm:ss</span>
<span class="na">locale</span><span class="pi">:</span> <span class="s">zh_CN</span>
<span class="na">time-zone</span><span class="pi">:</span> <span class="s">GMT+8</span>
<span class="na">visibility</span><span class="pi">:</span>
<span class="na">getter</span><span class="pi">:</span> <span class="s">any</span>
<span class="na">property-naming-strategy</span><span class="pi">:</span> <span class="s">LOWER_CAMEL_CASE</span>
</code></pre></div></div>
<p><strong>SNAKE_CASE</strong></p>
<p><code class="highlighter-rouge">SNAKE_CASE</code>主要包含的规则,详见<a href="https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/latest/com/fasterxml/jackson/databind/PropertyNamingStrategy.SnakeCaseStrategy.html">SnakeCaseStrategy</a>:</p>
<ul>
<li>java属性名称中所有大写的字符都会转换为两个字符,下划线和该字符的小写形式,例如<code class="highlighter-rouge">userName</code>会转换为<code class="highlighter-rouge">user_name</code>,对于连续性的大写字符,近第一个进行下划线转换,后面的大小字符则是小写,例如<code class="highlighter-rouge">theWWW</code>会转换为<code class="highlighter-rouge">the_www</code></li>
<li>对于首字母大写的情况,近转成小写,例如:<code class="highlighter-rouge">Results</code>会转换为<code class="highlighter-rouge">results</code>,并不会转换为<code class="highlighter-rouge">_results</code></li>
<li>针对属性中已经包含下划线的情况,仅做小写转换处理</li>
<li>下划线出现在首位的情况下,会被去除处理,例如属性名:<code class="highlighter-rouge">_user</code>会被转换为<code class="highlighter-rouge">user</code></li>
</ul>
<p>真实效果如下图:</p>
<p><img src="/assets/images/springboot/code-action-jackson/image-20210315125556270.png" alt="image-20210315125556270" /></p>
<p><strong>UPPER_CAMEL_CASE</strong></p>
<p><code class="highlighter-rouge">UPPER_CAMEL_CASE</code>顾名思义,驼峰命名法的规则,只是首字母会转换为大写,详见<a href="https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/latest/com/fasterxml/jackson/databind/PropertyNamingStrategy.UpperCamelCaseStrategy.html">UpperCamelCaseStrategy</a></p>
<p>真实效果图如下:</p>
<p><img src="/assets/images/springboot/code-action-jackson/image-20210315131430536.png" alt="image-20210315131430536" /></p>
<p><strong>LOWER_CAMEL_CASE</strong></p>
<p><code class="highlighter-rouge">LOWER_CAMEL_CASE</code>效果和<code class="highlighter-rouge">UPPER_CAMEL_CASE</code>正好相反,其首字母会变成小写,详见<a href="https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/latest/com/fasterxml/jackson/databind/PropertyNamingStrategies.LowerCamelCaseStrategy.html">LowerCamelCaseStrategy</a></p>
<p>效果图如下:</p>
<p><img src="/assets/images/springboot/code-action-jackson/image-20210315131729151.png" alt="image-20210315131729151" /></p>
<p><strong>LOWER_CASE</strong></p>
<p><code class="highlighter-rouge">LOWER_CASE</code>从命名来看很明显,将属性名 全部转为小写,详见<a href="https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/latest/com/fasterxml/jackson/databind/PropertyNamingStrategy.LowerCaseStrategy.html">LowerCaseStrategy</a></p>
<p><strong>KEBAB_CASE</strong></p>
<p><code class="highlighter-rouge">KEBAB_CASE</code>策略和<code class="highlighter-rouge">SNAKE_CASE</code>规则类似,只是下划线变成了横线<code class="highlighter-rouge">-</code>,详见<a href="https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/latest/com/fasterxml/jackson/databind/PropertyNamingStrategy.KebabCaseStrategy.html">KebabCaseStrategy</a></p>
<p>效果图如下:</p>
<p><img src="/assets/images/springboot/code-action-jackson/image-20210315132557384.png" alt="image-20210315132557384" /></p>
<p><strong>LOWER_DOT_CASE</strong></p>
<p><code class="highlighter-rouge">LOWER_DOT_CASE</code>策略和<code class="highlighter-rouge">KEBAB_CASE</code>规则相似,只是由横线变成了点<code class="highlighter-rouge">.</code>,详见<a href="https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/latest/com/fasterxml/jackson/databind/PropertyNamingStrategy.LowerDotCaseStrategy.html">LowerDotCaseStrategy</a></p>
<p>效果图如下:</p>
<p><img src="/assets/images/springboot/code-action-jackson/image-20210315132720262.png" alt="image-20210315132720262" /></p>
<p><strong>总结</strong>:看了上面这么多属性名称的策略,其实每一种类型只是不同的场景下才需要,如果上面jackson给定的默认策略名称无法满足,我们从源码中也能看到,通过自定义实现类,也能满足企业的个性化需求,非常方便。</p>
<h3 id="36-mapper通用功能开关配置">3.6 mapper通用功能开关配置</h3>
<p><code class="highlighter-rouge">mapper</code>属性是一个Map类型,主要是针对<code class="highlighter-rouge">MapperFeature</code>定义开关属性,是否启用这些特性</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
* Jackson general purpose on/off features.
*/</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="n">Map</span><span class="o"><</span><span class="n">MapperFeature</span><span class="o">,</span> <span class="n">Boolean</span><span class="o">></span> <span class="n">mapper</span> <span class="o">=</span> <span class="k">new</span> <span class="n">EnumMap</span><span class="o"><>(</span><span class="n">MapperFeature</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
</code></pre></div></div>
<p>在<code class="highlighter-rouge">MapperFeature.java</code>中,我们可以跟踪源码来看:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
* Enumeration that defines simple on/off features to set
* for {@link ObjectMapper}, and accessible (but not changeable)
* via {@link ObjectReader} and {@link ObjectWriter} (as well as
* through various convenience methods through context objects).
*<p>
* Note that in addition to being only mutable via {@link ObjectMapper},
* changes only take effect when done <b>before any serialization or
* deserialization</b> calls -- that is, caller must follow
* "configure-then-use" pattern.
*/</span>
<span class="kd">public</span> <span class="kd">enum</span> <span class="n">MapperFeature</span> <span class="kd">implements</span> <span class="n">ConfigFeature</span>
<span class="o">{</span>
<span class="c1">//.......</span>
<span class="o">}</span>
</code></pre></div></div>
<p><code class="highlighter-rouge">MapperFeature</code>是一个枚举类型,对当前jackson的一些特性通过枚举变量的方式来定义开关属性,也是方便使用者来使用的。</p>
<p>主要包含以下枚举变量:</p>
<ul>
<li><code class="highlighter-rouge">USE_ANNOTATIONS</code>:</li>
<li><code class="highlighter-rouge">USE_GETTERS_AS_SETTERS</code></li>
<li><code class="highlighter-rouge">PROPAGATE_TRANSIENT_MARKER</code></li>
<li><code class="highlighter-rouge">AUTO_DETECT_CREATORS</code></li>
<li><code class="highlighter-rouge">AUTO_DETECT_FIELDS</code></li>
<li><code class="highlighter-rouge">AUTO_DETECT_GETTERS</code></li>
<li><code class="highlighter-rouge">AUTO_DETECT_IS_GETTERS</code></li>
<li><code class="highlighter-rouge">AUTO_DETECT_SETTERS</code></li>
<li><code class="highlighter-rouge">REQUIRE_SETTERS_FOR_GETTERS</code></li>
<li><code class="highlighter-rouge">ALLOW_FINAL_FIELDS_AS_MUTATORS</code></li>
<li><code class="highlighter-rouge">INFER_PROPERTY_MUTATORS</code></li>
<li><code class="highlighter-rouge">INFER_CREATOR_FROM_CONSTRUCTOR_PROPERTIES</code></li>
<li><code class="highlighter-rouge">CAN_OVERRIDE_ACCESS_MODIFIERS</code></li>
<li><code class="highlighter-rouge">OVERRIDE_PUBLIC_ACCESS_MODIFIERS</code></li>
<li><code class="highlighter-rouge">USE_STATIC_TYPING</code></li>
<li><code class="highlighter-rouge">USE_BASE_TYPE_AS_DEFAULT_IMPL</code></li>
<li><code class="highlighter-rouge">DEFAULT_VIEW_INCLUSION</code></li>
<li><code class="highlighter-rouge">SORT_PROPERTIES_ALPHABETICALLY</code></li>
<li><code class="highlighter-rouge">ACCEPT_CASE_INSENSITIVE_PROPERTIES</code></li>
<li><code class="highlighter-rouge">ACCEPT_CASE_INSENSITIVE_ENUMS</code></li>
<li><code class="highlighter-rouge">ACCEPT_CASE_INSENSITIVE_VALUES</code></li>
<li><code class="highlighter-rouge">USE_WRAPPER_NAME_AS_PROPERTY_NAME</code></li>
<li><code class="highlighter-rouge">USE_STD_BEAN_NAMING</code></li>
<li><code class="highlighter-rouge">ALLOW_EXPLICIT_PROPERTY_RENAMING</code></li>
<li><code class="highlighter-rouge">ALLOW_COERCION_OF_SCALARS</code></li>
<li><code class="highlighter-rouge">IGNORE_DUPLICATE_MODULE_REGISTRATIONS</code></li>
<li><code class="highlighter-rouge">IGNORE_MERGE_FOR_UNMERGEABLE</code></li>
<li><code class="highlighter-rouge">BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES</code></li>
</ul>
<h3 id="37-serialization序列化特性开关配置">3.7 serialization序列化特性开关配置</h3>
<p><code class="highlighter-rouge">serialization</code>属性同<code class="highlighter-rouge">mapper</code>类似,也是一个Map类型的属性</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
* Jackson on/off features that affect the way Java objects are serialized.
*/</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="n">Map</span><span class="o"><</span><span class="n">SerializationFeature</span><span class="o">,</span> <span class="n">Boolean</span><span class="o">></span> <span class="n">serialization</span> <span class="o">=</span> <span class="k">new</span> <span class="n">EnumMap</span><span class="o"><>(</span><span class="n">SerializationFeature</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
</code></pre></div></div>
<h3 id="38-deserialization反序列化开关配置">3.8 deserialization反序列化开关配置</h3>
<p><code class="highlighter-rouge">deserialization</code>反序列化配置</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
* Jackson on/off features that affect the way Java objects are deserialized.
*/</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="n">Map</span><span class="o"><</span><span class="n">DeserializationFeature</span><span class="o">,</span> <span class="n">Boolean</span><span class="o">></span> <span class="n">deserialization</span> <span class="o">=</span> <span class="k">new</span> <span class="n">EnumMap</span><span class="o"><>(</span><span class="n">DeserializationFeature</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
</code></pre></div></div>
<h3 id="39-parser配置">3.9 parser配置</h3>
<h3 id="310-generator配置">3.10 generator配置</h3>
<h3 id="311-defaultpropertyinclusion序列化包含的属性配置">3.11 defaultPropertyInclusion序列化包含的属性配置</h3>
<p>该属性是一个枚举配置,主要包含:</p>
<ul>
<li><code class="highlighter-rouge">ALWAYS</code>:顾名思义,始终包含,和属性的值无关</li>
<li><code class="highlighter-rouge">NON_NULL</code>:值非空的属性才会包含属性</li>
<li><code class="highlighter-rouge">NON_ABSENT</code>:值非空的属性,或者<code class="highlighter-rouge">Optional</code>类型的属性非空</li>
<li><code class="highlighter-rouge">NON_EMPTY</code>: 空值的属性不包含</li>
<li><code class="highlighter-rouge">NON_DEFAULT</code>:不使用jackson的默认规则对该字段进行序列化,详见<a href="https://www.logicbig.com/tutorials/misc/jackson/json-include-non-default.html">示例</a></li>
<li><code class="highlighter-rouge">CUSTOM</code>:自定义规则</li>
<li><code class="highlighter-rouge">USE_DEFAULTS</code>:配置使用该规则的属性字段,将会优先使用class上的注解规则,否则会使用全局的序列化规则,详见<a href="https://www.logicbig.com/tutorials/misc/jackson/json-include-use-defaults.html">示例</a></li>
</ul>
<p><code class="highlighter-rouge">CUSTOM</code>自定义规则是需要开发者在属性字段上使用<code class="highlighter-rouge">@JsonInclude</code>注解,并且指定<code class="highlighter-rouge">valueFilter</code>属性,该属性需要传递一个<code class="highlighter-rouge">Class</code>,示例如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//User.java</span>
<span class="c1">//指定value级别是CUSTOM</span>
<span class="nd">@JsonInclude</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="n">JsonInclude</span><span class="o">.</span><span class="na">Include</span><span class="o">.</span><span class="na">CUSTOM</span><span class="o">,</span> <span class="n">valueFilter</span> <span class="o">=</span> <span class="n">StringFilter</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
</code></pre></div></div>
<p><code class="highlighter-rouge">StringFilter</code>则是判断非空的依据,该依据由开发者自己定义,返回<code class="highlighter-rouge">true</code>将会被排除,<code class="highlighter-rouge">false</code>则不会排除,示例如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//自定义非空判断规则</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">StringFilter</span> <span class="o">{</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">equals</span><span class="o">(</span><span class="n">Object</span> <span class="n">other</span><span class="o">)</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">other</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// Filter null's.</span>
<span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
<span class="o">}</span>
<span class="c1">// Filter "custom_string".</span>
<span class="k">return</span> <span class="s">"custom_string"</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">other</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<h2 id="4spring-boot针对jackson的约定配置做的事情">4.Spring Boot针对Jackson的约定配置做的事情</h2>
<p>在前面的文章中,我们已经详细的了解了Jackson在Spring Boot框架中的各个配置项,那么Spring Boot针对Jackson框架在约定配置时会做哪些事情呢?</p>
<p>在Spring Boot的<code class="highlighter-rouge">spring-boot-autoconfigure-x.x.jar</code>包中,我们可以看到Spring Boot框架针对jackson的处理源码,如下图:</p>
<p><img src="/assets/images/springboot/code-action-jackson/image-20210320122535467.png" alt="image-20210320122535467" /></p>
<p>主要包含三个类:</p>
<ul>
<li>JacksonProperties:Spring Boot框架提供jackson的配置属性类,即开发者在<code class="highlighter-rouge">application.yml</code>配置文件中的配置项属性</li>
<li>JacksonAutoConfiguration:Jackson的默认注入配置类</li>
<li>Jackson2ObjectMapperBuilderCustomizer:自定义用于注入jackson的配置辅助接口</li>
</ul>
<p>核心类是<code class="highlighter-rouge">JacksonAutoConfiguration.java</code>,该类是Spring Boot框架将Jackson相关实体Bean注入Spring容器的关键配置类。其主要作用:</p>
<ul>
<li>注入Jackson的<code class="highlighter-rouge">ObjectMapper</code>实体Bean到Spring容器中</li>
<li>注入<code class="highlighter-rouge">ParameterNamesModule</code>实体Bean到Spring容器中</li>
<li>注入<code class="highlighter-rouge">Jackson2ObjectMapperBuilder</code>实体Bean</li>
<li>注入<code class="highlighter-rouge">JsonComponentModule</code>实体Bean</li>
<li>注入<code class="highlighter-rouge">StandardJackson2ObjectMapperBuilderCustomizer</code>实体Bean,该类是上面<code class="highlighter-rouge">Jackson2ObjectMapperBuilderCustomizer</code>的实现类,主要用于接收<code class="highlighter-rouge">JacksonProperties</code>属性,将Jackson的外部配置属性接收,然后最终执行<code class="highlighter-rouge">customize</code>方法,构建<code class="highlighter-rouge">ObjectMapper</code>所需要的<code class="highlighter-rouge">Jackson2ObjectMapperBuilder</code>属性,最终为<code class="highlighter-rouge">ObjectMapper</code>属性赋值准备</li>
</ul>
<p>源码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Configuration</span><span class="o">(</span><span class="n">proxyBeanMethods</span> <span class="o">=</span> <span class="kc">false</span><span class="o">)</span>
<span class="nd">@ConditionalOnClass</span><span class="o">(</span><span class="n">ObjectMapper</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">JacksonAutoConfiguration</span> <span class="o">{</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="n">Map</span><span class="o"><?,</span> <span class="n">Boolean</span><span class="o">></span> <span class="n">FEATURE_DEFAULTS</span><span class="o">;</span>
<span class="kd">static</span> <span class="o">{</span>
<span class="n">Map</span><span class="o"><</span><span class="n">Object</span><span class="o">,</span> <span class="n">Boolean</span><span class="o">></span> <span class="n">featureDefaults</span> <span class="o">=</span> <span class="k">new</span> <span class="n">HashMap</span><span class="o"><>();</span>
<span class="n">featureDefaults</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="n">SerializationFeature</span><span class="o">.</span><span class="na">WRITE_DATES_AS_TIMESTAMPS</span><span class="o">,</span> <span class="kc">false</span><span class="o">);</span>
<span class="n">featureDefaults</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="n">SerializationFeature</span><span class="o">.</span><span class="na">WRITE_DURATIONS_AS_TIMESTAMPS</span><span class="o">,</span> <span class="kc">false</span><span class="o">);</span>
<span class="n">FEATURE_DEFAULTS</span> <span class="o">=</span> <span class="n">Collections</span><span class="o">.</span><span class="na">unmodifiableMap</span><span class="o">(</span><span class="n">featureDefaults</span><span class="o">);</span>
<span class="o">}</span>
<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="n">JsonComponentModule</span> <span class="nf">jsonComponentModule</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">JsonComponentModule</span><span class="o">();</span>
<span class="o">}</span>
<span class="nd">@Configuration</span><span class="o">(</span><span class="n">proxyBeanMethods</span> <span class="o">=</span> <span class="kc">false</span><span class="o">)</span>
<span class="nd">@ConditionalOnClass</span><span class="o">(</span><span class="n">Jackson2ObjectMapperBuilder</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="kd">static</span> <span class="kd">class</span> <span class="nc">JacksonObjectMapperConfiguration</span> <span class="o">{</span>
<span class="nd">@Bean</span>
<span class="nd">@Primary</span>
<span class="nd">@ConditionalOnMissingBean</span>
<span class="n">ObjectMapper</span> <span class="nf">jacksonObjectMapper</span><span class="o">(</span><span class="n">Jackson2ObjectMapperBuilder</span> <span class="n">builder</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">builder</span><span class="o">.</span><span class="na">createXmlMapper</span><span class="o">(</span><span class="kc">false</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="nd">@Configuration</span><span class="o">(</span><span class="n">proxyBeanMethods</span> <span class="o">=</span> <span class="kc">false</span><span class="o">)</span>
<span class="nd">@ConditionalOnClass</span><span class="o">(</span><span class="n">ParameterNamesModule</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="kd">static</span> <span class="kd">class</span> <span class="nc">ParameterNamesModuleConfiguration</span> <span class="o">{</span>
<span class="nd">@Bean</span>
<span class="nd">@ConditionalOnMissingBean</span>
<span class="n">ParameterNamesModule</span> <span class="nf">parameterNamesModule</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">ParameterNamesModule</span><span class="o">(</span><span class="n">JsonCreator</span><span class="o">.</span><span class="na">Mode</span><span class="o">.</span><span class="na">DEFAULT</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="nd">@Configuration</span><span class="o">(</span><span class="n">proxyBeanMethods</span> <span class="o">=</span> <span class="kc">false</span><span class="o">)</span>
<span class="nd">@ConditionalOnClass</span><span class="o">(</span><span class="n">Jackson2ObjectMapperBuilder</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="kd">static</span> <span class="kd">class</span> <span class="nc">JacksonObjectMapperBuilderConfiguration</span> <span class="o">{</span>
<span class="nd">@Bean</span>
<span class="nd">@Scope</span><span class="o">(</span><span class="s">"prototype"</span><span class="o">)</span>
<span class="nd">@ConditionalOnMissingBean</span>
<span class="n">Jackson2ObjectMapperBuilder</span> <span class="nf">jacksonObjectMapperBuilder</span><span class="o">(</span><span class="n">ApplicationContext</span> <span class="n">applicationContext</span><span class="o">,</span>
<span class="n">List</span><span class="o"><</span><span class="n">Jackson2ObjectMapperBuilderCustomizer</span><span class="o">></span> <span class="n">customizers</span><span class="o">)</span> <span class="o">{</span>
<span class="n">Jackson2ObjectMapperBuilder</span> <span class="n">builder</span> <span class="o">=</span> <span class="k">new</span> <span class="n">Jackson2ObjectMapperBuilder</span><span class="o">();</span>
<span class="n">builder</span><span class="o">.</span><span class="na">applicationContext</span><span class="o">(</span><span class="n">applicationContext</span><span class="o">);</span>
<span class="n">customize</span><span class="o">(</span><span class="n">builder</span><span class="o">,</span> <span class="n">customizers</span><span class="o">);</span>
<span class="k">return</span> <span class="n">builder</span><span class="o">;</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="kt">void</span> <span class="nf">customize</span><span class="o">(</span><span class="n">Jackson2ObjectMapperBuilder</span> <span class="n">builder</span><span class="o">,</span>
<span class="n">List</span><span class="o"><</span><span class="n">Jackson2ObjectMapperBuilderCustomizer</span><span class="o">></span> <span class="n">customizers</span><span class="o">)</span> <span class="o">{</span>
<span class="k">for</span> <span class="o">(</span><span class="n">Jackson2ObjectMapperBuilderCustomizer</span> <span class="n">customizer</span> <span class="o">:</span> <span class="n">customizers</span><span class="o">)</span> <span class="o">{</span>
<span class="n">customizer</span><span class="o">.</span><span class="na">customize</span><span class="o">(</span><span class="n">builder</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="nd">@Configuration</span><span class="o">(</span><span class="n">proxyBeanMethods</span> <span class="o">=</span> <span class="kc">false</span><span class="o">)</span>
<span class="nd">@ConditionalOnClass</span><span class="o">(</span><span class="n">Jackson2ObjectMapperBuilder</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="nd">@EnableConfigurationProperties</span><span class="o">(</span><span class="n">JacksonProperties</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="kd">static</span> <span class="kd">class</span> <span class="nc">Jackson2ObjectMapperBuilderCustomizerConfiguration</span> <span class="o">{</span>
<span class="nd">@Bean</span>
<span class="n">StandardJackson2ObjectMapperBuilderCustomizer</span> <span class="nf">standardJacksonObjectMapperBuilderCustomizer</span><span class="o">(</span>
<span class="n">ApplicationContext</span> <span class="n">applicationContext</span><span class="o">,</span> <span class="n">JacksonProperties</span> <span class="n">jacksonProperties</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">StandardJackson2ObjectMapperBuilderCustomizer</span><span class="o">(</span><span class="n">applicationContext</span><span class="o">,</span> <span class="n">jacksonProperties</span><span class="o">);</span>
<span class="o">}</span>
<span class="kd">static</span> <span class="kd">final</span> <span class="kd">class</span> <span class="nc">StandardJackson2ObjectMapperBuilderCustomizer</span>
<span class="kd">implements</span> <span class="n">Jackson2ObjectMapperBuilderCustomizer</span><span class="o">,</span> <span class="n">Ordered</span> <span class="o">{</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="n">ApplicationContext</span> <span class="n">applicationContext</span><span class="o">;</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="n">JacksonProperties</span> <span class="n">jacksonProperties</span><span class="o">;</span>
<span class="n">StandardJackson2ObjectMapperBuilderCustomizer</span><span class="o">(</span><span class="n">ApplicationContext</span> <span class="n">applicationContext</span><span class="o">,</span>
<span class="n">JacksonProperties</span> <span class="n">jacksonProperties</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">applicationContext</span> <span class="o">=</span> <span class="n">applicationContext</span><span class="o">;</span>
<span class="k">this</span><span class="o">.</span><span class="na">jacksonProperties</span> <span class="o">=</span> <span class="n">jacksonProperties</span><span class="o">;</span>
<span class="o">}</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">int</span> <span class="nf">getOrder</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="mi">0</span><span class="o">;</span>
<span class="o">}</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">customize</span><span class="o">(</span><span class="n">Jackson2ObjectMapperBuilder</span> <span class="n">builder</span><span class="o">)</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">jacksonProperties</span><span class="o">.</span><span class="na">getDefaultPropertyInclusion</span><span class="o">()</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="n">builder</span><span class="o">.</span><span class="na">serializationInclusion</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">jacksonProperties</span><span class="o">.</span><span class="na">getDefaultPropertyInclusion</span><span class="o">());</span>
<span class="o">}</span>
<span class="k">if</span> <span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">jacksonProperties</span><span class="o">.</span><span class="na">getTimeZone</span><span class="o">()</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="n">builder</span><span class="o">.</span><span class="na">timeZone</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">jacksonProperties</span><span class="o">.</span><span class="na">getTimeZone</span><span class="o">());</span>
<span class="o">}</span>
<span class="n">configureFeatures</span><span class="o">(</span><span class="n">builder</span><span class="o">,</span> <span class="n">FEATURE_DEFAULTS</span><span class="o">);</span>
<span class="n">configureVisibility</span><span class="o">(</span><span class="n">builder</span><span class="o">,</span> <span class="k">this</span><span class="o">.</span><span class="na">jacksonProperties</span><span class="o">.</span><span class="na">getVisibility</span><span class="o">());</span>
<span class="n">configureFeatures</span><span class="o">(</span><span class="n">builder</span><span class="o">,</span> <span class="k">this</span><span class="o">.</span><span class="na">jacksonProperties</span><span class="o">.</span><span class="na">getDeserialization</span><span class="o">());</span>
<span class="n">configureFeatures</span><span class="o">(</span><span class="n">builder</span><span class="o">,</span> <span class="k">this</span><span class="o">.</span><span class="na">jacksonProperties</span><span class="o">.</span><span class="na">getSerialization</span><span class="o">());</span>
<span class="n">configureFeatures</span><span class="o">(</span><span class="n">builder</span><span class="o">,</span> <span class="k">this</span><span class="o">.</span><span class="na">jacksonProperties</span><span class="o">.</span><span class="na">getMapper</span><span class="o">());</span>
<span class="n">configureFeatures</span><span class="o">(</span><span class="n">builder</span><span class="o">,</span> <span class="k">this</span><span class="o">.</span><span class="na">jacksonProperties</span><span class="o">.</span><span class="na">getParser</span><span class="o">());</span>
<span class="n">configureFeatures</span><span class="o">(</span><span class="n">builder</span><span class="o">,</span> <span class="k">this</span><span class="o">.</span><span class="na">jacksonProperties</span><span class="o">.</span><span class="na">getGenerator</span><span class="o">());</span>
<span class="n">configureDateFormat</span><span class="o">(</span><span class="n">builder</span><span class="o">);</span>
<span class="n">configurePropertyNamingStrategy</span><span class="o">(</span><span class="n">builder</span><span class="o">);</span>
<span class="n">configureModules</span><span class="o">(</span><span class="n">builder</span><span class="o">);</span>
<span class="n">configureLocale</span><span class="o">(</span><span class="n">builder</span><span class="o">);</span>
<span class="o">}</span>
<span class="c1">//more configure methods...</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p><strong>总结</strong>:通过一系列的方法,最终构造一个生产级别可用的<code class="highlighter-rouge">ObjectMapper</code>对象,供在Spring Boot框架中对Java对象实现序列化与反序列化操作。</p>
<h2 id="5jackson常见注解使用示例">5.Jackson常见注解使用示例</h2>
<blockquote>
<p><strong>备注</strong>:本小结内容来源<a href="https://www.baeldung.com/jackson-annotations">https://www.baeldung.com/jackson-annotations</a>,如果工作中对于jackson的注解使用较少的情况下,可以看看该篇文章,是一个非常好的补充。</p>
</blockquote>
<h3 id="51-序列化">5.1 序列化</h3>
<h4 id="511-jsonanygetter">5.1.1 @JsonAnyGetter</h4>
<p><code class="highlighter-rouge">@JsonAnyGetter</code>注解运行可以灵活的使用<code class="highlighter-rouge">Map</code>类型的作为属性字段</p>
<p>实体类如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">ExtendableBean</span> <span class="o">{</span>
<span class="kd">public</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">Map</span><span class="o"><</span><span class="n">String</span><span class="o">,</span> <span class="n">String</span><span class="o">></span> <span class="n">properties</span><span class="o">;</span>
<span class="nd">@JsonAnyGetter</span>
<span class="kd">public</span> <span class="n">Map</span><span class="o"><</span><span class="n">String</span><span class="o">,</span> <span class="n">String</span><span class="o">></span> <span class="nf">getProperties</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">properties</span><span class="o">;</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="nf">ExtendableBean</span><span class="o">(</span><span class="n">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">name</span> <span class="o">=</span> <span class="n">name</span><span class="o">;</span>
<span class="k">this</span><span class="o">.</span><span class="na">properties</span><span class="o">=</span><span class="k">new</span> <span class="n">HashMap</span><span class="o"><</span><span class="n">String</span><span class="o">,</span> <span class="n">String</span><span class="o">>();</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">add</span><span class="o">(</span><span class="n">String</span> <span class="n">key</span><span class="o">,</span><span class="n">String</span> <span class="n">value</span><span class="o">){</span>
<span class="k">this</span><span class="o">.</span><span class="na">properties</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="n">key</span><span class="o">,</span><span class="n">value</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>通过序列化该实体Bean,我们将会得到<code class="highlighter-rouge">Map</code>属性中的所有<code class="highlighter-rouge">Key</code>作为属性值,测试序列化代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">whenSerializingUsingJsonAnyGetter_thenCorrect</span><span class="o">()</span>
<span class="kd">throws</span> <span class="n">JsonProcessingException</span> <span class="o">{</span>
<span class="n">ExtendableBean</span> <span class="n">bean</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ExtendableBean</span><span class="o">(</span><span class="s">"My bean"</span><span class="o">);</span>
<span class="n">bean</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="s">"attr1"</span><span class="o">,</span> <span class="s">"val1"</span><span class="o">);</span>
<span class="n">bean</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="s">"attr2"</span><span class="o">,</span> <span class="s">"val2"</span><span class="o">);</span>
<span class="n">String</span> <span class="n">result</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ObjectMapper</span><span class="o">().</span><span class="na">writeValueAsString</span><span class="o">(</span><span class="n">bean</span><span class="o">);</span>
<span class="n">assertThat</span><span class="o">(</span><span class="n">result</span><span class="o">,</span> <span class="n">containsString</span><span class="o">(</span><span class="s">"attr1"</span><span class="o">));</span>
<span class="n">assertThat</span><span class="o">(</span><span class="n">result</span><span class="o">,</span> <span class="n">containsString</span><span class="o">(</span><span class="s">"val1"</span><span class="o">));</span>
<span class="o">}</span>
</code></pre></div></div>
<p>最终输出结果如下:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="s2">"My bean"</span><span class="p">,</span><span class="w">
</span><span class="s2">"attr2"</span><span class="p">:</span><span class="s2">"val2"</span><span class="p">,</span><span class="w">
</span><span class="s2">"attr1"</span><span class="p">:</span><span class="s2">"val1"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>如果不使用<code class="highlighter-rouge">@JsonAnyGetter</code>注解,那么最终序列化结果将会在<code class="highlighter-rouge">properties</code>属性下面,结果如下:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"My bean"</span><span class="p">,</span><span class="w">
</span><span class="s2">"properties"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"attr2"</span><span class="p">:</span><span class="w"> </span><span class="s2">"val2"</span><span class="p">,</span><span class="w">
</span><span class="s2">"attr1"</span><span class="p">:</span><span class="w"> </span><span class="s2">"val1"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<h4 id="512-jsongetter">5.1.2 @JsonGetter</h4>
<p><code class="highlighter-rouge">@JsonGetter</code>注解是一个替代<code class="highlighter-rouge">@JsonProperty</code>的注解,可以将一个方法标注为<code class="highlighter-rouge">getter</code>方法</p>
<p>例如下面的示例中,我们通过注解<code class="highlighter-rouge">@JsonGetter</code>将方法<code class="highlighter-rouge">getTheName()</code>作为属性<code class="highlighter-rouge">name</code>的<code class="highlighter-rouge">getter</code>方法</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">MyBean</span> <span class="o">{</span>
<span class="kd">public</span> <span class="kt">int</span> <span class="n">id</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="nd">@JsonGetter</span><span class="o">(</span><span class="s">"name"</span><span class="o">)</span>
<span class="kd">public</span> <span class="n">String</span> <span class="nf">getTheName</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">name</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<h4 id="513-jsonpropertyorder">5.1.3 @JsonPropertyOrder</h4>
<p>可以通过使用<code class="highlighter-rouge">@JsonPropertyOrder</code>注解来指定属性的序列化顺序</p>
<p>实体bean定义如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@JsonPropertyOrder</span><span class="o">({</span> <span class="s">"name"</span><span class="o">,</span> <span class="s">"id"</span> <span class="o">})</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">MyBean</span> <span class="o">{</span>
<span class="kd">public</span> <span class="kt">int</span> <span class="n">id</span><span class="o">;</span>
<span class="kd">public</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>最终序列化结果为:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="s2">"My bean"</span><span class="p">,</span><span class="w">
</span><span class="s2">"id"</span><span class="p">:</span><span class="mi">1</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>也可以通过<code class="highlighter-rouge">@JsonPropertyOrder(alphabetic=true)</code>来指定按照字母排序,那么响应结果将是:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="s2">"id"</span><span class="p">:</span><span class="mi">1</span><span class="p">,</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="s2">"My bean"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<h4 id="514-jsonrawvalue">5.1.4 @JsonRawValue</h4>
<p><code class="highlighter-rouge">@JsonRawValue</code>注解可以指定字符串属性类为json,如下代码:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">RawBean</span> <span class="o">{</span>
<span class="kd">public</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="nd">@JsonRawValue</span>
<span class="kd">public</span> <span class="n">String</span> <span class="n">json</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>创建<code class="highlighter-rouge">RawBean</code>的示例,给属性<code class="highlighter-rouge">json</code>赋值,代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="n">RawBean</span> <span class="n">bean</span> <span class="o">=</span> <span class="k">new</span> <span class="n">RawBean</span><span class="o">(</span><span class="s">"My bean"</span><span class="o">,</span> <span class="s">"{\"attr\":false}"</span><span class="o">);</span>
<span class="n">String</span> <span class="n">result</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ObjectMapper</span><span class="o">().</span><span class="na">writeValueAsString</span><span class="o">(</span><span class="n">bean</span><span class="o">);</span>
</code></pre></div></div>
<p>最终序列化结果如下:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="s2">"My bean"</span><span class="p">,</span><span class="w">
</span><span class="s2">"json"</span><span class="p">:{</span><span class="w">
</span><span class="s2">"attr"</span><span class="p">:</span><span class="kc">false</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<h4 id="515-jsonvalue">5.1.5 @JsonValue</h4>
<p><code class="highlighter-rouge">@JsonValue</code>注解主要用于序列化整个实例对象的单个方法,例如,在一个枚举类中,<code class="highlighter-rouge">@JsonValue</code>注解进行标注,代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">enum</span> <span class="n">TypeEnumWithValue</span> <span class="o">{</span>
<span class="n">TYPE1</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="s">"Type A"</span><span class="o">),</span> <span class="n">TYPE2</span><span class="o">(</span><span class="mi">2</span><span class="o">,</span> <span class="s">"Type 2"</span><span class="o">);</span>
<span class="kd">private</span> <span class="n">Integer</span> <span class="n">id</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="n">TypeEnumWithValue</span><span class="o">(</span><span class="n">Integer</span> <span class="n">id</span><span class="o">,</span> <span class="n">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">id</span> <span class="o">=</span> <span class="n">id</span><span class="o">;</span>
<span class="k">this</span><span class="o">.</span><span class="na">name</span> <span class="o">=</span> <span class="n">name</span><span class="o">;</span>
<span class="o">}</span>
<span class="nd">@JsonValue</span>
<span class="kd">public</span> <span class="n">String</span> <span class="nf">getName</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">name</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>测试代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">String</span> <span class="n">enumAsString</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ObjectMapper</span><span class="o">()</span>
<span class="o">.</span><span class="na">writeValueAsString</span><span class="o">(</span><span class="n">TypeEnumWithValue</span><span class="o">.</span><span class="na">TYPE1</span><span class="o">);</span>
<span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">enumAsString</span><span class="o">);</span>
</code></pre></div></div>
<p>最终通过序列化代码得到的结果将是:</p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"Type A"
</code></pre></div></div>
<h4 id="516-jsonrootname">5.1.6 @JsonRootName</h4>
<p><code class="highlighter-rouge">@JsonRootName</code>注解旨在给当前序列化的实体对象加一层包裹对象。</p>
<p>举例如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//RootUser.java</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">RootUser</span> <span class="o">{</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">title</span><span class="o">;</span>
<span class="kd">public</span> <span class="nf">RootUser</span><span class="o">(</span><span class="n">String</span> <span class="n">name</span><span class="o">,</span> <span class="n">String</span> <span class="n">title</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">name</span> <span class="o">=</span> <span class="n">name</span><span class="o">;</span>
<span class="k">this</span><span class="o">.</span><span class="na">title</span> <span class="o">=</span> <span class="n">title</span><span class="o">;</span>
<span class="o">}</span>
<span class="c1">//getter and setters </span>
<span class="o">}</span>
</code></pre></div></div>
<p>在上面的实体类中,正常情况下,如果要序列号<code class="highlighter-rouge">RootUser</code>对象,其结果格式为:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"name1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"title1"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>在<code class="highlighter-rouge">RootUser</code>加上<code class="highlighter-rouge">@JsonRootName</code>注解后,该类改动如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//RootUser.java</span>
<span class="nd">@JsonRootName</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"root"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">RootUser</span> <span class="o">{</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">title</span><span class="o">;</span>
<span class="kd">public</span> <span class="nf">RootUser</span><span class="o">(</span><span class="n">String</span> <span class="n">name</span><span class="o">,</span> <span class="n">String</span> <span class="n">title</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">name</span> <span class="o">=</span> <span class="n">name</span><span class="o">;</span>
<span class="k">this</span><span class="o">.</span><span class="na">title</span> <span class="o">=</span> <span class="n">title</span><span class="o">;</span>
<span class="o">}</span>
<span class="c1">//getter and setters </span>
<span class="o">}</span>
</code></pre></div></div>
<p>启用<code class="highlighter-rouge">ObjectMapper</code>对象的<code class="highlighter-rouge">WRAP_ROOT_VALUE</code>特性,测试代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ObjectMapper</span> <span class="n">objectMapper</span><span class="o">=</span><span class="k">new</span> <span class="n">ObjectMapper</span><span class="o">();</span>
<span class="n">objectMapper</span><span class="o">.</span><span class="na">enable</span><span class="o">(</span><span class="n">SerializationFeature</span><span class="o">.</span><span class="na">WRAP_ROOT_VALUE</span><span class="o">);</span>
<span class="n">String</span> <span class="n">result</span><span class="o">=</span><span class="n">objectMapper</span><span class="o">.</span><span class="na">writeValueAsString</span><span class="o">(</span><span class="k">new</span> <span class="n">RootUser</span><span class="o">(</span><span class="s">"name1"</span><span class="o">,</span><span class="s">"title1"</span><span class="o">));</span>
</code></pre></div></div>
<p>最终序列化JSON结果如下:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="s2">"root"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"name1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"title1"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<h4 id="517-jsonserialize">5.1.7 @JsonSerialize</h4>
<p><code class="highlighter-rouge">@JsonSerialize</code>注解允许开发者自定义序列化实现,来看代码实现</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">EventWithSerializer</span> <span class="o">{</span>
<span class="kd">public</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="nd">@JsonSerialize</span><span class="o">(</span><span class="n">using</span> <span class="o">=</span> <span class="n">CustomDateSerializer</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="kd">public</span> <span class="n">Date</span> <span class="n">eventDate</span><span class="o">;</span>
<span class="kd">public</span> <span class="n">Date</span> <span class="n">publishDate</span><span class="o">;</span>
<span class="c1">//getter and setter...</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在上面的代码中,针对<code class="highlighter-rouge">eventDate</code>字段,我们通过使用<code class="highlighter-rouge">@JsonSerialize</code>注解,自定义了一个序列化实现类<code class="highlighter-rouge">CustomDateSerializer</code>,该类实现如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//CustomDateSerializer.java</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">CustomDateSerializer</span> <span class="kd">extends</span> <span class="n">StdSerializer</span><span class="o"><</span><span class="n">Date</span><span class="o">></span> <span class="o">{</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="n">SimpleDateFormat</span> <span class="n">formatter</span>
<span class="o">=</span> <span class="k">new</span> <span class="n">SimpleDateFormat</span><span class="o">(</span><span class="s">"dd-MM-yyyy hh:mm:ss"</span><span class="o">);</span>
<span class="kd">public</span> <span class="nf">CustomDateSerializer</span><span class="o">()</span> <span class="o">{</span>
<span class="k">this</span><span class="o">(</span><span class="kc">null</span><span class="o">);</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="nf">CustomDateSerializer</span><span class="o">(</span><span class="n">Class</span><span class="o"><</span><span class="n">Date</span><span class="o">></span> <span class="n">t</span><span class="o">)</span> <span class="o">{</span>
<span class="kd">super</span><span class="o">(</span><span class="n">t</span><span class="o">);</span>
<span class="o">}</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">serialize</span><span class="o">(</span>
<span class="n">Date</span> <span class="n">value</span><span class="o">,</span> <span class="n">JsonGenerator</span> <span class="n">gen</span><span class="o">,</span> <span class="n">SerializerProvider</span> <span class="n">arg2</span><span class="o">)</span>
<span class="kd">throws</span> <span class="n">IOException</span><span class="o">,</span> <span class="n">JsonProcessingException</span> <span class="o">{</span>
<span class="n">gen</span><span class="o">.</span><span class="na">writeString</span><span class="o">(</span><span class="n">formatter</span><span class="o">.</span><span class="na">format</span><span class="o">(</span><span class="n">value</span><span class="o">));</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>最终序列化的结果格式如下:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"名称"</span><span class="p">,</span><span class="w">
</span><span class="s2">"eventDate"</span><span class="p">:</span><span class="w"> </span><span class="s2">"24-03-2021 06:14:32"</span><span class="p">,</span><span class="w">
</span><span class="s2">"publishDate"</span><span class="p">:</span><span class="w"> </span><span class="mi">1616580872574</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>从结果我们可以得知,针对某个特定的字段序列化的方式,我们可以完全自定义,非常的方便。</p>
<h3 id="52-反序列化">5.2 反序列化</h3>
<h4 id="521-jsoncreator">5.2.1 @JsonCreator</h4>
<p><code class="highlighter-rouge">@JsonCreator</code>配合<code class="highlighter-rouge">@JsonProperty</code>注解能到达在反序列化实体对象时,指定不变更属性名称的效果</p>
<p>例如有如下JSON:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="s2">"id"</span><span class="p">:</span><span class="mi">1</span><span class="p">,</span><span class="w">
</span><span class="s2">"theName"</span><span class="p">:</span><span class="s2">"My bean"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>在实体类中,我们没有属性名称是<code class="highlighter-rouge">theName</code>,但我们想把<code class="highlighter-rouge">theName</code>属性反序列化时赋值给<code class="highlighter-rouge">name</code>,此时实体类对象结构如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">BeanWithCreator</span> <span class="o">{</span>
<span class="kd">public</span> <span class="kt">int</span> <span class="n">id</span><span class="o">;</span>
<span class="kd">public</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="nd">@JsonCreator</span>
<span class="kd">public</span> <span class="nf">BeanWithCreator</span><span class="o">(</span>
<span class="nd">@JsonProperty</span><span class="o">(</span><span class="s">"id"</span><span class="o">)</span> <span class="kt">int</span> <span class="n">id</span><span class="o">,</span>
<span class="nd">@JsonProperty</span><span class="o">(</span><span class="s">"theName"</span><span class="o">)</span> <span class="n">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">id</span> <span class="o">=</span> <span class="n">id</span><span class="o">;</span>
<span class="k">this</span><span class="o">.</span><span class="na">name</span> <span class="o">=</span> <span class="n">name</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在<code class="highlighter-rouge">BeanWithCreator</code>的构造函数中添加<code class="highlighter-rouge">@JsonCreator</code>注解,并且配合<code class="highlighter-rouge">@JsonProperty</code>注解进行属性指向,最终反序列化代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">whenDeserializingUsingJsonCreator_thenCorrect</span><span class="o">()</span>
<span class="kd">throws</span> <span class="n">IOException</span> <span class="o">{</span>
<span class="n">String</span> <span class="n">json</span> <span class="o">=</span> <span class="s">"{\"id\":1,\"theName\":\"My bean\"}"</span><span class="o">;</span>
<span class="n">BeanWithCreator</span> <span class="n">bean</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ObjectMapper</span><span class="o">()</span>
<span class="o">.</span><span class="na">readerFor</span><span class="o">(</span><span class="n">BeanWithCreator</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="o">.</span><span class="na">readValue</span><span class="o">(</span><span class="n">json</span><span class="o">);</span>
<span class="n">assertEquals</span><span class="o">(</span><span class="s">"My bean"</span><span class="o">,</span> <span class="n">bean</span><span class="o">.</span><span class="na">name</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<h4 id="522-jacksoninject">5.2.2 @JacksonInject</h4>
<p><code class="highlighter-rouge">@JacksonInject</code>注解可以指定反序列化对象时,属性值不从来源JSON获取,而从<code class="highlighter-rouge">injection</code>中获取</p>
<p>实体类如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">BeanWithInject</span> <span class="o">{</span>
<span class="nd">@JacksonInject</span>
<span class="kd">public</span> <span class="kt">int</span> <span class="n">id</span><span class="o">;</span>
<span class="kd">public</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>反序列化代码</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">whenDeserializingUsingJsonInject_thenCorrect</span><span class="o">()</span>
<span class="kd">throws</span> <span class="n">IOException</span> <span class="o">{</span>
<span class="n">String</span> <span class="n">json</span> <span class="o">=</span> <span class="s">"{\"name\":\"My bean\"}"</span><span class="o">;</span>
<span class="n">InjectableValues</span> <span class="n">inject</span> <span class="o">=</span> <span class="k">new</span> <span class="n">InjectableValues</span><span class="o">.</span><span class="na">Std</span><span class="o">()</span>
<span class="o">.</span><span class="na">addValue</span><span class="o">(</span><span class="kt">int</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="mi">1</span><span class="o">);</span>
<span class="n">BeanWithInject</span> <span class="n">bean</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ObjectMapper</span><span class="o">().</span><span class="na">reader</span><span class="o">(</span><span class="n">inject</span><span class="o">)</span>
<span class="o">.</span><span class="na">forType</span><span class="o">(</span><span class="n">BeanWithInject</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="o">.</span><span class="na">readValue</span><span class="o">(</span><span class="n">json</span><span class="o">);</span>
<span class="n">assertEquals</span><span class="o">(</span><span class="s">"My bean"</span><span class="o">,</span> <span class="n">bean</span><span class="o">.</span><span class="na">name</span><span class="o">);</span>
<span class="n">assertEquals</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="n">bean</span><span class="o">.</span><span class="na">id</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<h4 id="523-jsonanysetter">5.2.3 @JsonAnySetter</h4>
<p><code class="highlighter-rouge">@JsonAnySetter</code>和<code class="highlighter-rouge">@JsonAnyGetter</code>注解意思一致,只不过是针对序列化与反序列化而言,<code class="highlighter-rouge">@JsonAnySetter</code>注解可以将来源JSON最终转化为<code class="highlighter-rouge">Map</code>类型的属性结构</p>
<p>实体代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">ExtendableBean</span> <span class="o">{</span>
<span class="kd">public</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">Map</span><span class="o"><</span><span class="n">String</span><span class="o">,</span> <span class="n">String</span><span class="o">></span> <span class="n">properties</span><span class="o">;</span>
<span class="nd">@JsonAnySetter</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">add</span><span class="o">(</span><span class="n">String</span> <span class="n">key</span><span class="o">,</span> <span class="n">String</span> <span class="n">value</span><span class="o">)</span> <span class="o">{</span>
<span class="n">properties</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="n">value</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>JSON源如下:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="s2">"My bean"</span><span class="p">,</span><span class="w">
</span><span class="s2">"attr2"</span><span class="p">:</span><span class="s2">"val2"</span><span class="p">,</span><span class="w">
</span><span class="s2">"attr1"</span><span class="p">:</span><span class="s2">"val1"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>通过<code class="highlighter-rouge">@JsonAnySetter</code>的注解标注,最终<code class="highlighter-rouge">attr1</code>及<code class="highlighter-rouge">attr2</code>的值将会添加到<code class="highlighter-rouge">properties</code>的Map对象中</p>
<p>示例代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">whenDeserializingUsingJsonAnySetter_thenCorrect</span><span class="o">()</span>
<span class="kd">throws</span> <span class="n">IOException</span> <span class="o">{</span>
<span class="n">String</span> <span class="n">json</span>
<span class="o">=</span> <span class="s">"{\"name\":\"My bean\",\"attr2\":\"val2\",\"attr1\":\"val1\"}"</span><span class="o">;</span>
<span class="n">ExtendableBean</span> <span class="n">bean</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ObjectMapper</span><span class="o">()</span>
<span class="o">.</span><span class="na">readerFor</span><span class="o">(</span><span class="n">ExtendableBean</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="o">.</span><span class="na">readValue</span><span class="o">(</span><span class="n">json</span><span class="o">);</span>
<span class="n">assertEquals</span><span class="o">(</span><span class="s">"My bean"</span><span class="o">,</span> <span class="n">bean</span><span class="o">.</span><span class="na">name</span><span class="o">);</span>
<span class="n">assertEquals</span><span class="o">(</span><span class="s">"val2"</span><span class="o">,</span> <span class="n">bean</span><span class="o">.</span><span class="na">getProperties</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="s">"attr2"</span><span class="o">));</span>
<span class="o">}</span>
</code></pre></div></div>
<h4 id="524-jsonsetter">5.2.4 @JsonSetter</h4>
<p><code class="highlighter-rouge">@JsonSetter</code>注解是<code class="highlighter-rouge">@JsonProperty</code>的替代注解,用于标注该方法为<code class="highlighter-rouge">setter</code>方法</p>
<p>当我们需要读取一些JSON数据时,但是目标实体类与该数据不完全匹配是,该注解是非常有用的。</p>
<p>示例代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">MyBean</span> <span class="o">{</span>
<span class="kd">public</span> <span class="kt">int</span> <span class="n">id</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="nd">@JsonSetter</span><span class="o">(</span><span class="s">"name"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">setTheName</span><span class="o">(</span><span class="n">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">name</span> <span class="o">=</span> <span class="n">name</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>通过指定<code class="highlighter-rouge">setTheName</code>作为属性<code class="highlighter-rouge">name</code>的<code class="highlighter-rouge">setter</code>方法,反序列化时可以达到最终效果</p>
<p>示例如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">whenDeserializingUsingJsonSetter_thenCorrect</span><span class="o">()</span>
<span class="kd">throws</span> <span class="n">IOException</span> <span class="o">{</span>
<span class="n">String</span> <span class="n">json</span> <span class="o">=</span> <span class="s">"{\"id\":1,\"name\":\"My bean\"}"</span><span class="o">;</span>
<span class="n">MyBean</span> <span class="n">bean</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ObjectMapper</span><span class="o">()</span>
<span class="o">.</span><span class="na">readerFor</span><span class="o">(</span><span class="n">MyBean</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="o">.</span><span class="na">readValue</span><span class="o">(</span><span class="n">json</span><span class="o">);</span>
<span class="n">assertEquals</span><span class="o">(</span><span class="s">"My bean"</span><span class="o">,</span> <span class="n">bean</span><span class="o">.</span><span class="na">getTheName</span><span class="o">());</span>
<span class="o">}</span>
</code></pre></div></div>
<h4 id="525-jsondeserialize">5.2.5 @JsonDeserialize</h4>
<p><code class="highlighter-rouge">@JsonDeserialize</code>注解和序列化注解<code class="highlighter-rouge">@JsonSerialize</code>的效果是一致的,作用与反序列化时,针对特定的字段,存在差异化的发序列化效果</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">EventWithSerializer</span> <span class="o">{</span>
<span class="kd">public</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="nd">@JsonDeserialize</span><span class="o">(</span><span class="n">using</span> <span class="o">=</span> <span class="n">CustomDateDeserializer</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="kd">public</span> <span class="n">Date</span> <span class="n">eventDate</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p><code class="highlighter-rouge">CustomDateDeserializer</code>代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">CustomDateDeserializer</span>
<span class="kd">extends</span> <span class="n">StdDeserializer</span><span class="o"><</span><span class="n">Date</span><span class="o">></span> <span class="o">{</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="n">SimpleDateFormat</span> <span class="n">formatter</span>
<span class="o">=</span> <span class="k">new</span> <span class="n">SimpleDateFormat</span><span class="o">(</span><span class="s">"dd-MM-yyyy hh:mm:ss"</span><span class="o">);</span>
<span class="kd">public</span> <span class="nf">CustomDateDeserializer</span><span class="o">()</span> <span class="o">{</span>
<span class="k">this</span><span class="o">(</span><span class="kc">null</span><span class="o">);</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="nf">CustomDateDeserializer</span><span class="o">(</span><span class="n">Class</span><span class="o"><?></span> <span class="n">vc</span><span class="o">)</span> <span class="o">{</span>
<span class="kd">super</span><span class="o">(</span><span class="n">vc</span><span class="o">);</span>
<span class="o">}</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="n">Date</span> <span class="nf">deserialize</span><span class="o">(</span>
<span class="n">JsonParser</span> <span class="n">jsonparser</span><span class="o">,</span> <span class="n">DeserializationContext</span> <span class="n">context</span><span class="o">)</span>
<span class="kd">throws</span> <span class="n">IOException</span> <span class="o">{</span>
<span class="n">String</span> <span class="n">date</span> <span class="o">=</span> <span class="n">jsonparser</span><span class="o">.</span><span class="na">getText</span><span class="o">();</span>
<span class="k">try</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">formatter</span><span class="o">.</span><span class="na">parse</span><span class="o">(</span><span class="n">date</span><span class="o">);</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">ParseException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">RuntimeException</span><span class="o">(</span><span class="n">e</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>最终,反序列化JSON,时,得到<code class="highlighter-rouge">eventDate</code>字段,测试代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">whenDeserializingUsingJsonDeserialize_thenCorrect</span><span class="o">()</span>
<span class="kd">throws</span> <span class="n">IOException</span> <span class="o">{</span>
<span class="n">String</span> <span class="n">json</span>
<span class="o">=</span> <span class="s">"{"</span><span class="n">name</span><span class="s">":"</span><span class="n">party</span><span class="s">","</span><span class="n">eventDate</span><span class="s">":"</span><span class="mi">20</span><span class="o">-</span><span class="mi">12</span><span class="o">-</span><span class="mi">2014</span> <span class="mo">02</span><span class="o">:</span><span class="mi">30</span><span class="o">:</span><span class="mo">00</span><span class="s">"}"</span><span class="o">;</span>
<span class="n">SimpleDateFormat</span> <span class="n">df</span>
<span class="o">=</span> <span class="k">new</span> <span class="n">SimpleDateFormat</span><span class="o">(</span><span class="s">"dd-MM-yyyy hh:mm:ss"</span><span class="o">);</span>
<span class="n">EventWithSerializer</span> <span class="n">event</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ObjectMapper</span><span class="o">()</span>
<span class="o">.</span><span class="na">readerFor</span><span class="o">(</span><span class="n">EventWithSerializer</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="o">.</span><span class="na">readValue</span><span class="o">(</span><span class="n">json</span><span class="o">);</span>
<span class="n">assertEquals</span><span class="o">(</span>
<span class="s">"20-12-2014 02:30:00"</span><span class="o">,</span> <span class="n">df</span><span class="o">.</span><span class="na">format</span><span class="o">(</span><span class="n">event</span><span class="o">.</span><span class="na">eventDate</span><span class="o">));</span>
<span class="o">}</span>
</code></pre></div></div>
<h4 id="526-jsonalias">5.2.6 @JsonAlias</h4>
<p><code class="highlighter-rouge">@JsonAlias</code>注解作用于可以指定一个别名与JSON数据中的字段进行对于,最终反序列化时,能将该值最终反序列化时赋值给对象</p>
<p>实体如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">AliasBean</span> <span class="o">{</span>
<span class="nd">@JsonAlias</span><span class="o">({</span> <span class="s">"fName"</span><span class="o">,</span> <span class="s">"f_name"</span> <span class="o">})</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">firstName</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">lastName</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>上面的代码中,<code class="highlighter-rouge">firstName</code>字段通过<code class="highlighter-rouge">@JsonAlias</code>注解指定了两个别名字段,意思是反序列化时可以从JSON中读取<code class="highlighter-rouge">fName</code>或者<code class="highlighter-rouge">f_name</code>的值赋值到<code class="highlighter-rouge">firstName</code>中</p>
<p>测试代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">whenDeserializingUsingJsonAlias_thenCorrect</span><span class="o">()</span> <span class="kd">throws</span> <span class="n">IOException</span> <span class="o">{</span>
<span class="n">String</span> <span class="n">json</span> <span class="o">=</span> <span class="s">"{\"fName\": \"John\", \"lastName\": \"Green\"}"</span><span class="o">;</span>
<span class="n">AliasBean</span> <span class="n">aliasBean</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ObjectMapper</span><span class="o">().</span><span class="na">readerFor</span><span class="o">(</span><span class="n">AliasBean</span><span class="o">.</span><span class="na">class</span><span class="o">).</span><span class="na">readValue</span><span class="o">(</span><span class="n">json</span><span class="o">);</span>
<span class="n">assertEquals</span><span class="o">(</span><span class="s">"John"</span><span class="o">,</span> <span class="n">aliasBean</span><span class="o">.</span><span class="na">getFirstName</span><span class="o">());</span>
<span class="o">}</span>
</code></pre></div></div>
<h3 id="53-属性注解">5.3 属性注解</h3>
<h4 id="531-jsonignoreproperties">5.3.1 @JsonIgnoreProperties</h4>
<p>使用<code class="highlighter-rouge">@JsonIgnoreProperties</code>注解作用于class级别中可以达到在序列化时忽略一个或多个字段的效果</p>
<p>实体代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@JsonIgnoreProperties</span><span class="o">({</span> <span class="s">"id"</span> <span class="o">})</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">BeanWithIgnore</span> <span class="o">{</span>
<span class="kd">public</span> <span class="kt">int</span> <span class="n">id</span><span class="o">;</span>
<span class="kd">public</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>最终在序列化<code class="highlighter-rouge">BeanWithIgnore</code>实体对象时,字段<code class="highlighter-rouge">id</code>将会被忽略</p>
<h4 id="532-jsonignore">5.3.2 @JsonIgnore</h4>
<p><code class="highlighter-rouge">@JsonIgnore</code>注解作用与属性级别中,在序列化时可以忽略该字段</p>
<p>实体代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">BeanWithIgnore</span> <span class="o">{</span>
<span class="nd">@JsonIgnore</span>
<span class="kd">public</span> <span class="kt">int</span> <span class="n">id</span><span class="o">;</span>
<span class="kd">public</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>最终在序列化<code class="highlighter-rouge">BeanWithIgnore</code>实体对象时,字段<code class="highlighter-rouge">id</code>将会被忽略</p>
<h4 id="533-jsonignoretype">5.3.3 @JsonIgnoreType</h4>
<p><code class="highlighter-rouge">@JsonIgnoreType</code>指定忽略类型属性</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">User</span> <span class="o">{</span>
<span class="kd">public</span> <span class="kt">int</span> <span class="n">id</span><span class="o">;</span>
<span class="kd">public</span> <span class="n">Name</span> <span class="n">name</span><span class="o">;</span>
<span class="nd">@JsonIgnoreType</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">Name</span> <span class="o">{</span>
<span class="kd">public</span> <span class="n">String</span> <span class="n">firstName</span><span class="o">;</span>
<span class="kd">public</span> <span class="n">String</span> <span class="n">lastName</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在上面的示例中,类型<code class="highlighter-rouge">Name</code>将会被忽略</p>
<h4 id="534-jsoninclude">5.3.4 @JsonInclude</h4>
<p>使用<code class="highlighter-rouge">@JsonInclude</code>注解可以排除属性值中包含<code class="highlighter-rouge">empty/null/default</code>的属性</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@JsonInclude</span><span class="o">(</span><span class="n">Include</span><span class="o">.</span><span class="na">NON_NULL</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">MyBean</span> <span class="o">{</span>
<span class="kd">public</span> <span class="kt">int</span> <span class="n">id</span><span class="o">;</span>
<span class="kd">public</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在<code class="highlighter-rouge">MyBean</code>中使用了<code class="highlighter-rouge">Include.NON_NULL</code>则代表该实体对象序列化时不会包含空值</p>
<h4 id="535-jsonautodetect">5.3.5 @JsonAutoDetect</h4>
<p><code class="highlighter-rouge">@JsonAutoDetect</code>可以覆盖实体对象属性中的默认可见级别,比如私有属性可见与不可见</p>
<p>实体对象如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">PrivateBean</span> <span class="o">{</span>
<span class="kd">private</span> <span class="kt">int</span> <span class="n">id</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="kd">public</span> <span class="nf">PrivateBean</span><span class="o">(</span><span class="kt">int</span> <span class="n">id</span><span class="o">,</span> <span class="n">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">id</span> <span class="o">=</span> <span class="n">id</span><span class="o">;</span>
<span class="k">this</span><span class="o">.</span><span class="na">name</span> <span class="o">=</span> <span class="n">name</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在<code class="highlighter-rouge">PrivateBean</code>中,没有给属性字段<code class="highlighter-rouge">id</code>、<code class="highlighter-rouge">name</code>设置公共的<code class="highlighter-rouge">getter</code>方法,此时,如果我们如果直接对该实体对象进行序列化时,jackson会提示错误</p>
<pre><code class="language-log">Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.xiaoymin.boot.action.jackson.model.PrivateBean and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
</code></pre>
<p>我们修改<code class="highlighter-rouge">PrivateBean</code>中的代码,增加<code class="highlighter-rouge">@JsonAutoDetect</code>注解,代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@JsonAutoDetect</span><span class="o">(</span><span class="n">fieldVisibility</span> <span class="o">=</span> <span class="n">JsonAutoDetect</span><span class="o">.</span><span class="na">Visibility</span><span class="o">.</span><span class="na">ANY</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">PrivateBean</span> <span class="o">{</span>
<span class="kd">private</span> <span class="kt">int</span> <span class="n">id</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="kd">public</span> <span class="nf">PrivateBean</span><span class="o">(</span><span class="kt">int</span> <span class="n">id</span><span class="o">,</span> <span class="n">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">id</span> <span class="o">=</span> <span class="n">id</span><span class="o">;</span>
<span class="k">this</span><span class="o">.</span><span class="na">name</span> <span class="o">=</span> <span class="n">name</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>此时,在序列化该实体对象,将会得到响应结果</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">PrivateBean</span> <span class="n">bean</span> <span class="o">=</span> <span class="k">new</span> <span class="n">PrivateBean</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="s">"My bean"</span><span class="o">);</span>
<span class="n">String</span> <span class="n">result</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ObjectMapper</span><span class="o">().</span><span class="na">writeValueAsString</span><span class="o">(</span><span class="n">bean</span><span class="o">);</span>
<span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">result</span><span class="o">);</span>
</code></pre></div></div>
<h3 id="54-常规注解">5.4 常规注解</h3>
<h4 id="541-jsonproperty">5.4.1 @JsonProperty</h4>
<p>我们可以添加<code class="highlighter-rouge">@JsonProperty</code>批注以在JSON中指示属性名称。</p>
<p>当实体对象中没有标准的<code class="highlighter-rouge">getter/setter</code>方法时,我们可以使用该注解进行指定属性名称已方便jackson框架进行序列化/反序列化</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">MyBean</span> <span class="o">{</span>
<span class="kd">public</span> <span class="kt">int</span> <span class="n">id</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="nd">@JsonProperty</span><span class="o">(</span><span class="s">"name"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">setTheName</span><span class="o">(</span><span class="n">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">name</span> <span class="o">=</span> <span class="n">name</span><span class="o">;</span>
<span class="o">}</span>
<span class="nd">@JsonProperty</span><span class="o">(</span><span class="s">"name"</span><span class="o">)</span>
<span class="kd">public</span> <span class="n">String</span> <span class="nf">getTheName</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">name</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<h4 id="542-jsonformat">5.4.2 @JsonFormat</h4>
<p>针对日期字段可以通过使用<code class="highlighter-rouge">@JsonFormat</code>注解进行格式化输出</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">EventWithFormat</span> <span class="o">{</span>
<span class="kd">public</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="nd">@JsonFormat</span><span class="o">(</span>
<span class="n">shape</span> <span class="o">=</span> <span class="n">JsonFormat</span><span class="o">.</span><span class="na">Shape</span><span class="o">.</span><span class="na">STRING</span><span class="o">,</span>
<span class="n">pattern</span> <span class="o">=</span> <span class="s">"dd-MM-yyyy hh:mm:ss"</span><span class="o">)</span>
<span class="kd">public</span> <span class="n">Date</span> <span class="n">eventDate</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<h4 id="543-jsonunwrapped">5.4.3 @JsonUnwrapped</h4>
<p><code class="highlighter-rouge">@JsonUnwrapped</code>注解可以指定jackson框架在序列化/反序列化时是否需要对该字段进行<code class="highlighter-rouge">wrapped</code>操作</p>
<p>示例代码:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">UnwrappedUser</span> <span class="o">{</span>
<span class="kd">public</span> <span class="kt">int</span> <span class="n">id</span><span class="o">;</span>
<span class="nd">@JsonUnwrapped</span>
<span class="kd">public</span> <span class="n">Name</span> <span class="n">name</span><span class="o">;</span>
<span class="c1">//getter and setter...</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">Name</span> <span class="o">{</span>
<span class="kd">public</span> <span class="n">String</span> <span class="n">firstName</span><span class="o">;</span>
<span class="kd">public</span> <span class="n">String</span> <span class="n">lastName</span><span class="o">;</span>
<span class="c1">//getter and setter</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>通过注解<code class="highlighter-rouge">@JsonUnwrapped</code>标注<code class="highlighter-rouge">name</code>属性,最终序列化该对象时,会和正常情况下有所区别</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">UnwrappedUser</span><span class="o">.</span><span class="na">Name</span> <span class="n">name</span> <span class="o">=</span> <span class="k">new</span> <span class="n">UnwrappedUser</span><span class="o">.</span><span class="na">Name</span><span class="o">(</span><span class="s">"John"</span><span class="o">,</span> <span class="s">"Doe"</span><span class="o">);</span>
<span class="n">UnwrappedUser</span> <span class="n">user</span> <span class="o">=</span> <span class="k">new</span> <span class="n">UnwrappedUser</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="n">name</span><span class="o">);</span>
<span class="n">String</span> <span class="n">result</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ObjectMapper</span><span class="o">().</span><span class="na">writeValueAsString</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>
</code></pre></div></div>
<p>我们得到的结果如下:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="s2">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
</span><span class="s2">"firstName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"John"</span><span class="p">,</span><span class="w">
</span><span class="s2">"lastName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Doe"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<h4 id="544-jsonview">5.4.4 @JsonView</h4>
<p>通过<code class="highlighter-rouge">View</code>的方式来指定序列化/反序列化时是否包含属性</p>
<p>示例代码如下:</p>
<p><code class="highlighter-rouge">View</code>定义</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">Views</span> <span class="o">{</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">Public</span> <span class="o">{}</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">Internal</span> <span class="kd">extends</span> <span class="n">Public</span> <span class="o">{}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>实体代码:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">Item</span> <span class="o">{</span>
<span class="nd">@JsonView</span><span class="o">(</span><span class="n">Views</span><span class="o">.</span><span class="na">Public</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="kd">public</span> <span class="kt">int</span> <span class="n">id</span><span class="o">;</span>
<span class="nd">@JsonView</span><span class="o">(</span><span class="n">Views</span><span class="o">.</span><span class="na">Public</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="kd">public</span> <span class="n">String</span> <span class="n">itemName</span><span class="o">;</span>
<span class="nd">@JsonView</span><span class="o">(</span><span class="n">Views</span><span class="o">.</span><span class="na">Internal</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="kd">public</span> <span class="n">String</span> <span class="n">ownerName</span><span class="o">;</span>
<span class="c1">//getter and setter..</span>
<span class="o">}</span>
</code></pre></div></div>
<p>最终序列化代码示例:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Item</span> <span class="n">item</span> <span class="o">=</span> <span class="k">new</span> <span class="n">Item</span><span class="o">(</span><span class="mi">2</span><span class="o">,</span> <span class="s">"book"</span><span class="o">,</span> <span class="s">"John"</span><span class="o">);</span>
<span class="n">String</span> <span class="n">result</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ObjectMapper</span><span class="o">().</span><span class="na">writerWithView</span><span class="o">(</span><span class="n">Views</span><span class="o">.</span><span class="na">Public</span><span class="o">.</span><span class="na">class</span><span class="o">).</span><span class="na">writeValueAsString</span><span class="o">(</span><span class="n">item</span><span class="o">);</span>
<span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">result</span><span class="o">);</span>
</code></pre></div></div>
<p>最终序列化结果输出:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="s2">"id"</span><span class="p">:</span><span class="mi">2</span><span class="p">,</span><span class="s2">"itemName"</span><span class="p">:</span><span class="s2">"book"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>##</p>肖玉民1.前言Spring Boot框架中针对数据文件模板的下载总结2021-03-03T00:00:00+08:002021-03-03T00:00:00+08:00https://xiaoym.gitee.io/2021/03/03/spring-boot-common-file-download<h2 id="1前言">1.前言</h2>
<p>在我们的日常开发中,经常会碰到注入导入Excel数据到系统中的需求,而在导入Excel数据时,一般的业务系统都会提供数据的Excel模板,只有提交的Excel数据满足业务系统要求的模板时,数据才能够正常的导入系统中。因此针对这种需求,一般我们会在系统中提供一个Excel模板的下载按钮,业务人员在使用时,可以先下载Excel模板,然后按照模板中的格式将数据填充,即可导入成功。本文主要总结目前在开发这类需求时碰到的问题。</p>
<h2 id="2解决方案">2.解决方案</h2>
<p>从需求上来看,目前有大致三种解决方案,针对数据文件的模板下载,分别是:</p>
<ul>
<li>模板文件直接存放在前端,作为静态资源,前端直接可以发送请求进行下载</li>
<li>模板文件存服务器磁盘,提供接口下载</li>
<li>模板文件存储在项目jar包中,提供接口下载</li>
</ul>
<h3 id="21-作为静态资源直接下载">2.1 作为静态资源直接下载</h3>
<p>第一种方式是最简单的,将数据文件直接作为静态资源放在前端目录,前端通过请求即可进行下载</p>
<h3 id="22-模板文件存储在服务器提供接口下载">2.2 模板文件存储在服务器,提供接口下载</h3>
<p>第二种也是我们经常使用的方法,开发人员将模板文件放在服务器中的某个目录下,通过在代码中配置存储目录的方式,并且提供下载接口,当前端发起接口请求时,服务端根据请求将文件写入到响应流中</p>
<p>示例代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Value</span><span class="o">(</span><span class="s">"${templateFile}"</span><span class="o">)</span>
<span class="n">String</span> <span class="n">downloadFilePath</span><span class="o">;</span>
<span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/download"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">downloadExcel</span><span class="o">(</span><span class="n">HttpServletResponse</span> <span class="n">response</span><span class="o">){</span>
<span class="n">logger</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"下载Excel模板"</span><span class="o">);</span>
<span class="k">try</span> <span class="o">{</span>
<span class="n">File</span> <span class="n">file</span><span class="o">=</span><span class="k">new</span> <span class="n">File</span><span class="o">(</span><span class="n">downloadFilePath</span><span class="o">);</span>
<span class="n">ServletUtil</span><span class="o">.</span><span class="na">write</span><span class="o">(</span><span class="n">response</span><span class="o">,</span><span class="n">file</span><span class="o">);</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">IOException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
<span class="n">logger</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="n">e</span><span class="o">.</span><span class="na">getMessage</span><span class="o">(),</span><span class="n">e</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>因为文件存储在磁盘中,并且通过Spring提供的<code class="highlighter-rouge">@Value</code>注解将文件的位置在配置文件中进行配置,因此文件对象我们可以直接通过<code class="highlighter-rouge">new File</code>的方式直接获取到文件,最终调用工具类<code class="highlighter-rouge">ServletUtil</code>将该文件写入到<code class="highlighter-rouge">HttpServletResponse</code>的流中去,实现下载的目录</p>
<h3 id="23-模板文件存在在jar中提供接口下载">2.3 模板文件存在在jar中,提供接口下载</h3>
<p>通过上面的两种下载方式,我们基本已经能实现文件的下载,满足业务的需要,但有时候我也会思考,是否把数据模板文件直接放在Spring Boot的jar中,这种方式的优势:</p>
<ul>
<li>防止模板文件存储在磁盘时被误删的操作发送</li>
<li>如果程序部署需要迁移服务器,能有效避免下载接口的容错,忘记迁移模板文件等情况会导致程序异常</li>
<li>和程序代码存储在一起更加完整</li>
</ul>
<p>基于上面的优势,因此,针对数据模板文件,我认为应该和项目直接放在一起,这样对于程序部署等都是非常有利的。</p>
<p>一般,在Spring Boot的开发框架中,我们可以在<code class="highlighter-rouge">resources</code>目录下建立文件夹,然后将相应的数据文件放入目录中,再提供接口读取该文件进行下载</p>
<p>目录结构如下:</p>
<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">|---project</span>
<span class="err">|--------src/main/java</span>
<span class="err">|--------src/main/resources</span>
<span class="err">|------------data</span>
<span class="c"># 模板文件
</span><span class="err">|--------------template.xlsx</span>
</code></pre></div></div>
<p>因为我们将文件放在了<code class="highlighter-rouge">resources</code>目录下,此时如果要读取该文件,我们需要利用到Spring提供的<a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/io/ClassPathResource.html"><code class="highlighter-rouge">ClassPathResource</code></a>类进行读取,调用代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ClassPathResource</span> <span class="n">classPathResource</span><span class="o">=</span><span class="k">new</span> <span class="n">ClassPathResource</span><span class="o">(</span><span class="s">"data/tag_data_template.xlsx"</span><span class="o">);</span>
</code></pre></div></div>
<p>此时,我们的下载接口代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/download"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">downloadExcel</span><span class="o">(</span><span class="n">HttpServletResponse</span> <span class="n">response</span><span class="o">){</span>
<span class="n">logger</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"下载Excel模板"</span><span class="o">);</span>
<span class="n">ClassPathResource</span> <span class="n">classPathResource</span><span class="o">=</span><span class="k">new</span> <span class="n">ClassPathResource</span><span class="o">(</span><span class="s">"data/template.xlsx"</span><span class="o">);</span>
<span class="k">try</span> <span class="o">{</span>
<span class="c1">//创建临时文件</span>
<span class="n">File</span> <span class="n">file</span><span class="o">=</span><span class="n">File</span><span class="o">.</span><span class="na">createTempFile</span><span class="o">(</span><span class="s">"template"</span><span class="o">,</span><span class="s">".xlsx"</span><span class="o">);</span>
<span class="c1">//从当前resources目录下的文件流拷贝到File中</span>
<span class="n">FileUtils</span><span class="o">.</span><span class="na">copyInputStreamToFile</span><span class="o">(</span><span class="n">classPathResource</span><span class="o">.</span><span class="na">getInputStream</span><span class="o">(),</span><span class="n">file</span><span class="o">);</span>
<span class="n">logger</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"fileName:{}"</span><span class="o">,</span><span class="n">file</span><span class="o">.</span><span class="na">getName</span><span class="o">());</span>
<span class="c1">//将临时文件写出到流中</span>
<span class="n">ServletUtil</span><span class="o">.</span><span class="na">write</span><span class="o">(</span><span class="n">response</span><span class="o">,</span><span class="n">file</span><span class="o">);</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">IOException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
<span class="n">logger</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="n">e</span><span class="o">.</span><span class="na">getMessage</span><span class="o">(),</span><span class="n">e</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>这里会有1个疑问点,就是我们既然已经使用了Spring提供的<a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/io/ClassPathResource.html"><code class="highlighter-rouge">ClassPathResource</code></a>进行读取文件,而该类通过继承<a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/io/AbstractFileResolvingResource.html">AbstractFileResolvingResource</a>也提供了<code class="highlighter-rouge">getFile</code>方法获取File对象,为何不直接调用?</p>
<p>比如下载的接口代码改成这样:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/download"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">downloadExcel</span><span class="o">(</span><span class="n">HttpServletResponse</span> <span class="n">response</span><span class="o">){</span>
<span class="n">logger</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"下载Excel模板"</span><span class="o">);</span>
<span class="n">ClassPathResource</span> <span class="n">classPathResource</span><span class="o">=</span><span class="k">new</span> <span class="n">ClassPathResource</span><span class="o">(</span><span class="s">"data/template.xlsx"</span><span class="o">);</span>
<span class="k">try</span> <span class="o">{</span>
<span class="c1">//直接获取文件</span>
<span class="n">File</span> <span class="n">file</span><span class="o">=</span><span class="n">classPathResource</span><span class="o">.</span><span class="na">getFile</span><span class="o">();</span>
<span class="c1">//将临时文件写出到流中</span>
<span class="n">ServletUtil</span><span class="o">.</span><span class="na">write</span><span class="o">(</span><span class="n">response</span><span class="o">,</span><span class="n">file</span><span class="o">);</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">IOException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
<span class="n">logger</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="n">e</span><span class="o">.</span><span class="na">getMessage</span><span class="o">(),</span><span class="n">e</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>通过源码来分析</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//AbstractFileResolvingResource.getFile</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="n">File</span> <span class="nf">getFile</span><span class="o">()</span> <span class="kd">throws</span> <span class="n">IOException</span> <span class="o">{</span>
<span class="n">URL</span> <span class="n">url</span> <span class="o">=</span> <span class="n">getURL</span><span class="o">();</span>
<span class="k">if</span> <span class="o">(</span><span class="n">url</span><span class="o">.</span><span class="na">getProtocol</span><span class="o">().</span><span class="na">startsWith</span><span class="o">(</span><span class="n">ResourceUtils</span><span class="o">.</span><span class="na">URL_PROTOCOL_VFS</span><span class="o">))</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">VfsResourceDelegate</span><span class="o">.</span><span class="na">getResource</span><span class="o">(</span><span class="n">url</span><span class="o">).</span><span class="na">getFile</span><span class="o">();</span>
<span class="o">}</span>
<span class="k">return</span> <span class="n">ResourceUtils</span><span class="o">.</span><span class="na">getFile</span><span class="o">(</span><span class="n">url</span><span class="o">,</span> <span class="n">getDescription</span><span class="o">());</span>
<span class="o">}</span>
<span class="c1">//ResourceUtils</span>
<span class="cm">/** URL protocol for a file in the file system: "file". */</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="n">String</span> <span class="n">URL_PROTOCOL_FILE</span> <span class="o">=</span> <span class="s">"file"</span><span class="o">;</span>
<span class="c1">//ResourceUtils.getFile</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="n">File</span> <span class="nf">getFile</span><span class="o">(</span><span class="n">URL</span> <span class="n">resourceUrl</span><span class="o">,</span> <span class="n">String</span> <span class="n">description</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">FileNotFoundException</span> <span class="o">{</span>
<span class="n">Assert</span><span class="o">.</span><span class="na">notNull</span><span class="o">(</span><span class="n">resourceUrl</span><span class="o">,</span> <span class="s">"Resource URL must not be null"</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(!</span><span class="n">URL_PROTOCOL_FILE</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">resourceUrl</span><span class="o">.</span><span class="na">getProtocol</span><span class="o">()))</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">FileNotFoundException</span><span class="o">(</span>
<span class="n">description</span> <span class="o">+</span> <span class="s">" cannot be resolved to absolute file path "</span> <span class="o">+</span>
<span class="s">"because it does not reside in the file system: "</span> <span class="o">+</span> <span class="n">resourceUrl</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">try</span> <span class="o">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">File</span><span class="o">(</span><span class="n">toURI</span><span class="o">(</span><span class="n">resourceUrl</span><span class="o">).</span><span class="na">getSchemeSpecificPart</span><span class="o">());</span>
<span class="o">}</span>
<span class="k">catch</span> <span class="o">(</span><span class="n">URISyntaxException</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// Fallback for URLs that are not valid URIs (should hardly ever happen).</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">File</span><span class="o">(</span><span class="n">resourceUrl</span><span class="o">.</span><span class="na">getFile</span><span class="o">());</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在最终的<code class="highlighter-rouge">ResourceUtils.getFile</code>方法获取File对象时,Spring会对当前URL对象的协议进行判断,如果文件的协议不是<code class="highlighter-rouge">file</code>,则会抛出异常,提示</p>
<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="err">class</span> <span class="err">path</span> <span class="err">resource</span> <span class="err">[data/tag_data_template.xlsx]</span> <span class="err">cannot</span> <span class="err">be</span> <span class="err">resolved</span> <span class="err">to</span> <span class="err">absolute</span> <span class="err">file</span> <span class="err">path</span> <span class="err">because</span> <span class="err">it</span> <span class="err">does</span> <span class="err">not</span> <span class="err">reside</span> <span class="err">in</span> <span class="err">the</span> <span class="err">file</span> <span class="py">system</span><span class="p">:</span> <span class="s">jar:file:/home/app.jar/BOOT-INF/classes!/data/template.xlsx</span>
</code></pre></div></div>
<p>大致的意思就是该文件不在文件系统中,既然Spring不允许这么干,那么我们只能通过获取该文件的输入流的方式,将流写到临时文件中去,最终将该临时文件写出。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//FileUtils.copyInputStreamToFile方法</span>
<span class="c1">//commons-io 包中提供的方法</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">copyInputStreamToFile</span><span class="o">(</span><span class="n">InputStream</span> <span class="n">source</span><span class="o">,</span> <span class="n">File</span> <span class="n">destination</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">IOException</span> <span class="o">{</span>
<span class="k">try</span> <span class="o">{</span>
<span class="n">FileOutputStream</span> <span class="n">output</span> <span class="o">=</span> <span class="n">openOutputStream</span><span class="o">(</span><span class="n">destination</span><span class="o">);</span>
<span class="k">try</span> <span class="o">{</span>
<span class="n">IOUtils</span><span class="o">.</span><span class="na">copy</span><span class="o">(</span><span class="n">source</span><span class="o">,</span> <span class="n">output</span><span class="o">);</span>
<span class="n">output</span><span class="o">.</span><span class="na">close</span><span class="o">();</span> <span class="c1">// don't swallow close Exception if copy completes normally</span>
<span class="o">}</span> <span class="k">finally</span> <span class="o">{</span>
<span class="n">IOUtils</span><span class="o">.</span><span class="na">closeQuietly</span><span class="o">(</span><span class="n">output</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span> <span class="k">finally</span> <span class="o">{</span>
<span class="n">IOUtils</span><span class="o">.</span><span class="na">closeQuietly</span><span class="o">(</span><span class="n">source</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>以上的操作完成后,我们可能还会碰到部署时,代码还是会抛异常的问题,说文件找不到,这种情况一般会和我们项目的maven打包配置有关,我们需要在项目的maven配置中将模板文件也一起打包进去,例如增加配置如下:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><resources></span>
<span class="nt"><resource></span>
<span class="nt"><directory></span>src/main/resources<span class="nt"></directory></span>
<span class="nt"><includes></span>
<span class="c"><!--包含data目录下的所有文件一起打包--></span>
<span class="nt"><include></span>**/data/**<span class="nt"></include></span>
<span class="nt"></includes></span>
<span class="nt"><filtering></span>false<span class="nt"></filtering></span>
<span class="nt"></resource></span>
<span class="nt"></resources></span>
</code></pre></div></div>
<p>至此,就大功告成了!!!</p>
<h2 id="3附录">3.附录</h2>
<h3 id="31-servletutilwrite方法">3.1 <code class="highlighter-rouge">ServletUtil.write</code>方法</h3>
<p><a href="https://hutool.cn/docs/#/extra/Servlet%E5%B7%A5%E5%85%B7-ServletUtil"><code class="highlighter-rouge">ServletUtil</code></a>工具类是引用的开源项目<a href="https://github.com/looly/hutool">Hutool</a>中的一个关于Servlet的工具类封装.</p>
<p><code class="highlighter-rouge">write</code>方法提供了将文件写入到流中的封装,来看具体的源码:</p>
<blockquote>
<p>封装了我们工作中基础的写出流的操作,我们在代码中也可以通过调用此方法简化我们的代码。</p>
</blockquote>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/** 默认缓存大小 8192*/</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">int</span> <span class="n">DEFAULT_BUFFER_SIZE</span> <span class="o">=</span> <span class="mi">2</span> <span class="o"><<</span> <span class="mi">12</span><span class="o">;</span>
<span class="cm">/**
* 返回文件给客户端
*
* @param response 响应对象{@link HttpServletResponse}
* @param file 写出的文件对象
* @since 4.1.15
*/</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">write</span><span class="o">(</span><span class="n">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span> <span class="n">File</span> <span class="n">file</span><span class="o">)</span> <span class="o">{</span>
<span class="kd">final</span> <span class="n">String</span> <span class="n">fileName</span> <span class="o">=</span> <span class="n">file</span><span class="o">.</span><span class="na">getName</span><span class="o">();</span>
<span class="c1">//根据文件名称获取文件的响应类型,如果没有则默认application/octet-stream</span>
<span class="kd">final</span> <span class="n">String</span> <span class="n">contentType</span> <span class="o">=</span> <span class="n">ObjectUtil</span><span class="o">.</span><span class="na">defaultIfNull</span><span class="o">(</span><span class="n">FileUtil</span><span class="o">.</span><span class="na">getMimeType</span><span class="o">(</span><span class="n">fileName</span><span class="o">),</span><span class="s">"application/octet-stream"</span><span class="o">);</span>
<span class="n">BufferedInputStream</span> <span class="n">in</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
<span class="k">try</span> <span class="o">{</span>
<span class="n">in</span> <span class="o">=</span> <span class="n">FileUtil</span><span class="o">.</span><span class="na">getInputStream</span><span class="o">(</span><span class="n">file</span><span class="o">);</span>
<span class="c1">//再次调用,写出Header等信息</span>
<span class="n">write</span><span class="o">(</span><span class="n">response</span><span class="o">,</span> <span class="n">in</span><span class="o">,</span> <span class="n">contentType</span><span class="o">,</span> <span class="n">fileName</span><span class="o">);</span>
<span class="o">}</span> <span class="k">finally</span> <span class="o">{</span>
<span class="n">IoUtil</span><span class="o">.</span><span class="na">close</span><span class="o">(</span><span class="n">in</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="cm">/**
* 返回数据给客户端
*
* @param response 响应对象{@link HttpServletResponse}
* @param in 需要返回客户端的内容
* @param contentType 返回的类型
* @param fileName 文件名
* @since 4.1.15
*/</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">write</span><span class="o">(</span><span class="n">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span> <span class="n">InputStream</span> <span class="n">in</span><span class="o">,</span> <span class="n">String</span> <span class="n">contentType</span><span class="o">,</span> <span class="n">String</span> <span class="n">fileName</span><span class="o">)</span> <span class="o">{</span>
<span class="kd">final</span> <span class="n">String</span> <span class="n">charset</span> <span class="o">=</span> <span class="n">ObjectUtil</span><span class="o">.</span><span class="na">defaultIfNull</span><span class="o">(</span><span class="n">response</span><span class="o">.</span><span class="na">getCharacterEncoding</span><span class="o">(),</span> <span class="n">CharsetUtil</span><span class="o">.</span><span class="na">UTF_8</span><span class="o">);</span>
<span class="n">response</span><span class="o">.</span><span class="na">setHeader</span><span class="o">(</span><span class="s">"Content-Disposition"</span><span class="o">,</span> <span class="n">StrUtil</span><span class="o">.</span><span class="na">format</span><span class="o">(</span><span class="s">"attachment;filename={}"</span><span class="o">,</span> <span class="n">URLUtil</span><span class="o">.</span><span class="na">encode</span><span class="o">(</span><span class="n">fileName</span><span class="o">,</span> <span class="n">charset</span><span class="o">)));</span>
<span class="n">response</span><span class="o">.</span><span class="na">setContentType</span><span class="o">(</span><span class="n">contentType</span><span class="o">);</span>
<span class="c1">//写出</span>
<span class="n">write</span><span class="o">(</span><span class="n">response</span><span class="o">,</span> <span class="n">in</span><span class="o">);</span>
<span class="o">}</span>
<span class="cm">/**
* 返回数据给客户端
*
* @param response 响应对象{@link HttpServletResponse}
* @param in 需要返回客户端的内容
*/</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">write</span><span class="o">(</span><span class="n">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span> <span class="n">InputStream</span> <span class="n">in</span><span class="o">)</span> <span class="o">{</span>
<span class="n">write</span><span class="o">(</span><span class="n">response</span><span class="o">,</span> <span class="n">in</span><span class="o">,</span> <span class="n">IoUtil</span><span class="o">.</span><span class="na">DEFAULT_BUFFER_SIZE</span><span class="o">);</span>
<span class="o">}</span>
<span class="cm">/**
* 返回数据给客户端
*
* @param response 响应对象{@link HttpServletResponse}
* @param in 需要返回客户端的内容
* @param bufferSize 缓存大小
*/</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">write</span><span class="o">(</span><span class="n">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span> <span class="n">InputStream</span> <span class="n">in</span><span class="o">,</span> <span class="kt">int</span> <span class="n">bufferSize</span><span class="o">)</span> <span class="o">{</span>
<span class="n">ServletOutputStream</span> <span class="n">out</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
<span class="k">try</span> <span class="o">{</span>
<span class="n">out</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="na">getOutputStream</span><span class="o">();</span>
<span class="n">IoUtil</span><span class="o">.</span><span class="na">copy</span><span class="o">(</span><span class="n">in</span><span class="o">,</span> <span class="n">out</span><span class="o">,</span> <span class="n">bufferSize</span><span class="o">);</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">IOException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">UtilException</span><span class="o">(</span><span class="n">e</span><span class="o">);</span>
<span class="o">}</span> <span class="k">finally</span> <span class="o">{</span>
<span class="n">IoUtil</span><span class="o">.</span><span class="na">close</span><span class="o">(</span><span class="n">out</span><span class="o">);</span>
<span class="n">IoUtil</span><span class="o">.</span><span class="na">close</span><span class="o">(</span><span class="n">in</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>肖玉民1.前言基于Servlet体系的HTTP请求代理转发Spring Boot组件2021-02-03T00:00:00+08:002021-02-03T00:00:00+08:00https://xiaoym.gitee.io/2021/02/03/spring-boot-servlet-gateway-compoents<h2 id="背景概述">背景概述</h2>
<p>两个项目组原本都是各自负责两个产品线(<strong>产品A</strong>、<strong>产品B</strong>),由于公司业务的发展,目前需要将两个产品合并成一个大产品(功能整合,部分做取舍,最终产出<strong>产品C</strong>),前后端代码必然也需要整合,包括两个产品线的用户体系等。并且给出的<strong>时间节点很紧张</strong>。</p>
<p>目前两个产品线的区别点:</p>
<p><strong>产品A</strong></p>
<ul>
<li>前端模块载体是微信小程序,没有H5、APP等需求,因此所采用的技术栈是原生写法,没有用到技术框架</li>
<li>服务端技术架构是单体架构,Spring Boot框架,管理后台框架采用的是Apache Shiro</li>
<li>前后端接口调用采用的是服务端<code class="highlighter-rouge">token</code>鉴权的方式交互</li>
<li>用户体系简单,小程序端没有会员等业务,仅涉及到微信openid,管理后台涉及权限菜单.</li>
<li>后端管理系统前端开发技术框架是React</li>
</ul>
<p><strong>产品B</strong></p>
<ul>
<li>前端模块载体多样,包括微信小程序、H5、APP等,因此采用的是多端统一框架,例如:union-app</li>
<li>服务端技术架构单体架构,Spring Boot框架</li>
<li>前后端接口调用采用的是服务端<code class="highlighter-rouge">token</code>鉴权的方式交互</li>
<li>用户体系复杂,有会员、优惠券等业务,管理后台涉及权限菜单</li>
<li>后端管理系统前端开发技术框架是Vue</li>
</ul>
<p><strong>产品C</strong></p>
<ul>
<li>载体是微信小程序,没有H5、APP等需求</li>
<li>产品A中的功能居多,产品B中的功能占用少部分</li>
</ul>
<p>鉴于上面的背景,我们讨论接下来产品线合并的可能性</p>
<ul>
<li>前端代码重写,虽说是产品线合并,但是原来两个产品线的功能点只是做整合,并没有太多新增的功能,因此原来的部分功能模块可以复用,采用原生写法,不用多端框架</li>
<li>后端用户体系复用产品B中的体系,基本控制菜单权限即可</li>
<li>考虑到时间紧迫,因此原本产品A\B两个产品线的已有的功能基本不动,只对新增模块的功能进行开发。</li>
<li>产品B的后端系统功能菜单、权限系统较A完善,因此作为产品C的管理后端进行复用,将产品A的后端功能全部移动到产品C中,由于两个产品线管理后台开发的技术栈不一样,因此产品C中的部分功能需要重写,将产品A的功能使用Vue的技术栈移到产品C中</li>
</ul>
<h3 id="游客端小程序端">游客端(小程序端)</h3>
<p>针对产品C的小程序端,由于需要包含产品A中的某一核心功能,因此不太可能使用多端框架进行重写(PS:主要是领导给的时间不够),因此采用的做法是直接在产品A的基础上衍生一个版本,最终将产品B中的部分功能,通过原生框架,最终在产品C中进行呈现。</p>
<p>因为小程序的接口调用方式是直连,通过发起<code class="highlighter-rouge">HTTPS</code>的接口请求即可,因此服务端接口逻辑不动,前端开发人员只需要和产品B的人员进行接口对接即可,最终接口调用流程示意图如下:</p>
<p><img src="/assets/images/springboot/servlet-gateway/servlet-gateway.png" alt="" /></p>
<h3 id="管理端pc端">管理端(PC端)</h3>
<p>管理端则不同,由于是使用的产品B中的后台,因此产品A中的权限控制需要去除(例如登录后才能调用接口等限制),而产品A中的接口权限控制需要交给B来管,发送请求时需要校验当前请求的权限,校验通过后再转发给A,调用时序图如下:</p>
<p><img src="/assets/images/springboot/servlet-gateway/servlet-gateway2.png" alt="" /></p>
<p>上面这张图也是这个组件雏形,寄希望与通过该转发组件,通过提供不同的转发方式,封装转发HTTP请求的能力,达到直连服务的目的</p>
<blockquote>
<p>如果单纯从一个新产品C的角度出发,<code class="highlighter-rouge">ServiceA</code>中的服务接口代码应该合并到<code class="highlighter-rouge">ServiceB</code>,最终形成一个新的<code class="highlighter-rouge">ServiceC</code>,但是考虑到时间紧迫,所以代码层面的合并并没有形成,因此考虑直接将请求HTTP转发的方式,最终将任务完成。</p>
</blockquote>
<h2 id="程序设计">程序设计</h2>
<p>从需求背景出发,在程序设计上需要考虑的几个点:</p>
<ul>
<li>上游服务接收到的固定请求头,或者请求参数,比如多租户系统需要接收一个租户的请求header,因此转发组件需要有配置固定header的能力,以便在实际转发过程中发送到下游服务,方便系统扩展</li>
<li>需要提供权限验证的接口,不同的权限框架可能验证方式不同,有些系统是<code class="highlighter-rouge">Shiro</code>,或者<code class="highlighter-rouge">Spring Security</code>,或者自研,因此在最终权限校验时,考虑到和系统的兼容性,对于下游的转发服务接口,需要提供和系统兼容的验证接口,不可打破原系统的稳定性</li>
<li>转发的方式支持类别,考虑到系统的健壮性,需要提供不同的转发类别支撑</li>
</ul>
<p>由于是基于Servlet体系,因此对于接口的请求,需要做一层拦截判断,以验证当前的请求是否是需要转发到下游服务,核心过滤器如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">ServletGatewayRouteProxyFilter</span> <span class="kd">implements</span> <span class="n">Filter</span> <span class="o">{</span>
<span class="c1">//执行器对象</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="n">RouteDispatcher</span> <span class="n">routeDispatcher</span><span class="o">;</span>
<span class="c1">//权限对象</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="n">ServletGatewayAuthentication</span> <span class="n">servletGatewayAuthentication</span><span class="o">;</span>
<span class="n">Logger</span> <span class="n">logger</span><span class="o">=</span> <span class="n">LoggerFactory</span><span class="o">.</span><span class="na">getLogger</span><span class="o">(</span><span class="n">ServletGatewayRouteProxyFilter</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
<span class="cm">/**
* 狗仔ProxyHttpFilter 对象实例
* @param routeDispatcher 执行器对象
* @param servletGatewayAuthentication 权限校验对象
*/</span>
<span class="kd">public</span> <span class="nf">ServletGatewayRouteProxyFilter</span><span class="o">(</span><span class="n">RouteDispatcher</span> <span class="n">routeDispatcher</span><span class="o">,</span> <span class="n">ServletGatewayAuthentication</span> <span class="n">servletGatewayAuthentication</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">routeDispatcher</span> <span class="o">=</span> <span class="n">routeDispatcher</span><span class="o">;</span>
<span class="k">this</span><span class="o">.</span><span class="na">servletGatewayAuthentication</span> <span class="o">=</span> <span class="n">servletGatewayAuthentication</span><span class="o">;</span>
<span class="o">}</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">doFilter</span><span class="o">(</span><span class="n">ServletRequest</span> <span class="n">servletRequest</span><span class="o">,</span> <span class="n">ServletResponse</span> <span class="n">servletResponse</span><span class="o">,</span> <span class="n">FilterChain</span> <span class="n">filterChain</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">IOException</span><span class="o">,</span> <span class="n">ServletException</span> <span class="o">{</span>
<span class="n">HttpServletRequest</span> <span class="n">request</span><span class="o">=</span> <span class="o">(</span><span class="n">HttpServletRequest</span><span class="o">)</span> <span class="n">servletRequest</span><span class="o">;</span>
<span class="n">HttpServletResponse</span> <span class="n">response</span><span class="o">=(</span><span class="n">HttpServletResponse</span><span class="o">)</span> <span class="n">servletResponse</span><span class="o">;</span>
<span class="c1">//根据程序配置方式,截取当前请求是否符合转发请求</span>
<span class="n">Optional</span><span class="o"><</span><span class="n">ServiceRoute</span><span class="o">></span> <span class="n">serviceRouteOptional</span><span class="o">=</span><span class="n">routeDispatcher</span><span class="o">.</span><span class="na">assertServletRequest</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">serviceRouteOptional</span><span class="o">.</span><span class="na">isPresent</span><span class="o">()){</span>
<span class="n">logger</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"转发目标服务,地址:{}"</span><span class="o">,</span><span class="n">request</span><span class="o">.</span><span class="na">getRequestURI</span><span class="o">());</span>
<span class="k">if</span> <span class="o">(</span><span class="n">servletGatewayAuthentication</span><span class="o">.</span><span class="na">required</span><span class="o">()){</span>
<span class="k">if</span> <span class="o">(</span><span class="n">servletGatewayAuthentication</span><span class="o">.</span><span class="na">auth</span><span class="o">(</span><span class="n">servletRequest</span><span class="o">,</span><span class="n">servletResponse</span><span class="o">)){</span>
<span class="n">routeDispatcher</span><span class="o">.</span><span class="na">execute</span><span class="o">(</span><span class="n">request</span><span class="o">,</span><span class="n">response</span><span class="o">,</span><span class="n">serviceRouteOptional</span><span class="o">.</span><span class="na">get</span><span class="o">());</span>
<span class="o">}</span><span class="k">else</span><span class="o">{</span>
<span class="n">servletGatewayAuthentication</span><span class="o">.</span><span class="na">failedHandle</span><span class="o">(</span><span class="n">servletRequest</span><span class="o">,</span><span class="n">servletResponse</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span><span class="k">else</span><span class="o">{</span>
<span class="n">routeDispatcher</span><span class="o">.</span><span class="na">execute</span><span class="o">(</span><span class="n">request</span><span class="o">,</span><span class="n">response</span><span class="o">,</span><span class="n">serviceRouteOptional</span><span class="o">.</span><span class="na">get</span><span class="o">());</span>
<span class="o">}</span>
<span class="o">}</span><span class="k">else</span><span class="o">{</span>
<span class="c1">//不符合,继续执行</span>
<span class="n">filterChain</span><span class="o">.</span><span class="na">doFilter</span><span class="o">(</span><span class="n">servletRequest</span><span class="o">,</span><span class="n">servletResponse</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="c1">//other code...</span>
<span class="o">}</span>
</code></pre></div></div>
<p>对于当前的<code class="highlighter-rouge">HttpServletRequest</code>信息做判断,获取当前请求的<code class="highlighter-rouge">ServiceRoute</code>对象,以此来判断请求是否需要转发</p>
<p><code class="highlighter-rouge">ServiceRoute</code>对象主要包含下游转发服务的HTTP地址、端口号、固定Header信息</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">ServiceRoute</span> <span class="o">{</span>
<span class="cm">/**
* 转发模式
*/</span>
<span class="kd">private</span> <span class="n">RouteModeEnum</span> <span class="n">mode</span><span class="o">;</span>
<span class="cm">/**
* 匹配值
*/</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">value</span><span class="o">;</span>
<span class="cm">/**
* 转发目标地址,例如:http://192.179.0.1:8999
*/</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">uri</span><span class="o">;</span>
<span class="cm">/**
* 发送请求头
*/</span>
<span class="kd">private</span> <span class="n">Map</span><span class="o"><</span><span class="n">String</span><span class="o">,</span><span class="n">String</span><span class="o">></span> <span class="n">headers</span><span class="o">;</span>
<span class="c1">//getter and setter</span>
<span class="o">}</span>
</code></pre></div></div>
<p>而<code class="highlighter-rouge">ServiceRoute</code>是最终交给开发者配置的信息,转发请求方式,判断逻辑如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
* 校验当前路由规则是否符合
* @param serviceRoute 路由实例
* @param servletRequest 请求对象
* @return 是否符合规则
*/</span>
<span class="kd">protected</span> <span class="kt">boolean</span> <span class="nf">checkRoute</span><span class="o">(</span><span class="n">ServiceRoute</span> <span class="n">serviceRoute</span><span class="o">,</span><span class="n">HttpServletRequest</span> <span class="n">servletRequest</span><span class="o">){</span>
<span class="kt">boolean</span> <span class="n">flag</span><span class="o">=</span><span class="kc">false</span><span class="o">;</span>
<span class="k">if</span> <span class="o">(</span><span class="n">serviceRoute</span><span class="o">!=</span><span class="kc">null</span><span class="o">){</span>
<span class="k">switch</span> <span class="o">(</span><span class="n">serviceRoute</span><span class="o">.</span><span class="na">getMode</span><span class="o">()){</span>
<span class="c1">//基于请求头</span>
<span class="k">case</span> <span class="nl">ROUTE_MODE_HEADER:</span>
<span class="n">String</span> <span class="n">value</span><span class="o">=</span><span class="n">servletRequest</span><span class="o">.</span><span class="na">getHeader</span><span class="o">(</span><span class="n">ROUTE_MODE_HEADER_NAME</span><span class="o">);</span>
<span class="n">flag</span><span class="o">=</span><span class="n">StrUtil</span><span class="o">.</span><span class="na">equalsIgnoreCase</span><span class="o">(</span><span class="n">value</span><span class="o">,</span><span class="n">serviceRoute</span><span class="o">.</span><span class="na">getValue</span><span class="o">());</span>
<span class="k">break</span><span class="o">;</span>
<span class="c1">//基于URI的前缀匹配</span>
<span class="k">case</span> <span class="nl">ROUTE_MODE_PREFIX:</span>
<span class="n">flag</span><span class="o">=</span><span class="n">servletRequest</span><span class="o">.</span><span class="na">getRequestURI</span><span class="o">().</span><span class="na">startsWith</span><span class="o">(</span><span class="n">serviceRoute</span><span class="o">.</span><span class="na">getValue</span><span class="o">());</span>
<span class="k">break</span><span class="o">;</span>
<span class="c1">//基于URI的后缀匹配</span>
<span class="k">case</span> <span class="nl">ROUTE_MODE_SUFFIX:</span>
<span class="n">flag</span><span class="o">=</span><span class="n">servletRequest</span><span class="o">.</span><span class="na">getRequestURI</span><span class="o">().</span><span class="na">endsWith</span><span class="o">(</span><span class="n">serviceRoute</span><span class="o">.</span><span class="na">getValue</span><span class="o">());</span>
<span class="k">break</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="k">return</span> <span class="n">flag</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>针对权限的设计,在<code class="highlighter-rouge">ServletGatewayRouteProxyFilter</code>中,提供了<code class="highlighter-rouge">ServletGatewayAuthentication</code>接口,该接口设计如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">ServletGatewayAuthentication</span> <span class="o">{</span>
<span class="cm">/**
* 权限校验
* @param request 请求request对象
* @param response 响应对象
* @return 是否权限校验通过
*/</span>
<span class="kt">boolean</span> <span class="nf">auth</span><span class="o">(</span><span class="n">ServletRequest</span> <span class="n">request</span><span class="o">,</span> <span class="n">ServletResponse</span> <span class="n">response</span><span class="o">);</span>
<span class="cm">/**
* 权限校验失败后的处理逻辑
* @param request 请求对象
* @param response 响应对象
*/</span>
<span class="kt">void</span> <span class="nf">failedHandle</span><span class="o">(</span><span class="n">ServletRequest</span> <span class="n">request</span><span class="o">,</span> <span class="n">ServletResponse</span> <span class="n">response</span><span class="o">);</span>
<span class="cm">/**
* 是否需要鉴权,默认true
* @return 是否需要鉴权
*/</span>
<span class="k">default</span> <span class="kt">boolean</span> <span class="nf">required</span><span class="o">(){</span><span class="k">return</span> <span class="kc">true</span><span class="o">;}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>主要包含三个接口:</p>
<ul>
<li><code class="highlighter-rouge">auth</code>:权限验证,返回布尔值,该接口方法主要是兼容系统中的权限,对于当前的请求,可以方便的做出权限判断,交由开发者实现</li>
<li><code class="highlighter-rouge">failedHandle</code>:如果权限验证失败,最终响应信息给前端,开发者实现</li>
<li><code class="highlighter-rouge">required</code>:是否需要鉴权的标志,默认是true,代表需要鉴权</li>
</ul>
<p><strong>最后</strong>再来看代理请求的执行逻辑(<code class="highlighter-rouge">RouteDispatcher.java#execute()</code>方法),部分核心代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">execute</span><span class="o">(</span><span class="n">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span> <span class="n">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span><span class="n">ServiceRoute</span> <span class="n">serviceRoute</span><span class="o">){</span>
<span class="k">try</span><span class="o">{</span>
<span class="c1">//构建请求对象</span>
<span class="n">RouteRequestContext</span> <span class="n">routeContext</span><span class="o">=</span><span class="k">new</span> <span class="n">RouteRequestContext</span><span class="o">();</span>
<span class="c1">//请求对象赋值</span>
<span class="k">this</span><span class="o">.</span><span class="na">buildContext</span><span class="o">(</span><span class="n">routeContext</span><span class="o">,</span><span class="n">request</span><span class="o">,</span><span class="n">serviceRoute</span><span class="o">);</span>
<span class="c1">//发送请求</span>
<span class="n">RouteResponse</span> <span class="n">routeResponse</span><span class="o">=</span><span class="n">routeExecutor</span><span class="o">.</span><span class="na">executor</span><span class="o">(</span><span class="n">routeContext</span><span class="o">);</span>
<span class="c1">//响应结果</span>
<span class="n">writeResponseHeader</span><span class="o">(</span><span class="n">routeResponse</span><span class="o">,</span><span class="n">response</span><span class="o">);</span>
<span class="n">writeBody</span><span class="o">(</span><span class="n">routeResponse</span><span class="o">,</span><span class="n">response</span><span class="o">);</span>
<span class="o">}</span><span class="k">catch</span> <span class="o">(</span><span class="n">Exception</span> <span class="n">e</span><span class="o">){</span>
<span class="n">logger</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="s">"has Error:{}"</span><span class="o">,</span><span class="n">e</span><span class="o">.</span><span class="na">getMessage</span><span class="o">());</span>
<span class="n">logger</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="n">e</span><span class="o">.</span><span class="na">getMessage</span><span class="o">(),</span><span class="n">e</span><span class="o">);</span>
<span class="c1">//write Default</span>
<span class="n">writeDefault</span><span class="o">(</span><span class="n">request</span><span class="o">,</span><span class="n">response</span><span class="o">,</span><span class="n">e</span><span class="o">.</span><span class="na">getMessage</span><span class="o">());</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>针对请求上下文的赋值,主要是接收当前请求的请求参数以及请求头,并且根据<code class="highlighter-rouge">ServiceRoute</code>路由基础信息,进行基础赋值,代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
* 构建路由的请求上下文
* @param routeRequestContext 请求上下文对象
* @param request 请求
* @param serviceRoute 路由实例
* @throws IOException IO异常
*/</span>
<span class="kd">protected</span> <span class="kt">void</span> <span class="nf">buildContext</span><span class="o">(</span><span class="n">RouteRequestContext</span> <span class="n">routeRequestContext</span><span class="o">,</span><span class="n">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span><span class="n">ServiceRoute</span> <span class="n">serviceRoute</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">IOException</span> <span class="o">{</span>
<span class="c1">//String uri="http://knife4j.xiaominfo.com";</span>
<span class="n">String</span> <span class="n">uri</span><span class="o">=</span><span class="n">serviceRoute</span><span class="o">.</span><span class="na">getUri</span><span class="o">();</span>
<span class="k">if</span> <span class="o">(</span><span class="n">StrUtil</span><span class="o">.</span><span class="na">isBlank</span><span class="o">(</span><span class="n">uri</span><span class="o">)){</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">RuntimeException</span><span class="o">(</span><span class="s">"Uri is Empty"</span><span class="o">);</span>
<span class="o">}</span>
<span class="n">String</span> <span class="n">host</span><span class="o">=</span><span class="n">URI</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="n">uri</span><span class="o">).</span><span class="na">getHost</span><span class="o">();</span>
<span class="n">String</span> <span class="n">fromUri</span><span class="o">=</span><span class="n">request</span><span class="o">.</span><span class="na">getRequestURI</span><span class="o">();</span>
<span class="n">StringBuilder</span> <span class="n">requestUrlBuilder</span><span class="o">=</span><span class="k">new</span> <span class="n">StringBuilder</span><span class="o">();</span>
<span class="n">requestUrlBuilder</span><span class="o">.</span><span class="na">append</span><span class="o">(</span><span class="n">uri</span><span class="o">);</span>
<span class="c1">//判断当前聚合项目的contextPath</span>
<span class="k">if</span> <span class="o">(</span><span class="n">StrUtil</span><span class="o">.</span><span class="na">isNotBlank</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">rootPath</span><span class="o">)&&!</span><span class="n">StrUtil</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">rootPath</span><span class="o">,</span><span class="n">ROUTE_BASE_PATH</span><span class="o">)){</span>
<span class="n">fromUri</span><span class="o">=</span><span class="n">fromUri</span><span class="o">.</span><span class="na">replaceFirst</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">rootPath</span><span class="o">,</span><span class="s">""</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">if</span> <span class="o">(</span><span class="n">serviceRoute</span><span class="o">.</span><span class="na">getMode</span><span class="o">()==</span> <span class="n">RouteModeEnum</span><span class="o">.</span><span class="na">ROUTE_MODE_PREFIX</span><span class="o">){</span>
<span class="c1">//前缀转发,替换</span>
<span class="n">fromUri</span><span class="o">=</span><span class="n">fromUri</span><span class="o">.</span><span class="na">replaceFirst</span><span class="o">(</span><span class="n">serviceRoute</span><span class="o">.</span><span class="na">getValue</span><span class="o">(),</span><span class="s">"/"</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">if</span> <span class="o">(!</span><span class="n">StrUtil</span><span class="o">.</span><span class="na">startWith</span><span class="o">(</span><span class="n">fromUri</span><span class="o">,</span><span class="s">"/"</span><span class="o">)){</span>
<span class="n">requestUrlBuilder</span><span class="o">.</span><span class="na">append</span><span class="o">(</span><span class="s">"/"</span><span class="o">);</span>
<span class="o">}</span>
<span class="n">requestUrlBuilder</span><span class="o">.</span><span class="na">append</span><span class="o">(</span><span class="n">fromUri</span><span class="o">);</span>
<span class="c1">//String requestUrl=uri+fromUri;</span>
<span class="n">String</span> <span class="n">requestUrl</span><span class="o">=</span><span class="n">requestUrlBuilder</span><span class="o">.</span><span class="na">toString</span><span class="o">();</span>
<span class="n">logger</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"目标请求Url:{},请求类型:{},Host:{}"</span><span class="o">,</span><span class="n">requestUrl</span><span class="o">,</span><span class="n">request</span><span class="o">.</span><span class="na">getMethod</span><span class="o">(),</span><span class="n">host</span><span class="o">);</span>
<span class="n">routeRequestContext</span><span class="o">.</span><span class="na">setOriginalUri</span><span class="o">(</span><span class="n">fromUri</span><span class="o">);</span>
<span class="n">routeRequestContext</span><span class="o">.</span><span class="na">setUrl</span><span class="o">(</span><span class="n">requestUrl</span><span class="o">);</span>
<span class="n">routeRequestContext</span><span class="o">.</span><span class="na">setMethod</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getMethod</span><span class="o">());</span>
<span class="n">Enumeration</span><span class="o"><</span><span class="n">String</span><span class="o">></span> <span class="n">enumeration</span><span class="o">=</span><span class="n">request</span><span class="o">.</span><span class="na">getHeaderNames</span><span class="o">();</span>
<span class="k">while</span> <span class="o">(</span><span class="n">enumeration</span><span class="o">.</span><span class="na">hasMoreElements</span><span class="o">()){</span>
<span class="n">String</span> <span class="n">key</span><span class="o">=</span><span class="n">enumeration</span><span class="o">.</span><span class="na">nextElement</span><span class="o">();</span>
<span class="n">String</span> <span class="n">value</span><span class="o">=</span><span class="n">request</span><span class="o">.</span><span class="na">getHeader</span><span class="o">(</span><span class="n">key</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(!</span><span class="n">ignoreHeaders</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="n">key</span><span class="o">.</span><span class="na">toLowerCase</span><span class="o">())){</span>
<span class="n">routeRequestContext</span><span class="o">.</span><span class="na">addHeader</span><span class="o">(</span><span class="n">key</span><span class="o">,</span><span class="n">value</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="c1">//是否有默认Header需要发送</span>
<span class="k">if</span> <span class="o">(</span><span class="n">CollectionUtil</span><span class="o">.</span><span class="na">isNotEmpty</span><span class="o">(</span><span class="n">serviceRoute</span><span class="o">.</span><span class="na">getHeaders</span><span class="o">())){</span>
<span class="k">for</span> <span class="o">(</span><span class="n">Map</span><span class="o">.</span><span class="na">Entry</span><span class="o"><</span><span class="n">String</span><span class="o">,</span><span class="n">String</span><span class="o">></span> <span class="nl">entry:</span><span class="n">serviceRoute</span><span class="o">.</span><span class="na">getHeaders</span><span class="o">().</span><span class="na">entrySet</span><span class="o">()){</span>
<span class="n">routeRequestContext</span><span class="o">.</span><span class="na">addHeader</span><span class="o">(</span><span class="n">entry</span><span class="o">.</span><span class="na">getKey</span><span class="o">(),</span><span class="n">entry</span><span class="o">.</span><span class="na">getValue</span><span class="o">());</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="n">routeRequestContext</span><span class="o">.</span><span class="na">addHeader</span><span class="o">(</span><span class="s">"Host"</span><span class="o">,</span><span class="n">host</span><span class="o">);</span>
<span class="n">Enumeration</span><span class="o"><</span><span class="n">String</span><span class="o">></span> <span class="n">params</span><span class="o">=</span><span class="n">request</span><span class="o">.</span><span class="na">getParameterNames</span><span class="o">();</span>
<span class="k">while</span> <span class="o">(</span><span class="n">params</span><span class="o">.</span><span class="na">hasMoreElements</span><span class="o">()){</span>
<span class="n">String</span> <span class="n">name</span><span class="o">=</span><span class="n">params</span><span class="o">.</span><span class="na">nextElement</span><span class="o">();</span>
<span class="n">String</span> <span class="n">value</span><span class="o">=</span><span class="n">request</span><span class="o">.</span><span class="na">getParameter</span><span class="o">(</span><span class="n">name</span><span class="o">);</span>
<span class="c1">//logger.info("param-name:{},value:{}",name,value);</span>
<span class="n">routeRequestContext</span><span class="o">.</span><span class="na">addParam</span><span class="o">(</span><span class="n">name</span><span class="o">,</span><span class="n">value</span><span class="o">);</span>
<span class="o">}</span>
<span class="n">routeRequestContext</span><span class="o">.</span><span class="na">setRequestContent</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getInputStream</span><span class="o">());</span>
<span class="o">}</span>
</code></pre></div></div>
<h2 id="使用指南">使用指南</h2>
<p><code class="highlighter-rouge">servlet-gateway-spring-boot-starter</code>组件是一组基于Servlet体系的业务转发HTTP组件,主要目的是在现有Spring Boot 框架的基础上,添加基于Filter过滤器的转发能力,丰富框架的业务能力。</p>
<p>目前支持三种模式:</p>
<ul>
<li><code class="highlighter-rouge">ROUTE_MODE_HEADER</code>:基于请求头的转发</li>
<li><code class="highlighter-rouge">ROUTE_MODE_PREFIX</code>:基于请求Uri的请求前缀匹配转发</li>
<li><code class="highlighter-rouge">ROUTE_MODE_SUFFIX</code>:基于请求URI的后缀匹配转发规则</li>
</ul>
<p>使用方法,在Spring Boot的框架中,pom.xml中引入当前组件,代码如下:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><dependency></span>
<span class="nt"><groupId></span>com.github.xiaoymin<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>servlet-gateway-spring-boot-starter<span class="nt"></artifactId></span>
<span class="nt"><version></span>1.0<span class="nt"></version></span>
<span class="nt"></dependency></span>
</code></pre></div></div>
<p>在Spring Boot框架的<code class="highlighter-rouge">application.yml</code>配置文件中进行配置,示例如下:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">server</span><span class="pi">:</span>
<span class="na">servlet</span><span class="pi">:</span>
<span class="na">gateway</span><span class="pi">:</span>
<span class="na">enable</span><span class="pi">:</span> <span class="no">true</span>
<span class="s">cloud</span><span class="pi">:</span>
<span class="na">enable</span><span class="pi">:</span> <span class="no">true</span>
<span class="c1"># Routes节点,可以配置多个</span>
<span class="na">routes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">mode</span><span class="pi">:</span> <span class="s">ROUTE_MODE_PREFIX</span>
<span class="err"> </span> <span class="c1"># 将所有以/abb开头的请求接口全部转发到uri中的目标服务</span>
<span class="na">value</span><span class="pi">:</span> <span class="s">/abb/</span>
<span class="na">uri</span><span class="pi">:</span> <span class="s">http://knife4j.xiaominfo.com</span>
<span class="c1"># 配置发送默认请求头(可选配置)</span>
<span class="na">headers</span><span class="pi">:</span>
<span class="na">code</span><span class="pi">:</span> <span class="s">TESS</span>
</code></pre></div></div>
<p>针对代理请求鉴权功能,该组件提供了<code class="highlighter-rouge">ServletGatewayAuthentication</code>接口,对于接入该组件的项目需要实现该接口,并且注入到 Spring 的容器中</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">ServletGatewayAuthentication</span> <span class="o">{</span>
<span class="cm">/**
* 权限校验
* @param request 请求request对象
* @param response 响应对象
* @return 是否权限校验通过
*/</span>
<span class="kt">boolean</span> <span class="nf">auth</span><span class="o">(</span><span class="n">ServletRequest</span> <span class="n">request</span><span class="o">,</span> <span class="n">ServletResponse</span> <span class="n">response</span><span class="o">);</span>
<span class="cm">/**
* 权限校验失败后的处理逻辑
* @param request 请求对象
* @param response 响应对象
*/</span>
<span class="kt">void</span> <span class="nf">failedHandle</span><span class="o">(</span><span class="n">ServletRequest</span> <span class="n">request</span><span class="o">,</span> <span class="n">ServletResponse</span> <span class="n">response</span><span class="o">);</span>
<span class="cm">/**
* 是否需要鉴权,默认true
* @return 是否需要鉴权
*/</span>
<span class="k">default</span> <span class="kt">boolean</span> <span class="nf">required</span><span class="o">(){</span><span class="k">return</span> <span class="kc">true</span><span class="o">;}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>以下是一个项目中通过Shiro控制权限的例子,对于代理的请求,需要验证当前的请求是否已经登录过</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">AideShiroAuthentication</span> <span class="kd">implements</span> <span class="n">ServletGatewayAuthentication</span> <span class="o">{</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="n">OtsWebSessionManager</span> <span class="n">otsWebSessionManager</span><span class="o">;</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="n">RedisTemplate</span> <span class="n">redisTemplate</span><span class="o">;</span>
<span class="n">Logger</span> <span class="n">logger</span><span class="o">=</span> <span class="n">LoggerFactory</span><span class="o">.</span><span class="na">getLogger</span><span class="o">(</span><span class="n">AideShiroAuthentication</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
<span class="kd">public</span> <span class="nf">AideShiroAuthentication</span><span class="o">(</span><span class="n">OtsWebSessionManager</span> <span class="n">otsWebSessionManager</span><span class="o">,</span> <span class="n">RedisTemplate</span> <span class="n">redisTemplate</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">otsWebSessionManager</span> <span class="o">=</span> <span class="n">otsWebSessionManager</span><span class="o">;</span>
<span class="k">this</span><span class="o">.</span><span class="na">redisTemplate</span> <span class="o">=</span> <span class="n">redisTemplate</span><span class="o">;</span>
<span class="o">}</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">auth</span><span class="o">(</span><span class="n">ServletRequest</span> <span class="n">request</span><span class="o">,</span> <span class="n">ServletResponse</span> <span class="n">response</span><span class="o">)</span> <span class="o">{</span>
<span class="n">Serializable</span> <span class="n">sessionId</span> <span class="o">=</span> <span class="n">otsWebSessionManager</span><span class="o">.</span><span class="na">getShiroSessionId</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">sessionId</span><span class="o">!=</span><span class="kc">null</span><span class="o">){</span>
<span class="n">Object</span> <span class="n">object</span><span class="o">=</span> <span class="n">redisTemplate</span><span class="o">.</span><span class="na">opsForValue</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="n">MyRedisSessionDao</span><span class="o">.</span><span class="na">PREFIX</span> <span class="o">+</span> <span class="n">sessionId</span><span class="o">.</span><span class="na">toString</span><span class="o">());</span>
<span class="k">if</span> <span class="o">(</span><span class="n">object</span><span class="o">!=</span><span class="kc">null</span><span class="o">){</span>
<span class="n">Session</span> <span class="n">session</span> <span class="o">=</span> <span class="o">(</span><span class="n">Session</span><span class="o">)</span><span class="n">object</span><span class="o">;</span>
<span class="k">return</span> <span class="n">session</span><span class="o">!=</span><span class="kc">null</span><span class="o">&&</span><span class="n">session</span><span class="o">.</span><span class="na">getId</span><span class="o">()!=</span><span class="kc">null</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
<span class="o">}</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">failedHandle</span><span class="o">(</span><span class="n">ServletRequest</span> <span class="n">request</span><span class="o">,</span> <span class="n">ServletResponse</span> <span class="n">response</span><span class="o">)</span> <span class="o">{</span>
<span class="n">logger</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"权限校验失败"</span><span class="o">);</span>
<span class="n">response</span><span class="o">.</span><span class="na">setCharacterEncoding</span><span class="o">(</span><span class="s">"UTF-8"</span><span class="o">);</span>
<span class="n">response</span><span class="o">.</span><span class="na">setContentType</span><span class="o">(</span><span class="s">"application/json; charset=utf-8"</span><span class="o">);</span>
<span class="n">RestResult</span><span class="o"><</span><span class="n">String</span><span class="o">></span> <span class="n">result</span> <span class="o">=</span> <span class="k">new</span> <span class="n">RestResult</span><span class="o"><>();</span>
<span class="n">result</span><span class="o">.</span><span class="na">setErrCode</span><span class="o">(</span><span class="n">BusinessErrorCode</span><span class="o">.</span><span class="na">NO_CURRENT_LOGIN_USER</span><span class="o">.</span><span class="na">getCode</span><span class="o">());</span>
<span class="n">result</span><span class="o">.</span><span class="na">setData</span><span class="o">(</span><span class="n">BusinessErrorCode</span><span class="o">.</span><span class="na">NO_CURRENT_LOGIN_USER</span><span class="o">.</span><span class="na">getMessage</span><span class="o">());</span>
<span class="k">try</span> <span class="o">(</span><span class="n">PrintWriter</span> <span class="n">out</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="na">getWriter</span><span class="o">())</span> <span class="o">{</span>
<span class="n">out</span><span class="o">.</span><span class="na">append</span><span class="o">(</span><span class="n">JSON</span><span class="o">.</span><span class="na">toJSONString</span><span class="o">(</span><span class="n">result</span><span class="o">));</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">IOException</span> <span class="n">e2</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>通过自定义权限接口后,需要注入到Spring的容器中(<strong>注意</strong>:需要添加<code class="highlighter-rouge">@Primary</code>注解),代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Configuration</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">AuthConfig</span> <span class="o">{</span>
<span class="nd">@Bean</span>
<span class="nd">@Primary</span>
<span class="kd">public</span> <span class="n">AideShiroAuthentication</span> <span class="nf">aideServletGatewayAuthentication</span><span class="o">(</span><span class="nd">@Autowired</span> <span class="n">OtsWebSessionManager</span> <span class="n">otsWebSessionManager</span><span class="o">,</span><span class="nd">@Autowired</span> <span class="n">RedisTemplate</span> <span class="n">redisTemplate</span><span class="o">){</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">AideShiroAuthentication</span><span class="o">(</span><span class="n">otsWebSessionManager</span><span class="o">,</span><span class="n">redisTemplate</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>肖玉民背景概述Spring Boot自定义starter必知必会条件2020-12-10T00:00:00+08:002020-12-10T00:00:00+08:00https://xiaoym.gitee.io/2020/12/10/spring-boot-self-starter<h2 id="前言">前言</h2>
<p>在目前的Spring Boot框架中,不管是Spring Boot官方还是非官方,都提供了非常多的starter系列组件,助力开发者在企业应用中的开发,提升研发人员的工作效率,Spring Boot框架提出的约定大于配置的规则,确实帮助开发者简化了以前Spring MVC时代的很多繁杂的配置。让开发者用起来也是非常爽的。</p>
<p>尽管Spring Boot或者一些开源组件已经帮助我们提供了非常多的starter组件,在满足日常的开发中,已经完全没有问题了。但有时候因为需求的可变性,导致企业架构也会随着调整,那么在Spring Boot框架中,官方或开源的第三方starter肯定不能满足企业内部研发人员的要求,这时候就需要开发者自定义企业内部的starter了。</p>
<p>企业或个人自定义Spring Boot的starter组件主要从哪些方面来入手呢,或者什么时候需要自定义starter组件?我个人认为主要有以下几个方面:</p>
<ul>
<li>规范企业内部编码流程,统一各个技术中间件的代码规范</li>
<li>减少不同类型中间件的使用成本,提升研发人员的研发工作效率</li>
<li>减少冗余代码的使用,统一封装,统一管理。</li>
<li>屏蔽中间件底层细节,暴露配置属性及方法,减少学习使用成本</li>
<li>可能还有更多?</li>
</ul>
<p>本篇博客结合自身的开发经验以及目前Spring Boot如何配置元数据的官方介绍文档进行结合,进行综合阐述。</p>
<p>Spring Boot官方元数据文档地址:<a href="https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-configuration-metadata.html">https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-configuration-metadata.html</a></p>
<p>封装Spring Boot的starter范围可以是一组规范的业务方法,也可以是通用的中间件底层。开发者通过封装,一定程度上也能起到规范企业编码的作用,同时也能组合复用公共业务逻辑。</p>
<p>那么我们在自定义Spring Boot框架的starter组件时,我们需要准备什么呢?</p>
<p>我认为主要包含以下几个方面:</p>
<ul>
<li>自定义starter的作用</li>
<li>命名规范</li>
<li>理解Maven或者Gradle依赖包管理的jar包引用传递机制</li>
<li>理解Spring Boot框架中基于Java代码的Configuration配置</li>
<li>理解Spring Boot框架自动装载的过程</li>
<li>学会利用Spring Boot提供的<code class="highlighter-rouge">@Conditional</code>系列条件注入充分发挥Spring Boot的优点</li>
<li>学会如何配置自定义starter组件时对外的属性注释配置,可以参考<a href="https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-configuration-metadata.html">官方文档</a></li>
</ul>
<h2 id="自定义starter的作用">自定义starter的作用</h2>
<p>我们在自定义starter组件之前,开发者首先需要想清楚,这个starter组件能带来什么,简化开发?或者复用组件的封装供其他同事使用,不写重复代码等等,这些都是需要思考清楚的。</p>
<p>自定义starter的场景很多,例如:</p>
<ul>
<li>项目中发送短信对接了不同的云服务商,那么可以封装一个短信的starter,屏蔽对接的细节,开发者只需要配置相应的厂商配置信息就可以使用该服务商发送短信了</li>
<li>OSS存储对接不同的云服务商,例如阿里云、七牛云、腾讯云等等</li>
<li>企业内部中间件封装使用,简化开发配置</li>
<li>more…</li>
</ul>
<p>根据笔者的经验,我认为自定义的starter的作用无外乎以下几个方面:</p>
<ul>
<li>充分利用Spring的特性,容器/依赖注入特性,将核心的类组件注入容器中,方便开发者通过注入直接获取拿来使用</li>
<li>通过属性初始化中间件的流程,屏蔽具体的细节</li>
<li>….</li>
</ul>
<h2 id="starter命名规范">starter命名规范</h2>
<p>根据Spring Boot的官方要求,如果是开发者指定第三方的starter组件,那么命名规范是<code class="highlighter-rouge">yourname-spring-boot-starter</code></p>
<p>拿<a href="https://xiaoym.gitee.io/knife4j/">Knife4j</a>举例说明如下:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><dependency></span>
<span class="nt"><groupId></span>com.github.xiaoymin<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>knife4j-spring-boot-starter<span class="nt"></artifactId></span>
<span class="c"><!--在引用时请在maven中央仓库搜索2.X最新版本号--></span>
<span class="nt"><version></span>2.0.8<span class="nt"></version></span>
<span class="nt"></dependency></span>
</code></pre></div></div>
<p>而Spring Boot官方维护发布的starter名称规范则是:<code class="highlighter-rouge">spring-boot-starter-name</code></p>
<p>例如我们引用最多的web组件,引用maven配置如下:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-starter-web<span class="nt"></artifactId></span>
<span class="nt"></dependency></span>
</code></pre></div></div>
<h2 id="jar包引用传递依赖机制">jar包引用传递依赖机制</h2>
<p>这是自定义封装Spring Boot的starter的前提条件,Gradle笔者并未使用过,这里仅以Maven为例进行阐述说明!</p>
<p>通常我们在封装一个SDK的jar包时,该jar包可能需要引用到第三方的jar包作为依赖包来辅助我们完成对该jar包的封装,但是我们在引用的时候是有讲究的。</p>
<p>针对Spring Boot的自定义starter说到底也是一个jar包,既然是jar包必然会用到第三方的jar(ps:全部都是你写的代码除外),那么我们应该如何明确在starter中的jar包的依赖传递,我认为主要有以下方面:</p>
<ul>
<li>作为第三方组件使用jar包时,明确第三方组件的版本</li>
<li>作为编译期间的包,需要修改默认的scope范围值,仅仅在编译期间生效,最终打包后引用不传递</li>
<li>自定义封装starter必须引用Spring Boot官方提供的</li>
</ul>
<p>在定义Spring Boot的第三方starter时,主要用到Maven管理jar包中的两种依赖隔离方式(均可以使用),分别如下:</p>
<ul>
<li>明确使用<code class="highlighter-rouge"><optional>true></optional></code>属性来强指定jar包不传递</li>
<li>使用<code class="highlighter-rouge"><scope>provided</scope></code>仅仅在编译期间有效,jar包依赖性不传递</li>
</ul>
<p>一般我们在自定义Spring Boot的starter组件时,都需要引用Spring Boot提供给开发者的依赖包,如下:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-autoconfigure<span class="nt"></artifactId></span>
<span class="nt"><version></span>2.3.0.RELEASE<span class="nt"></version></span>
<span class="nt"><scope></span>provided<span class="nt"></scope></span>
<span class="nt"></dependency></span>
</code></pre></div></div>
<p>当然,你也可以使用<code class="highlighter-rouge">optional</code>模式,如下:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-autoconfigure<span class="nt"></artifactId></span>
<span class="nt"><version></span>2.3.0.RELEASE<span class="nt"></version></span>
<span class="nt"><optional></span>true<span class="nt"></optional></span>
<span class="nt"></dependency></span>
</code></pre></div></div>
<h2 id="java代码方式的configuration">Java代码方式的Configuration</h2>
<p>基于Java编码的方式配置Spring的Bean已经成了目前的主流,这主要也是得益于Spring Boot框架的流行!</p>
<p>在Spring MVC框架流行的时候,开发人员一般都是通过配置XML文件来注入实体Bean的</p>
<p>而通过java编码的方式注入Bean的前提是<code class="highlighter-rouge">@Configuration</code>注解加在一个配置Java实体类上即可,示例如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Configuration</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">MyAutoConfiguration</span><span class="o">{</span>
<span class="c1">//do others...</span>
<span class="o">}</span>
</code></pre></div></div>
<h2 id="spring-boot框架的自动装载">Spring Boot框架的自动装载</h2>
<p>对于Spring Boot框架自定义的starter组件来说,提供的使用方式而言,我认为目前主要有3种方式,这个主要看封装starter组件的作者如何开放来定</p>
<h3 id="手工import导入">手工<code class="highlighter-rouge">@Import</code>导入</h3>
<p>第一种情况:使用者使用<code class="highlighter-rouge">@Import</code>注解将封装的starter组件的Java编码Configuration配置文件进行导入</p>
<p>假设目前封装的一个简单的Configuration配置如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Configuration</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">DemoAuthConfiguration</span> <span class="o">{</span>
<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="n">DemoClient</span> <span class="nf">demoClient</span><span class="o">(){</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">DemoClient</span><span class="o">();</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>开发者通过<code class="highlighter-rouge">DemoAutoConfiguration.java</code>向Spring的容器中注入了一个<code class="highlighter-rouge">DemoClient</code>的实体Bean,由于隶属于不同的package包路径,自定义的starter组件包路径是:<code class="highlighter-rouge">com.demo.spring</code></p>
<p>而开发者的项目主目录包路径是:<code class="highlighter-rouge">com.test</code>,所以Spring Boot框架默认是不会加载该配置的,此时,如果开发者要在Spring的容器中获取<code class="highlighter-rouge">DemoClient</code>的实体Bean应该怎么办呢?使用者应该在自己的主配置中使用<code class="highlighter-rouge">@Import</code>注解将该配置导入进来交给Spring容器初始化时进行创建,示例如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Import</span><span class="o">(</span><span class="n">DemoAutoConfiguration</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="nd">@SpringBootApplication</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">DemoDemoApplication</span> <span class="o">{</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="n">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">){</span>
<span class="n">SpringApplication</span><span class="o">.</span><span class="na">run</span><span class="o">(</span><span class="n">DemoDemoApplication</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="n">args</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<h3 id="提供便于记忆的注解enablexxx">提供便于记忆的注解<code class="highlighter-rouge">@EnableXXX</code></h3>
<p><code class="highlighter-rouge">@Enablexxx</code>系列注解相信开发者并不陌生,比如我们要使用Spring Boot的定时任务功能,我们会在启动入口引入<code class="highlighter-rouge">@EnableScheduling</code>注解,我们使用Springfox的Swagger组件,我们会引入<code class="highlighter-rouge">@EnableSwagger2</code>注解</p>
<p>其实这种方式只是为了让开发者能够更加方便的记忆,一个<code class="highlighter-rouge">@Enablexxx</code>系列注解,其所代表的功能特点也基本符合该starter组件,是在上面手工通过<code class="highlighter-rouge">@Import</code>注解的升级版本。</p>
<p>毕竟<code class="highlighter-rouge">Enable</code>单词所代表的含义是<strong>启用</strong>,这有利于开发者记忆</p>
<p>继续通过上面第一种的示例进行改在,此时,我们可以提供<code class="highlighter-rouge">@EnableDemoClient</code>注解,代码示例如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Documented</span>
<span class="nd">@Retention</span><span class="o">(</span><span class="n">RetentionPolicy</span><span class="o">.</span><span class="na">RUNTIME</span><span class="o">)</span>
<span class="nd">@Target</span><span class="o">(</span><span class="n">ElementType</span><span class="o">.</span><span class="na">TYPE</span><span class="o">)</span>
<span class="nd">@Import</span><span class="o">(</span><span class="n">DemoAutoConfiguration</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="kd">public</span> <span class="nd">@interface</span> <span class="n">EnableDemoClient</span> <span class="o">{</span>
<span class="o">}</span>
</code></pre></div></div>
<p>大家应该也看到了,我们在该<code class="highlighter-rouge">@EnableDemoClient</code>注解中,使用了<code class="highlighter-rouge">@Import</code>注解的方式导入了<code class="highlighter-rouge">DemoAutoConfiguration</code>配置</p>
<p>此时,我们在项目中可以使用<code class="highlighter-rouge">@EnableDemoClient</code>注解了,代码示例如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@EnableDemoClient</span>
<span class="nd">@SpringBootApplication</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">DemoDemoApplication</span> <span class="o">{</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="n">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">){</span>
<span class="n">SpringApplication</span><span class="o">.</span><span class="na">run</span><span class="o">(</span><span class="n">DemoDemoApplication</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="n">args</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>当然,<code class="highlighter-rouge">@Enable</code>这种注解作用不仅仅局限于此,还可以在该注解上定义外部的配置属性,通过配置该注解的方式达到最终初始化的目的。</p>
<h3 id="自动装载">自动装载</h3>
<p>自动装载是Spring Boot的一重大特点,开发者通过配置文件的方式即可默认加载第三方的starter配置,非常的方便,是上面两种方式的升级版</p>
<p>在之前的基础上,如果开发者希望在Maven的pom.xml工程中引入了该组件,就可以使用<code class="highlighter-rouge">DemoClient</code>类,那么此时我们应该怎么做呢?</p>
<p>我们需要在工程中创建<code class="highlighter-rouge">spring.factories</code>文件,文件目录:<code class="highlighter-rouge">src/resources/META-INF/spring.factories</code></p>
<p>在<code class="highlighter-rouge">spring.factories</code>文件中,配置开发者自定义的configuration类,如下:</p>
<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">org.springframework.boot.autoconfigure.EnableAutoConfiguration</span><span class="p">=</span><span class="s">com.demo.spring.DemoAutoConfiguration</span>
</code></pre></div></div>
<p>配置好后,此时再打包我们自定义的starter组件,Spring Boot框架默认会自动装载该配置类,我们在业务代码中也就可以直接使用了</p>
<p>我们可以在<code class="highlighter-rouge">SpringApplication.java</code>源码中看到Spring Boot初始化获取该类列表的过程</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="o"><</span><span class="n">T</span><span class="o">></span> <span class="n">Collection</span><span class="o"><</span><span class="n">T</span><span class="o">></span> <span class="nf">getSpringFactoriesInstances</span><span class="o">(</span><span class="n">Class</span><span class="o"><</span><span class="n">T</span><span class="o">></span> <span class="n">type</span><span class="o">,</span> <span class="n">Class</span><span class="o"><?>[]</span> <span class="n">parameterTypes</span><span class="o">,</span> <span class="n">Object</span><span class="o">...</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
<span class="n">ClassLoader</span> <span class="n">classLoader</span> <span class="o">=</span> <span class="n">getClassLoader</span><span class="o">();</span>
<span class="c1">// Use names and ensure unique to protect against duplicates</span>
<span class="n">Set</span><span class="o"><</span><span class="n">String</span><span class="o">></span> <span class="n">names</span> <span class="o">=</span> <span class="k">new</span> <span class="n">LinkedHashSet</span><span class="o"><>(</span><span class="n">SpringFactoriesLoader</span><span class="o">.</span><span class="na">loadFactoryNames</span><span class="o">(</span><span class="n">type</span><span class="o">,</span> <span class="n">classLoader</span><span class="o">));</span>
<span class="n">List</span><span class="o"><</span><span class="n">T</span><span class="o">></span> <span class="n">instances</span> <span class="o">=</span> <span class="n">createSpringFactoriesInstances</span><span class="o">(</span><span class="n">type</span><span class="o">,</span> <span class="n">parameterTypes</span><span class="o">,</span> <span class="n">classLoader</span><span class="o">,</span> <span class="n">args</span><span class="o">,</span> <span class="n">names</span><span class="o">);</span>
<span class="n">AnnotationAwareOrderComparator</span><span class="o">.</span><span class="na">sort</span><span class="o">(</span><span class="n">instances</span><span class="o">);</span>
<span class="k">return</span> <span class="n">instances</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>上述方法中的<code class="highlighter-rouge">SpringFactoriesLoader.loadFactoryNames</code>方法如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">static</span> <span class="n">List</span><span class="o"><</span><span class="n">String</span><span class="o">></span> <span class="nf">loadFactoryNames</span><span class="o">(</span><span class="n">Class</span><span class="o"><?></span> <span class="n">factoryType</span><span class="o">,</span> <span class="nd">@Nullable</span> <span class="n">ClassLoader</span> <span class="n">classLoader</span><span class="o">)</span> <span class="o">{</span>
<span class="n">String</span> <span class="n">factoryTypeName</span> <span class="o">=</span> <span class="n">factoryType</span><span class="o">.</span><span class="na">getName</span><span class="o">();</span>
<span class="k">return</span> <span class="nf">loadSpringFactories</span><span class="o">(</span><span class="n">classLoader</span><span class="o">).</span><span class="na">getOrDefault</span><span class="o">(</span><span class="n">factoryTypeName</span><span class="o">,</span> <span class="n">Collections</span><span class="o">.</span><span class="na">emptyList</span><span class="o">());</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="n">Map</span><span class="o"><</span><span class="n">String</span><span class="o">,</span> <span class="n">List</span><span class="o"><</span><span class="n">String</span><span class="o">>></span> <span class="nf">loadSpringFactories</span><span class="o">(</span><span class="nd">@Nullable</span> <span class="n">ClassLoader</span> <span class="n">classLoader</span><span class="o">)</span> <span class="o">{</span>
<span class="n">MultiValueMap</span><span class="o"><</span><span class="n">String</span><span class="o">,</span> <span class="n">String</span><span class="o">></span> <span class="n">result</span> <span class="o">=</span> <span class="n">cache</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">classLoader</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">result</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">result</span><span class="o">;</span>
<span class="o">}</span>
<span class="k">try</span> <span class="o">{</span>
<span class="c1">//加载META-INF/spring.factories配置,创建MultiValueMap集合放到该集合中</span>
<span class="n">Enumeration</span><span class="o"><</span><span class="n">URL</span><span class="o">></span> <span class="n">urls</span> <span class="o">=</span> <span class="o">(</span><span class="n">classLoader</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">?</span>
<span class="n">classLoader</span><span class="o">.</span><span class="na">getResources</span><span class="o">(</span><span class="n">FACTORIES_RESOURCE_LOCATION</span><span class="o">)</span> <span class="o">:</span>
<span class="n">ClassLoader</span><span class="o">.</span><span class="na">getSystemResources</span><span class="o">(</span><span class="n">FACTORIES_RESOURCE_LOCATION</span><span class="o">));</span>
<span class="n">result</span> <span class="o">=</span> <span class="k">new</span> <span class="n">LinkedMultiValueMap</span><span class="o"><>();</span>
<span class="k">while</span> <span class="o">(</span><span class="n">urls</span><span class="o">.</span><span class="na">hasMoreElements</span><span class="o">())</span> <span class="o">{</span>
<span class="n">URL</span> <span class="n">url</span> <span class="o">=</span> <span class="n">urls</span><span class="o">.</span><span class="na">nextElement</span><span class="o">();</span>
<span class="n">UrlResource</span> <span class="n">resource</span> <span class="o">=</span> <span class="k">new</span> <span class="n">UrlResource</span><span class="o">(</span><span class="n">url</span><span class="o">);</span>
<span class="n">Properties</span> <span class="n">properties</span> <span class="o">=</span> <span class="n">PropertiesLoaderUtils</span><span class="o">.</span><span class="na">loadProperties</span><span class="o">(</span><span class="n">resource</span><span class="o">);</span>
<span class="k">for</span> <span class="o">(</span><span class="n">Map</span><span class="o">.</span><span class="na">Entry</span><span class="o"><?,</span> <span class="o">?></span> <span class="n">entry</span> <span class="o">:</span> <span class="n">properties</span><span class="o">.</span><span class="na">entrySet</span><span class="o">())</span> <span class="o">{</span>
<span class="n">String</span> <span class="n">factoryTypeName</span> <span class="o">=</span> <span class="o">((</span><span class="n">String</span><span class="o">)</span> <span class="n">entry</span><span class="o">.</span><span class="na">getKey</span><span class="o">()).</span><span class="na">trim</span><span class="o">();</span>
<span class="k">for</span> <span class="o">(</span><span class="n">String</span> <span class="n">factoryImplementationName</span> <span class="o">:</span> <span class="n">StringUtils</span><span class="o">.</span><span class="na">commaDelimitedListToStringArray</span><span class="o">((</span><span class="n">String</span><span class="o">)</span> <span class="n">entry</span><span class="o">.</span><span class="na">getValue</span><span class="o">()))</span> <span class="o">{</span>
<span class="n">result</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">factoryTypeName</span><span class="o">,</span> <span class="n">factoryImplementationName</span><span class="o">.</span><span class="na">trim</span><span class="o">());</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="n">cache</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="n">classLoader</span><span class="o">,</span> <span class="n">result</span><span class="o">);</span>
<span class="k">return</span> <span class="n">result</span><span class="o">;</span>
<span class="o">}</span>
<span class="k">catch</span> <span class="o">(</span><span class="n">IOException</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalArgumentException</span><span class="o">(</span><span class="s">"Unable to load factories from location ["</span> <span class="o">+</span>
<span class="n">FACTORIES_RESOURCE_LOCATION</span> <span class="o">+</span> <span class="s">"]"</span><span class="o">,</span> <span class="n">ex</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<h2 id="充分利用spring-boot提供的conditional条件注入组件">充分利用Spring Boot提供的<code class="highlighter-rouge">@Conditional</code>条件注入组件</h2>
<p>通过上面的文章介绍,为Spring Boot框架制定一个简单的starter组件相信已经不在话下。但是,这才仅仅开始而已。</p>
<p>在上面介绍的自动装载过程中,开发者是否会存在疑问?</p>
<blockquote>
<p>当我们在pom.xml引入我们自定义的starter组件后,Spring Boot框架默认会将该组件直接注入到Spring的容器中,这种方式虽然在使用上并没有什么问题,但当我们封装给第三方使用时,这种方式往往会存在冲突,假设开发者自定义的starter组件中包含了向容器中注入Filter等过滤器,那么该过滤器直接生效,会全范围影响整个应用程序.这在实际开发中是不允许的!</p>
<p>那么应该怎么办呢?此时,我们就需要充分利用Spring Boot框架为开发者提供的<code class="highlighter-rouge">@Conditional</code>系列条件注入了</p>
</blockquote>
<p>条件注入顾名思义,就是只有使用者满足了组件规定的条件时,组件才会向Spring容器中进行注入Bean或者初始化的操作.这种方式也是将选择权直接交给使用者进行选择,减少非必要的组件冲突,是在Spring Boot自定义starter组件中必不可少的一环。</p>
<p>条件注入通常也配合属性类一起来进行使用,提供配置属性选项也是方便使用者在Spring Boot的配置文件<code class="highlighter-rouge">application.yml</code>或者<code class="highlighter-rouge">application.properties</code>进行配置开启操作,例如我们常见的配置操作如下:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">server</span><span class="pi">:</span>
<span class="na">port</span><span class="pi">:</span> <span class="s">18568</span>
<span class="na">servlet</span><span class="pi">:</span>
<span class="na">context-path</span><span class="pi">:</span> <span class="s">/test</span>
</code></pre></div></div>
<p>为Spring Boot的程序指定启动端口号和<code class="highlighter-rouge">context-path</code>属性.</p>
<p>我们继续以上面示例中的<code class="highlighter-rouge">DemoClient</code>为例进行阐述</p>
<blockquote>
<p>假设我们的<code class="highlighter-rouge">DemoClient</code>是对接外部API接口的封装组件,该组件规定访问外部API时需要提供<code class="highlighter-rouge">appid</code>和<code class="highlighter-rouge">secret</code>,根据appid及secret获取token,最后根据token才能调用API获取接口数据,</p>
</blockquote>
<p>那么,此时,我们的<code class="highlighter-rouge">DemoClient</code>的部分模拟接口代码可能会如下面示例:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">DemoClient</span> <span class="o">{</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="n">String</span> <span class="n">appid</span><span class="o">;</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="n">String</span> <span class="n">secret</span><span class="o">;</span>
<span class="kd">public</span> <span class="nf">DemoClient</span><span class="o">(</span><span class="n">String</span> <span class="n">appid</span><span class="o">,</span> <span class="n">String</span> <span class="n">secret</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">appid</span> <span class="o">=</span> <span class="n">appid</span><span class="o">;</span>
<span class="k">this</span><span class="o">.</span><span class="na">secret</span> <span class="o">=</span> <span class="n">secret</span><span class="o">;</span>
<span class="o">}</span>
<span class="cm">/**
* 获取资源
* @return
*/</span>
<span class="kd">public</span> <span class="n">String</span> <span class="nf">listResources</span><span class="o">(){</span>
<span class="c1">//获取token</span>
<span class="n">String</span> <span class="n">token</span><span class="o">=</span><span class="n">getToken</span><span class="o">();</span>
<span class="c1">//根据Token请求数据</span>
<span class="k">return</span> <span class="n">UUID</span><span class="o">.</span><span class="na">randomUUID</span><span class="o">().</span><span class="na">toString</span><span class="o">();</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="n">String</span> <span class="nf">getToken</span><span class="o">()</span> <span class="o">{</span>
<span class="c1">//根据appid & secret获取第三方API接口token</span>
<span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在上面的代码示例中,如果开发者要使用<code class="highlighter-rouge">DemoClient</code>的方法调用第三方的接口资源,那么需要传递<code class="highlighter-rouge">appid</code>及<code class="highlighter-rouge">secret</code>参数才能构造实体类,又考虑到我们需要利用Spring Boot的条件注入,只有开发者配置了开启操作,才能在Spring容器中使用<code class="highlighter-rouge">DemoClient</code>的方法。</p>
<p>那么此时,我们可以给该starter组件抽象一个<code class="highlighter-rouge">DemoProperties</code>的外部配置类来交给使用者在配置文件中进行配置开启操作,代码示例如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@ConfigurationProperties</span><span class="o">(</span><span class="n">prefix</span> <span class="o">=</span> <span class="s">"demo"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">DemoProperties</span> <span class="o">{</span>
<span class="cm">/**
* 是否启用
*/</span>
<span class="kd">private</span> <span class="kt">boolean</span> <span class="n">enable</span><span class="o">=</span><span class="kc">false</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">appid</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">secret</span><span class="o">;</span>
<span class="c1">//getter and setter...</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在配置类属性中,我们使用到了<code class="highlighter-rouge">@ConfigurationProperties</code>注解,并配置了<code class="highlighter-rouge">prefix</code>前缀参数,配置前缀也是自定义starter组件中所必须的,这约束了命名空间。一般是结合自身的业务以及starter组件所代表的功能含义进行命名<code class="highlighter-rouge">prefix</code>,有助于开发使用者记忆。</p>
<p>此时,我们的<code class="highlighter-rouge">DemoAutoConfiguration.java</code>配置类进行了调整,代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Configuration</span>
<span class="nd">@EnableConfigurationProperties</span><span class="o">(</span><span class="n">DemoProperties</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="nd">@ConditionalOnProperty</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"demo.enable"</span><span class="o">,</span><span class="n">havingValue</span> <span class="o">=</span> <span class="s">"true"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">DemoAutoConfiguration</span> <span class="o">{</span>
<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="n">DemoClient</span> <span class="nf">demoClient</span><span class="o">(</span><span class="n">DemoProperties</span> <span class="n">demoProperties</span><span class="o">){</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">DemoClient</span><span class="o">(</span><span class="n">demoProperties</span><span class="o">.</span><span class="na">getAppid</span><span class="o">(),</span> <span class="n">demoProperties</span><span class="o">.</span><span class="na">getSecret</span><span class="o">());</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>和上面的配置类进行比较不难发现,此处我们又多用了两个注解:</p>
<ul>
<li><code class="highlighter-rouge">@EnableConfigurationProperties</code>:该注解是我们自定义指定Proerpty实体类时,必须启用的注解,和实体类中的<code class="highlighter-rouge">@ConfigurationProperties</code>注解配合一起使用</li>
<li><code class="highlighter-rouge">@ConditionalOnProperty</code>:Spring Boot框架中条件注入的一种,代码根据配置的属性进行条件判断注入,此处我们配置了只有当<code class="highlighter-rouge">demo.enable=true</code>时,<code class="highlighter-rouge">DemoAutoConfiguration</code>配置类才会加载,向Spring容器中注入<code class="highlighter-rouge">DemoClient</code>的实体Bean</li>
</ul>
<p>当自定义starter组件封装到这一步时,基本已经快完结了,开发者可以通过在Spring Boot的配置文件中进行配置,来开启是否使用<code class="highlighter-rouge">DemoClient</code>组件</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">demo</span><span class="pi">:</span>
<span class="c1"># 通过配置该属性的true 或者false ,来开启组件的使用</span>
<span class="na">enable</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">appid</span><span class="pi">:</span> <span class="s">xxx</span>
<span class="na">secret</span><span class="pi">:</span> <span class="s">xxxx</span>
</code></pre></div></div>
<h2 id="属性元数据配置">属性元数据配置</h2>
<p>通过上面的配置,我们已经能够自定义一个Spring Boot框架的starter组件了,但是对于使用者来说,封装该starter组件的开发者还尚有最后一步需要完成,那就是给属性类提供元数据注释,提供元数据注释也是为了让使用者在配置<code class="highlighter-rouge">application.yml</code>属性时,通过IDEA等编辑器能够给出提示,这对使用者而已是大有裨益的,因为每一个属性都会有相应的注释供开发者进行参考。例如Knife4j组件提供的元数据注释如下图:</p>
<p><img src="https://oscimg.oschina.net/oscnet/up-91e85f9a3926723e50f6b05ae548d76b291.png" alt="" /></p>
<p>那么我们在制定starter组件时,如何给属性类提供元数据注释呢?目前主要有两种方式:</p>
<h3 id="引入spring-boot-configuration-processor自动注释">引入<code class="highlighter-rouge">spring-boot-configuration-processor</code>自动注释</h3>
<p>我们可以在自定义是starter组件中引入该组件,依赖如下:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-configuration-processor<span class="nt"></artifactId></span>
<span class="nt"><version></span>2.3.0.RELEASE<span class="nt"></version></span>
<span class="nt"><optional></span>true<span class="nt"></optional></span>
<span class="nt"></dependency></span>
</code></pre></div></div>
<p>引入该组件后,此时,我们只需要在我们的Java属性类中给每一个属性使用标准的javadoc进行注释即可,如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@ConfigurationProperties</span><span class="o">(</span><span class="n">prefix</span> <span class="o">=</span> <span class="s">"demo"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">DemoProperties</span> <span class="o">{</span>
<span class="cm">/**
* 是否启用
*/</span>
<span class="kd">private</span> <span class="kt">boolean</span> <span class="n">enable</span><span class="o">=</span><span class="kc">false</span><span class="o">;</span>
<span class="cm">/**
* 第三方appid
*/</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">appid</span><span class="o">;</span>
<span class="cm">/**
* 第三方secret
*/</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">secret</span><span class="o">;</span>
<span class="c1">//getter and setter...</span>
<span class="o">}</span>
</code></pre></div></div>
<p>最终在使用时,就会出现提示,如下图:</p>
<p><img src="/assets/images/springboot/self/authMetaData.png" alt="" /></p>
<p>这种方式如果属性类不是太多的情况下,开发者可以使用,很方便</p>
<h3 id="手工编写spring-configuration-meatadatajson文件">手工编写<code class="highlighter-rouge">spring-configuration-meatadata.json</code>文件</h3>
<p><code class="highlighter-rouge">spring-boot-configuration-processor</code>组件最终在打包生成starter的jar包时,也是帮助我们自动生成了<code class="highlighter-rouge">spring-configuration-metadata.json</code>文件,该文件和上面提到的<code class="highlighter-rouge">spring.factories</code>是同级目录</p>
<p>手工编写<code class="highlighter-rouge">spring-configuration-metadata.json</code>也是我推荐的方式,因为不仅仅是每个属性的注释,有时候我们还可以用更多的属性配置以便使用者使用。</p>
<p>结果如下:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="s2">"groups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"demo"</span><span class="p">,</span><span class="w">
</span><span class="s2">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"com.demo.spring.DemoProperties"</span><span class="p">,</span><span class="w">
</span><span class="s2">"sourceType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"com.demo.spring.DemoProperties"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="s2">"properties"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"demo.appid"</span><span class="p">,</span><span class="w">
</span><span class="s2">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"java.lang.String"</span><span class="p">,</span><span class="w">
</span><span class="s2">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"第三方appid"</span><span class="p">,</span><span class="w">
</span><span class="s2">"sourceType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"com.demo.spring.DemoProperties"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"demo.enable"</span><span class="p">,</span><span class="w">
</span><span class="s2">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"java.lang.Boolean"</span><span class="p">,</span><span class="w">
</span><span class="s2">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"是否启用"</span><span class="p">,</span><span class="w">
</span><span class="s2">"sourceType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"com.demo.spring.DemoProperties"</span><span class="p">,</span><span class="w">
</span><span class="s2">"defaultValue"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"demo.secret"</span><span class="p">,</span><span class="w">
</span><span class="s2">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"java.lang.String"</span><span class="p">,</span><span class="w">
</span><span class="s2">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"第三方secret"</span><span class="p">,</span><span class="w">
</span><span class="s2">"sourceType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"com.demo.spring.DemoProperties"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="s2">"hints"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>我们主要使用到的属性有3个:<code class="highlighter-rouge">groups</code>、<code class="highlighter-rouge">properties</code>、<code class="highlighter-rouge">hints</code></p>
<p><strong>groups</strong></p>
<p>字面意思分组,按我的理解即当我们使用的实体时,配置的<code class="highlighter-rouge">prefix</code>即代表该group,例如上面我们为<code class="highlighter-rouge">DemoProperties</code>配置了prefix的前缀是<code class="highlighter-rouge">demo</code>,那么分组这里可以设置为<code class="highlighter-rouge">demo</code>,当然如果<code class="highlighter-rouge">DemoProperties</code>类中包含的属性是一个第三方类,假设如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">DemoProperties</span><span class="o">{</span>
<span class="kd">private</span> <span class="n">OtherProperties</span> <span class="n">other</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>那么我们可以在groups属性中配置一个名为<code class="highlighter-rouge">demo.other</code>的分组名称</p>
<p>其包含的属性如下:</p>
<table>
<thead>
<tr>
<th>属性名称</th>
<th>类型</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>name</td>
<td>String</td>
<td>分组名称,可以理解为<code class="highlighter-rouge">prefix</code></td>
</tr>
<tr>
<td>type</td>
<td>String</td>
<td>组数据类名</td>
</tr>
<tr>
<td>description</td>
<td>String</td>
<td>分组简单的描述,可以省略</td>
</tr>
<tr>
<td>sourceType</td>
<td>String</td>
<td>组数据源类名,同type,如果源类型未知,可以忽略该属性</td>
</tr>
<tr>
<td>sourceMethod</td>
<td>String</td>
<td>组方法的名称,(例如,带<code class="highlighter-rouge">@ConfigurationProperties</code>注解的<code class="highlighter-rouge">@Bean</code>方法的名称)。 如果源方法未知,则可以省略。</td>
</tr>
</tbody>
</table>
<p><strong>properties</strong></p>
<p>顾名思义,就是我们实体类每个属性的配置,有多少属性需要添加元数据注释说明,就需要在该数组下全部添加,需要注意的是配置name时需要配置全路径,例如:<code class="highlighter-rouge">demo.enable</code>等</p>
<p>其包含的属性如下:</p>
<table>
<thead>
<tr>
<th>属性名称</th>
<th>类型</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>name</td>
<td>String</td>
<td>属性名称</td>
</tr>
<tr>
<td>type</td>
<td>String</td>
<td>属性类型</td>
</tr>
<tr>
<td>description</td>
<td>String</td>
<td>属性的简介说明</td>
</tr>
<tr>
<td>sourceType</td>
<td>String</td>
<td>该属性归属于那个类型</td>
</tr>
<tr>
<td>defaultValue</td>
<td>Object</td>
<td>该属性默认值</td>
</tr>
<tr>
<td>deprecation</td>
<td>Deprecation</td>
<td>用于指定该属性是否过时</td>
</tr>
</tbody>
</table>
<p>过时选项<code class="highlighter-rouge">Deprecation</code>包含以下几个属性:</p>
<table>
<thead>
<tr>
<th>名称</th>
<th>类型</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>level</td>
<td>String</td>
<td>过时的级别,可以指定<code class="highlighter-rouge">warning</code>或者<code class="highlighter-rouge">error</code>,当指定为<code class="highlighter-rouge">warning</code>时,代表该属性还可用,而指定<code class="highlighter-rouge">error</code>则代表彻底废弃</td>
</tr>
<tr>
<td>reason</td>
<td>String</td>
<td>原因</td>
</tr>
<tr>
<td>replacement</td>
<td>String</td>
<td>替换属性</td>
</tr>
</tbody>
</table>
<p><strong>hints</strong></p>
<p>针对该属性,我的理解是类似于Java中的枚举,只不过是给每一个属性的值配置一个说明,方便使用者在配置的时候能够按照规定的值进行正确配置</p>
<p>例如上面我们的示例:<code class="highlighter-rouge">demo.enable</code>属性,该属性类型为Boolean类型,要配置也只有两种值(true或者false)</p>
<p>那么我们可以给该值配置一个hints进行说明,示例如下:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"hints"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"demo.enable"</span><span class="p">,</span><span class="w">
</span><span class="s2">"values"</span><span class="p">:[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"value"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="s2">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"启用DemoClient组件"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"value"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="s2">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"禁用DemoClient组件"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>
<p>当我们进行这样的配置后,最终使用者在使用时就会出现如下图所示的提示:
<img src="/assets/images/springboot/self/metaHints.png" alt="" /></p>
<p>这对使用该starter组件的开发者来说,每个属性都有相应的说明,是非常方便的</p>
<p>hints主要包含的属性如下:</p>
<table>
<thead>
<tr>
<th>名称</th>
<th>类型</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>name</td>
<td>String</td>
<td>属性名称</td>
</tr>
<tr>
<td>values</td>
<td>ValueHint[]</td>
<td>一个ValueHint的数组</td>
</tr>
<tr>
<td>providers</td>
<td>ValueProvider[]</td>
<td>一个ValueProvinder数组</td>
</tr>
</tbody>
</table>
<p>ValueHint是对其提供的值进行注释说明,其属性如下:</p>
<table>
<thead>
<tr>
<th>名称</th>
<th>类型</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>value</td>
<td>Object</td>
<td>属性对应的值</td>
</tr>
<tr>
<td>description</td>
<td>String</td>
<td>该值的描述信息</td>
</tr>
</tbody>
</table>
<p>ValueProvider包含属性:</p>
<table>
<thead>
<tr>
<th>名称</th>
<th>类型</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>name</td>
<td>String</td>
<td>属性名称</td>
</tr>
<tr>
<td>parameters</td>
<td>JSON Object</td>
<td>提供程序支持的其他参数类型</td>
</tr>
</tbody>
</table>
<p>在上面我提过,hints类似于枚举,这映射到ValueHint属性,当我们配置了hints属性中的<code class="highlighter-rouge">values</code>时而不提供<code class="highlighter-rouge">providers</code>属性时,如果开发者最终在使用时,只能配置ValueHint中定义的值,否则配置其他值时会在IDEA编辑器中就会爆红出错</p>
<p>还是以上面的示例,假设我们给appid配置hint值,如下:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"hints"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"demo.appid"</span><span class="p">,</span><span class="w">
</span><span class="s2">"values"</span><span class="p">:[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"value"</span><span class="p">:</span><span class="w"> </span><span class="s2">"test1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"测试appid1"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"value"</span><span class="p">:</span><span class="w"> </span><span class="s2">"test2"</span><span class="p">,</span><span class="w">
</span><span class="s2">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"测试appid2"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>
<p>那么我们在使用组件时,在<code class="highlighter-rouge">application.yml</code>配置文件中配置其他值时,idea会提示错误,如下图:</p>
<p><img src="/assets/images/springboot/self/metaHints1.png" alt="" /></p>
<p>此时,<code class="highlighter-rouge">providers</code>属性就可以排上用场了</p>
<p>修改上面的配置如下:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"hints"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"demo.appid"</span><span class="p">,</span><span class="w">
</span><span class="s2">"values"</span><span class="p">:[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"value"</span><span class="p">:</span><span class="w"> </span><span class="s2">"test1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"测试appid1"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"value"</span><span class="p">:</span><span class="w"> </span><span class="s2">"test2"</span><span class="p">,</span><span class="w">
</span><span class="s2">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"测试appid2"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="s2">"providers"</span><span class="p">:[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="s2">"any"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>
<p>我们可以配置providers为<code class="highlighter-rouge">any</code>,这样说明开发者除了可以配置<code class="highlighter-rouge">test1</code>、<code class="highlighter-rouge">test2</code>外,当配置其他值时,也是允许的</p>
<p>针对<code class="highlighter-rouge">providers</code>中的name属性,主要有以下类别供选择:</p>
<table>
<thead>
<tr>
<th style="text-align: left">Name</th>
<th style="text-align: left">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left"><code class="highlighter-rouge">any</code></td>
<td style="text-align: left">Permits any additional value to be provided.</td>
</tr>
<tr>
<td style="text-align: left"><code class="highlighter-rouge">class-reference</code></td>
<td style="text-align: left">Auto-completes the classes available in the project. Usually constrained by a base class that is specified by the <code class="highlighter-rouge">target</code> parameter.</td>
</tr>
<tr>
<td style="text-align: left"><code class="highlighter-rouge">handle-as</code></td>
<td style="text-align: left">Handles the property as if it were defined by the type defined by the mandatory <code class="highlighter-rouge">target</code> parameter.</td>
</tr>
<tr>
<td style="text-align: left"><code class="highlighter-rouge">logger-name</code></td>
<td style="text-align: left">Auto-completes valid logger names and <a href="https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-custom-log-groups">logger groups</a>. Typically, package and class names available in the current project can be auto-completed as well as defined groups.</td>
</tr>
<tr>
<td style="text-align: left"><code class="highlighter-rouge">spring-bean-reference</code></td>
<td style="text-align: left">Auto-completes the available bean names in the current project. Usually constrained by a base class that is specified by the <code class="highlighter-rouge">target</code> parameter.</td>
</tr>
<tr>
<td style="text-align: left"><code class="highlighter-rouge">spring-profile-name</code></td>
<td style="text-align: left">Auto-completes the available Spring profile names in the project.</td>
</tr>
</tbody>
</table>
<h2 id="附录">附录</h2>
<ul>
<li><a href="/2020/09/23/spring-boot-conditional/">Spring Boot框架中如何优雅的注入实体Bean</a></li>
<li><a href="https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-configuration-metadata.html">Spring Boot Configuration Metadata Document</a></li>
</ul>肖玉民前言有意思的两段java代码2020-12-06T00:00:00+08:002020-12-06T00:00:00+08:00https://xiaoym.gitee.io/2020/12/06/funin-java8<p>首先,创建一个实体类Order对象,代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Order</span><span class="o">{</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">orderNo</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
<span class="kd">public</span> <span class="nf">Order</span><span class="o">(){</span>
<span class="n">setOrderNo</span><span class="o">(</span><span class="s">"order:"</span><span class="o">+</span> <span class="n">UUID</span><span class="o">.</span><span class="na">randomUUID</span><span class="o">().</span><span class="na">toString</span><span class="o">());</span>
<span class="n">setName</span><span class="o">(</span><span class="s">"name:"</span><span class="o">+</span><span class="n">UUID</span><span class="o">.</span><span class="na">randomUUID</span><span class="o">().</span><span class="na">toString</span><span class="o">());</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="n">String</span> <span class="nf">getOrderNo</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">orderNo</span><span class="o">;</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">setOrderNo</span><span class="o">(</span><span class="n">String</span> <span class="n">orderNo</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">orderNo</span> <span class="o">=</span> <span class="n">orderNo</span><span class="o">;</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="n">String</span> <span class="nf">getName</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">name</span><span class="o">;</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">setName</span><span class="o">(</span><span class="n">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">name</span> <span class="o">=</span> <span class="n">name</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>第一个 方法,使用for循环遍历查找,即使找到也不做任何事,代码片段如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="kd">private</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">test2</span><span class="o">(){</span>
<span class="n">List</span><span class="o"><</span><span class="n">Order</span><span class="o">></span> <span class="n">orderList</span><span class="o">=</span><span class="k">new</span> <span class="n">ArrayList</span><span class="o"><>();</span>
<span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span><span class="o">=</span><span class="mi">0</span><span class="o">;</span><span class="n">i</span><span class="o"><</span><span class="mi">1000</span><span class="o">;</span><span class="n">i</span><span class="o">++){</span>
<span class="n">orderList</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="k">new</span> <span class="n">Order</span><span class="o">());</span>
<span class="o">}</span>
<span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"aaaaa"</span><span class="o">);</span>
<span class="kt">boolean</span> <span class="n">flag</span><span class="o">=</span><span class="kc">true</span><span class="o">;</span>
<span class="n">AtomicLong</span> <span class="n">atomicLong</span><span class="o">=</span><span class="k">new</span> <span class="n">AtomicLong</span><span class="o">(</span><span class="mi">0</span><span class="o">);</span>
<span class="k">do</span> <span class="o">{</span>
<span class="c1">//遍历</span>
<span class="k">for</span> <span class="o">(</span><span class="n">Order</span> <span class="nl">order:</span><span class="n">orderList</span><span class="o">){</span>
<span class="k">if</span> <span class="o">(</span><span class="n">order</span><span class="o">.</span><span class="na">getName</span><span class="o">().</span><span class="na">equals</span><span class="o">(</span><span class="s">"abc"</span><span class="o">)){</span>
<span class="c1">//find but do nothing</span>
<span class="n">Optional</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">order</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="c1">//orderList.stream().filter(order -> order.getName().equals("abc")).findFirst();</span>
<span class="kt">long</span> <span class="n">value</span><span class="o">=</span><span class="n">atomicLong</span><span class="o">.</span><span class="na">incrementAndGet</span><span class="o">();</span>
<span class="k">if</span> <span class="o">(</span><span class="n">value</span><span class="o">></span><span class="mi">10000L</span><span class="o">){</span>
<span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"推出"</span><span class="o">);</span>
<span class="n">flag</span><span class="o">=</span><span class="kc">false</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span><span class="k">while</span> <span class="o">(</span><span class="n">flag</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>第二个方法,使用java8中的lambda表达式stream中的filter进行查找,代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">test1</span><span class="o">(){</span>
<span class="n">List</span><span class="o"><</span><span class="n">Order</span><span class="o">></span> <span class="n">orderList</span><span class="o">=</span><span class="k">new</span> <span class="n">ArrayList</span><span class="o"><>();</span>
<span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span><span class="o">=</span><span class="mi">0</span><span class="o">;</span><span class="n">i</span><span class="o"><</span><span class="mi">1000</span><span class="o">;</span><span class="n">i</span><span class="o">++){</span>
<span class="n">orderList</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="k">new</span> <span class="n">Order</span><span class="o">());</span>
<span class="o">}</span>
<span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"aaaaa"</span><span class="o">);</span>
<span class="kt">boolean</span> <span class="n">flag</span><span class="o">=</span><span class="kc">true</span><span class="o">;</span>
<span class="n">AtomicLong</span> <span class="n">atomicLong</span><span class="o">=</span><span class="k">new</span> <span class="n">AtomicLong</span><span class="o">(</span><span class="mi">0</span><span class="o">);</span>
<span class="k">do</span> <span class="o">{</span>
<span class="c1">//遍历</span>
<span class="n">orderList</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">filter</span><span class="o">(</span><span class="n">order</span> <span class="o">-></span> <span class="n">order</span><span class="o">.</span><span class="na">getName</span><span class="o">().</span><span class="na">equals</span><span class="o">(</span><span class="s">"abc"</span><span class="o">)).</span><span class="na">findFirst</span><span class="o">();</span>
<span class="kt">long</span> <span class="n">value</span><span class="o">=</span><span class="n">atomicLong</span><span class="o">.</span><span class="na">incrementAndGet</span><span class="o">();</span>
<span class="k">if</span> <span class="o">(</span><span class="n">value</span><span class="o">></span><span class="mi">10000L</span><span class="o">){</span>
<span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"推出"</span><span class="o">);</span>
<span class="n">flag</span><span class="o">=</span><span class="kc">false</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span><span class="k">while</span> <span class="o">(</span><span class="n">flag</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>两段代码如果不设置java的Xmx参数,都能正常运行,假设设置参数</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">java</span> <span class="o">-</span><span class="n">Xmx2m</span>
</code></pre></div></div>
<p>那么第二段使用java8的lambda表达式的程序将会报错,抛出异常</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
Exception <span class="k">in </span>thread <span class="s2">"main"</span> java.lang.InternalError: <span class="nv">zero_L</span><span class="o">=</span>Lambda<span class="o">(</span>a0:L<span class="o">)=>{</span>
t1:L<span class="o">=</span>LambdaForm.identity_L<span class="o">((</span>null<span class="o">))</span><span class="p">;</span>t1:L<span class="o">}</span>
at java.lang.invoke.MethodHandleStatics.newInternalError<span class="o">(</span>MethodHandleStatics.java:127<span class="o">)</span>
at java.lang.invoke.LambdaForm.compileToBytecode<span class="o">(</span>LambdaForm.java:660<span class="o">)</span>
at java.lang.invoke.LambdaForm.prepare<span class="o">(</span>LambdaForm.java:635<span class="o">)</span>
at java.lang.invoke.MethodHandle.<init><span class="o">(</span>MethodHandle.java:461<span class="o">)</span>
at java.lang.invoke.BoundMethodHandle.<init><span class="o">(</span>BoundMethodHandle.java:58<span class="o">)</span>
at java.lang.invoke.SimpleMethodHandle.<init><span class="o">(</span>SimpleMethodHandle.java:37<span class="o">)</span>
at java.lang.invoke.SimpleMethodHandle.make<span class="o">(</span>SimpleMethodHandle.java:41<span class="o">)</span>
at java.lang.invoke.LambdaForm.createIdentityForms<span class="o">(</span>LambdaForm.java:1783<span class="o">)</span>
at java.lang.invoke.LambdaForm.<clinit><span class="o">(</span>LambdaForm.java:1833<span class="o">)</span>
at java.lang.invoke.DirectMethodHandle.makePreparedLambdaForm<span class="o">(</span>DirectMethodHandle.java:222<span class="o">)</span>
at java.lang.invoke.DirectMethodHandle.preparedLambdaForm<span class="o">(</span>DirectMethodHandle.java:187<span class="o">)</span>
at java.lang.invoke.DirectMethodHandle.preparedLambdaForm<span class="o">(</span>DirectMethodHandle.java:176<span class="o">)</span>
at java.lang.invoke.DirectMethodHandle.make<span class="o">(</span>DirectMethodHandle.java:83<span class="o">)</span>
at java.lang.invoke.MethodHandles<span class="nv">$Lookup</span>.getDirectMethodCommon<span class="o">(</span>MethodHandles.java:1655<span class="o">)</span>
at java.lang.invoke.MethodHandles<span class="nv">$Lookup</span>.getDirectMethodNoSecurityManager<span class="o">(</span>MethodHandles.java:1612<span class="o">)</span>
at java.lang.invoke.MethodHandles<span class="nv">$Lookup</span>.getDirectMethodForConstant<span class="o">(</span>MethodHandles.java:1797<span class="o">)</span>
at java.lang.invoke.MethodHandles<span class="nv">$Lookup</span>.linkMethodHandleConstant<span class="o">(</span>MethodHandles.java:1746<span class="o">)</span>
at java.lang.invoke.MethodHandleNatives.linkMethodHandleConstant<span class="o">(</span>MethodHandleNatives.java:477<span class="o">)</span>
at com.github.xiaoymin.Java8Test.test1<span class="o">(</span>Java8Test.java:63<span class="o">)</span>
at com.github.xiaoymin.Java8Test.main<span class="o">(</span>Java8Test.java:24<span class="o">)</span>
Caused by: java.lang.OutOfMemoryError: GC overhead limit exceeded
at jdk.internal.org.objectweb.asm.ByteVector.<init><span class="o">(</span>ByteVector.java:84<span class="o">)</span>
at jdk.internal.org.objectweb.asm.MethodWriter.visitAnnotation<span class="o">(</span>MethodWriter.java:555<span class="o">)</span>
at java.lang.invoke.InvokerBytecodeGenerator.generateCustomizedCodeBytes<span class="o">(</span>InvokerBytecodeGenerator.java:640<span class="o">)</span>
at java.lang.invoke.InvokerBytecodeGenerator.generateCustomizedCode<span class="o">(</span>InvokerBytecodeGenerator.java:618<span class="o">)</span>
at java.lang.invoke.LambdaForm.compileToBytecode<span class="o">(</span>LambdaForm.java:654<span class="o">)</span>
... 18 more
<span class="k">***</span> java.lang.instrument ASSERTION FAILED <span class="k">***</span>: <span class="s2">"!errorOutstanding"</span> with message can<span class="s1">'t create byte arrau at JPLISAgent.c line: 813
*** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message can'</span>t create byte arrau at JPLISAgent.c line: 813
<span class="k">***</span> java.lang.instrument ASSERTION FAILED <span class="k">***</span>: <span class="s2">"!errorOutstanding"</span> with message can<span class="s1">'t create byte arrau at JPLISAgent.c line: 813
</span></code></pre></div></div>
<p>那么,是否可以认为在超过2层的for循环中,对于集合的筛选或者等其他各种操作,应该禁用java8的stream操作?</p>
<p>因为一旦外层for循环次数较多,java8中的stream操作将会创造很多临时对象,必然导致JVM频繁的GC操作!!</p>肖玉民首先,创建一个实体类Order对象,代码如下: ```java第一篇 Redis常用数据结构介绍及基本操作2020-11-29T00:00:00+08:002020-11-29T00:00:00+08:00https://xiaoym.gitee.io/2020/11/29/redis-1<p>目前Redis支持的主要数据结构包含5种,分别是:</p>
<ul>
<li>字符串(string)</li>
<li>散列(hash)</li>
<li>列表(list)</li>
<li>集合(set)</li>
<li>有序集合(sorted set)</li>
</ul>
<h3 id="11-常用数据结构介绍">1.1 常用数据结构介绍</h3>
<h4 id="111-字符串string">1.1.1 字符串(String)</h4>
<p>字符串结构是开发人员平常使用最多的结构,同时也是最简单的,它的值不仅仅是字符串,也可以是数值.</p>
<p>常用命令包括:<code class="highlighter-rouge">GET</code>、<code class="highlighter-rouge">SET</code>、<code class="highlighter-rouge">INCR</code>、<code class="highlighter-rouge">DECR</code>、<code class="highlighter-rouge">MGET</code>等等</p>
<p>主要应用场景:字符串是最常见的数据类型之一,普通的键/值存储可以使用该数据结构进行存储.从而可以完全实现当前的Memcached所实现的功能并提高效率。还可以享受Redis的定时持久性,操作日志和复制功能。除了提供GET,SET,INCR,DECR等操作外,Redis还提供以下功能:</p>
<ul>
<li>获取字符串的长度</li>
<li>字符串的内容进行追加</li>
<li>设置并获取一部分字符串</li>
<li>设置并获取字符串某一个bit</li>
<li>批量设置一系列字符串的内容</li>
</ul>
<p>常用的方案:使用该数据结构缓存程序计数器,例如:微博数量、粉丝数量等等</p>
<h4 id="112-散列hash">1.1.2 散列(Hash)</h4>
<p>常用命令:<code class="highlighter-rouge">HGET</code>、<code class="highlighter-rouge">HSET</code>、<code class="highlighter-rouge">HGETALL</code>等</p>
<p><code class="highlighter-rouge">Hash</code>和java语言中的<code class="highlighter-rouge">Map</code>集合结构很相似,只不过在<code class="highlighter-rouge">Redis</code>中,一个Hash结构需要一个key进行关联,数据结构有点类似于如下图:</p>
<p><img src="/images/blog/redis/hash.png" alt="" /></p>
<p>上图中黑色加粗的<strong>key1</strong>所代表的是Redis中存储的键值,而虚线框中的<code class="highlighter-rouge">key1</code>所代表的是Hash数据结构中的键值</p>
<p>简单操作如下:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>127.0.0.1:6379> hget tenant tenantId
<span class="o">(</span>nil<span class="o">)</span>
127.0.0.1:6379> hset tenant tenantId abc
<span class="o">(</span>integer<span class="o">)</span> 1
127.0.0.1:6379> hset tenant title <span class="nb">test</span>
<span class="o">(</span>integer<span class="o">)</span> 1
127.0.0.1:6379> hget tenant tenantId
<span class="s2">"abc"</span>
127.0.0.1:6379> hgetall tenant
1<span class="o">)</span> <span class="s2">"tenantId"</span>
2<span class="o">)</span> <span class="s2">"abc"</span>
3<span class="o">)</span> <span class="s2">"title"</span>
4<span class="o">)</span> <span class="s2">"test"</span>
127.0.0.1:6379>
</code></pre></div></div>
<p>假设我们需要存储包含以下信息的用户信息对象数据:</p>
<p>用户ID是要查找的键,储值用户对象包含名称,年龄,生日等信息,如果要以普通的键/值结构进行存储,主要有以下两种存储方法:</p>
<ul>
<li>
<p>第一种方法是将用户的ID作为Redis中的Key,值则是序列号的用户对象,这样做的缺点是在存储和获取时,增加了序列化/反序列化的成本,如果要修改用户的某一个属性时,则需要对整个对象进行操作,在并发条件下,会出现CAS之类的问题.</p>
</li>
<li>
<p>第二种方法是将多少个该用户信息对象的成员保存到键值数目中,其中用户id +相应属性的名称作为唯一标识符来获取相应属性的值,尽管这样做的代价是序列化和并发性被省略,但是用户ID被重复存储,如果存在大量此类数据,则存在内存大量浪费的情况。</p>
</li>
</ul>
<p>因此,Redis提供的哈希是解决此问题的好方法,Redis哈希实际上是内部存储的值作为哈希图,并提供对映射成员接口的直接访问,例如:</p>
<p>也就是说,键仍然是用户ID,值是地图,地图键是属性名称的成员,值是属性值,以便可以通过其内部地图键直接修改和访问数据(Redis称为内部映射键字段),这意味着可以通过键(用户ID)+字段(属性标签)来操纵相应的属性数据,而无需重复存储数据,也没有序列化和并发修改控制的问题。是一个很好的解决方案。</p>
<p>还需要注意的是,Redis提供了一个可以直接获取所有属性数据的接口(Hgetall),但是如果内部映射具有大量成员,则涉及遍历整个内部映射,这可能很耗时由于Redis单线程模型。其他客户端请求根本没有响应,这需要格外注意。</p>
<p>使用情况:存储部分更改数据,例如用户信息。</p>
<h4 id="113-列表list">1.1.3 列表(list)</h4>
<p>Redis的列表结构,存放一个string类型的链表.</p>
<p>常用命令:<code class="highlighter-rouge">lpush</code>、<code class="highlighter-rouge">rpush</code>、<code class="highlighter-rouge">lpop</code>、<code class="highlighter-rouge">rpop</code>、<code class="highlighter-rouge">lrange</code>等</p>
<p>应用场景:例如一个网站上的粉丝列表、监控列表、消息队列、日志收集器</p>
<p>list底层是一个双向链接的数据结构,相信自己具有数据结构知识的人应该能够理解其结构。使用列表结构,我们可以轻松实现最新消息排名和其他功能。也可以用作消息队列,你可以使用列表的推操作将任务放置在列表中,然后工作线程使用弹出操作将任务取出。 Redis还提供了用于操作列表的一部分的API,您可以直接查询以从列表的一部分中删除元素。</p>
<p>简单命令操作:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>127.0.0.1:6379> lpush list-user uid001
<span class="o">(</span>integer<span class="o">)</span> 1
127.0.0.1:6379> lpush list-user uid002
<span class="o">(</span>integer<span class="o">)</span> 2
127.0.0.1:6379> lrange list-user 0 1
1<span class="o">)</span> <span class="s2">"uid002"</span>
2<span class="o">)</span> <span class="s2">"uid001"</span>
127.0.0.1:6379> rpush list-user uid003
<span class="o">(</span>integer<span class="o">)</span> 3
127.0.0.1:6379> lrange list-user 0 2
1<span class="o">)</span> <span class="s2">"uid002"</span>
2<span class="o">)</span> <span class="s2">"uid001"</span>
3<span class="o">)</span> <span class="s2">"uid003"</span>
127.0.0.1:6379> lpop list-user
<span class="s2">"uid002"</span>
127.0.0.1:6379> llen list-user
<span class="o">(</span>integer<span class="o">)</span> 2
127.0.0.1:6379> lrange list-user 0 2
1<span class="o">)</span> <span class="s2">"uid001"</span>
2<span class="o">)</span> <span class="s2">"uid003"</span>
127.0.0.1:6379> rpush list-user uid004
<span class="o">(</span>integer<span class="o">)</span> 3
127.0.0.1:6379> llen list-user
<span class="o">(</span>integer<span class="o">)</span> 3
127.0.0.1:6379> lrange list-user 0 2
1<span class="o">)</span> <span class="s2">"uid001"</span>
2<span class="o">)</span> <span class="s2">"uid003"</span>
3<span class="o">)</span> <span class="s2">"uid004"</span>
127.0.0.1:6379> rpop list-user
<span class="s2">"uid004"</span>
127.0.0.1:6379> lrange list-user 0 2
1<span class="o">)</span> <span class="s2">"uid001"</span>
2<span class="o">)</span> <span class="s2">"uid003"</span>
127.0.0.1:6379>
</code></pre></div></div>
<h4 id="114-集合set">1.1.4 集合(Set)</h4>
<p>Set对外提供的功能类似于列表(list),但是Set更加轻量级(对于内存来说),Set存储的元素是不重复的</p>
<p>也可以根据不同的Set集合取交集、并集等</p>
<p>常用操作命令:<code class="highlighter-rouge">sadd</code>、<code class="highlighter-rouge">smembers</code>、<code class="highlighter-rouge">scard</code>、<code class="highlighter-rouge">sinter</code>、<code class="highlighter-rouge">sdiff</code>、<code class="highlighter-rouge">srem</code>等</p>
<p>常见命令操作:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>127.0.0.1:6379> sadd myset a1 a2 a3 a4 a5 a6 a7
<span class="o">(</span>integer<span class="o">)</span> 7
127.0.0.1:6379> smembers myset
1<span class="o">)</span> <span class="s2">"a6"</span>
2<span class="o">)</span> <span class="s2">"a1"</span>
3<span class="o">)</span> <span class="s2">"a2"</span>
4<span class="o">)</span> <span class="s2">"a3"</span>
5<span class="o">)</span> <span class="s2">"a5"</span>
6<span class="o">)</span> <span class="s2">"a7"</span>
7<span class="o">)</span> <span class="s2">"a4"</span>
127.0.0.1:6379>
</code></pre></div></div>
<p>比如在微博系统中,每一个用户都会有一个关注列表,此时有如下需求:</p>
<ul>
<li>如何找出两个用户共同关注的人</li>
<li>如何找出用户1与用户2关注的不同的人</li>
</ul>
<p>假设用户1的关注列表中有:<code class="highlighter-rouge">u1、u2、u3、u4、u5、u6</code></p>
<p>用户2的关注用户列表中有:<code class="highlighter-rouge">u2、u5、u7、u8</code></p>
<p>从上面的结果来看,用户1和用户2共同关注的用户有:<code class="highlighter-rouge">u2\u5</code></p>
<p>用户1关注的不同的人:<code class="highlighter-rouge">u1\u3\u4\u6</code></p>
<p>用户2关注的不同的人:<code class="highlighter-rouge">u7\u8</code></p>
<p>通过Redis的set集合来进行实现,命令如下:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 先集合初始化 u1set \u2set</span>
127.0.0.1:6379> sadd u1set u1 u2 u3 u4 u5 u6
<span class="o">(</span>integer<span class="o">)</span> 6
127.0.0.1:6379> sadd u2set u2 u5 u7 u8
<span class="o">(</span>integer<span class="o">)</span> 4
127.0.0.1:6379> smembers u1set
1<span class="o">)</span> <span class="s2">"u4"</span>
2<span class="o">)</span> <span class="s2">"u1"</span>
3<span class="o">)</span> <span class="s2">"u2"</span>
4<span class="o">)</span> <span class="s2">"u3"</span>
5<span class="o">)</span> <span class="s2">"u6"</span>
6<span class="o">)</span> <span class="s2">"u5"</span>
127.0.0.1:6379> smembers u2set
1<span class="o">)</span> <span class="s2">"u7"</span>
2<span class="o">)</span> <span class="s2">"u5"</span>
3<span class="o">)</span> <span class="s2">"u8"</span>
4<span class="o">)</span> <span class="s2">"u2"</span>
<span class="c"># 找出u1与u2用户关注的不同人的集合</span>
127.0.0.1:6379> sdiff u1set u2set
1<span class="o">)</span> <span class="s2">"u3"</span>
2<span class="o">)</span> <span class="s2">"u6"</span>
3<span class="o">)</span> <span class="s2">"u1"</span>
4<span class="o">)</span> <span class="s2">"u4"</span>
<span class="c"># 找出u2与u1用户关注的不同人的集合</span>
127.0.0.1:6379> sdiff u2set u1set
1<span class="o">)</span> <span class="s2">"u7"</span>
2<span class="o">)</span> <span class="s2">"u8"</span>
<span class="c"># u1\u2共同关注的人</span>
127.0.0.1:6379> sinter u1set u2set
1<span class="o">)</span> <span class="s2">"u5"</span>
2<span class="o">)</span> <span class="s2">"u2"</span>
127.0.0.1:6379>
</code></pre></div></div>
<h4 id="115-有序集合sorted-set">1.1.5 有序集合(Sorted Set)</h4>
<p>有序集合的数据结构类似于集合与哈希之间的混合体,与集合一样,其元素是由唯一非重复的字符串组成.</p>
<p>但是集合(Set)中的元素没有排序,而有序集合排序后,其中的每个元素都与一个浮点数值相关联(这也是为什么说该数据结构与哈希结构类似的原因,因为其每个元素都映射了一个排序值)</p>
<p>排序规则如下:</p>
<ul>
<li>如果A和B是两个分数不同的元素,如果A.score>B.store,那么A>B</li>
<li>如果A和B的分数完全相同,由于A字符串在字典上大于B字符串,所以A>B。AB不能相等,因为有序集合的元素具有唯一性</li>
</ul>
<p>常用操作命令:<code class="highlighter-rouge">zadd</code>、<code class="highlighter-rouge">zrange</code>、<code class="highlighter-rouge">zrank</code>、<code class="highlighter-rouge">zrangebyscore</code>、<code class="highlighter-rouge">zcard</code></p>
<p>简单操作命令:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>127.0.0.1:6379> zadd mysort 10 ab 40 cb 60 db
<span class="o">(</span>integer<span class="o">)</span> 3
127.0.0.1:6379> zrange mysort 0 2
1<span class="o">)</span> <span class="s2">"ab"</span>
2<span class="o">)</span> <span class="s2">"cb"</span>
3<span class="o">)</span> <span class="s2">"db"</span>
127.0.0.1:6379> zadd mysort 55 eb
<span class="o">(</span>integer<span class="o">)</span> 1
127.0.0.1:6379> zrange mysort 0 3
1<span class="o">)</span> <span class="s2">"ab"</span>
2<span class="o">)</span> <span class="s2">"cb"</span>
3<span class="o">)</span> <span class="s2">"eb"</span>
4<span class="o">)</span> <span class="s2">"db"</span>
127.0.0.1:6379> zcard mysort
<span class="o">(</span>integer<span class="o">)</span> 4
127.0.0.1:6379>
</code></pre></div></div>
<h3 id="12-java语言redis客户端介绍及操作">1.2 Java语言Redis客户端介绍及操作</h3>
<p>Java语言中使用Redis比较常见的客户端:<a href="https://github.com/xetorthio/jedis">jedis</a>、<a href="https://github.com/redisson/redisson">Redisson</a>、<a href="https://lettuce.io/">lettuce</a></p>
<table>
<thead>
<tr>
<th>客户端</th>
<th>GitHub</th>
<th>star</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>jedis</td>
<td><a href="https://github.com/xetorthio/jedis">https://github.com/xetorthio/jedis</a></td>
<td>9.4K</td>
<td>轻量级的java客户端</td>
</tr>
<tr>
<td>lettuce</td>
<td><a href="https://github.com/lettuce-io/lettuce-core">https://github.com/lettuce-io/lettuce-core</a></td>
<td>3.6K</td>
<td>高级Redis客户端,用于线程安全的同步,异步和反应式使用。 支持群集,前哨,流水线和编解码器。</td>
</tr>
<tr>
<td>Redisson</td>
<td><a href="https://github.com/redisson/redisson">https://github.com/redisson/redisson</a></td>
<td>14.6k</td>
<td>分布式以及可伸缩性更强</td>
</tr>
</tbody>
</table>
<p>目前使用的Spring Boot框架底层主要提供了jedis、lettuce两种支持,开发者通过yml中进行配置,Spring Boot自动选择,Spring Boot框架目前默认使用lettuce作为Redis的客户端。</p>
<p>以下主要是通过以上的客户端java库,对Redis的基本数据类型进行简单的操作</p>
<h4 id="121-使用jedis进行操作">1.2.1 使用Jedis进行操作</h4>
<p>通过Jedis操作Redis,根据Jedis提供的构造函数,主要有三种方式获取Jedis对象的实例</p>
<ul>
<li>根据Redis的ip地址、端口简单创建获取实例</li>
<li>根据连接池配置获取Redis进行操作(推荐做法)</li>
<li>根据集群配置创建Jedis实例获取Redis的操作对象</li>
</ul>
<p>先来看第一种简单的创建Jedis对象的方式,代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//Redis的ip及端口号</span>
<span class="n">Jedis</span> <span class="n">simple</span><span class="o">=</span><span class="k">new</span> <span class="n">Jedis</span><span class="o">(</span><span class="s">"localhost"</span><span class="o">,</span><span class="mi">6379</span><span class="o">);</span>
<span class="c1">//如果Redis设置了密码,此处需要设置密码,反之则不用</span>
<span class="n">simple</span><span class="o">.</span><span class="na">auth</span><span class="o">(</span><span class="s">"123456"</span><span class="o">);</span>
</code></pre></div></div>
<p>创建<code class="highlighter-rouge">JedisPool</code>连接池对象,从连接池获取Jedis实例,代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">GenericObjectPoolConfig</span> <span class="n">poolConfig</span><span class="o">=</span><span class="k">new</span> <span class="n">GenericObjectPoolConfig</span><span class="o">();</span>
<span class="c1">//连接池中的最大空闲连接</span>
<span class="n">poolConfig</span><span class="o">.</span><span class="na">setMaxIdle</span><span class="o">(</span><span class="mi">8</span><span class="o">);</span>
<span class="c1">//连接池最大连接数(使用负值表示没有限制)</span>
<span class="n">poolConfig</span><span class="o">.</span><span class="na">setMaxTotal</span><span class="o">(</span><span class="mi">8</span><span class="o">);</span>
<span class="c1">// 连接池中的最小空闲连接</span>
<span class="n">poolConfig</span><span class="o">.</span><span class="na">setMinIdle</span><span class="o">(</span><span class="mi">0</span><span class="o">);</span>
<span class="c1">//连接池最大阻塞等待时间(使用负值表示没有限制)</span>
<span class="n">poolConfig</span><span class="o">.</span><span class="na">setMaxWaitMillis</span><span class="o">(-</span><span class="mi">1</span><span class="o">);</span>
<span class="c1">//jedisPool只需要初始化1次即可,每次获取Jedis直接调用getResource方法从连接池中获取</span>
<span class="n">JedisPool</span> <span class="n">jedisPool</span><span class="o">=</span><span class="k">new</span> <span class="n">JedisPool</span><span class="o">(</span><span class="n">poolConfig</span><span class="o">,</span><span class="s">"localhost"</span><span class="o">,</span><span class="mi">6379</span><span class="o">,</span><span class="mi">5000</span><span class="o">,</span><span class="s">"123456"</span><span class="o">);</span>
<span class="n">Jedis</span> <span class="n">jedis</span><span class="o">=</span><span class="n">jedisPool</span><span class="o">.</span><span class="na">getResource</span><span class="o">();</span>
</code></pre></div></div>
<p>如果我们的Redis是集群部署,此时,我们可以通过集群的配置获取Jedis对象以操作Redis,代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//创建连接池对象</span>
<span class="n">GenericObjectPoolConfig</span> <span class="n">poolConfig</span><span class="o">=</span><span class="k">new</span> <span class="n">GenericObjectPoolConfig</span><span class="o">();</span>
<span class="c1">//连接池中的最大空闲连接</span>
<span class="n">poolConfig</span><span class="o">.</span><span class="na">setMaxIdle</span><span class="o">(</span><span class="mi">8</span><span class="o">);</span>
<span class="c1">//连接池最大连接数(使用负值表示没有限制)</span>
<span class="n">poolConfig</span><span class="o">.</span><span class="na">setMaxTotal</span><span class="o">(</span><span class="mi">8</span><span class="o">);</span>
<span class="c1">// 连接池中的最小空闲连接</span>
<span class="n">poolConfig</span><span class="o">.</span><span class="na">setMinIdle</span><span class="o">(</span><span class="mi">0</span><span class="o">);</span>
<span class="c1">//连接池最大阻塞等待时间(使用负值表示没有限制)</span>
<span class="n">poolConfig</span><span class="o">.</span><span class="na">setMaxWaitMillis</span><span class="o">(-</span><span class="mi">1</span><span class="o">);</span>
<span class="c1">//创建集群</span>
<span class="n">Set</span><span class="o"><</span><span class="n">HostAndPort</span><span class="o">></span> <span class="n">nodes</span><span class="o">=</span><span class="k">new</span> <span class="n">HashSet</span><span class="o"><>();</span>
<span class="n">HostAndPort</span> <span class="n">hostAndPort</span><span class="o">=</span><span class="k">new</span> <span class="n">HostAndPort</span><span class="o">(</span><span class="s">"192.168.0.11"</span><span class="o">,</span><span class="mi">133</span><span class="o">);</span>
<span class="n">HostAndPort</span> <span class="n">hostAndPort1</span><span class="o">=</span><span class="k">new</span> <span class="n">HostAndPort</span><span class="o">(</span><span class="s">"192.168.0.12"</span><span class="o">,</span><span class="mi">133</span><span class="o">);</span>
<span class="n">nodes</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">hostAndPort</span><span class="o">);</span>
<span class="n">nodes</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">hostAndPort1</span><span class="o">);</span>
<span class="c1">//1、connectionTimeout 连接超时时间</span>
<span class="c1">//2、soTimeout 读取数据超时时间</span>
<span class="c1">//3、maxAttempts 错误时最大尝试次数</span>
<span class="c1">//4、password 密码</span>
<span class="c1">//创建集群对象</span>
<span class="n">JedisCluster</span> <span class="n">jedisCluster</span><span class="o">=</span><span class="k">new</span> <span class="n">JedisCluster</span><span class="o">(</span><span class="n">nodes</span><span class="o">,</span><span class="mi">2000</span><span class="o">,</span><span class="mi">20000</span><span class="o">,</span><span class="mi">3</span><span class="o">,</span><span class="s">"123456"</span><span class="o">,</span><span class="n">poolConfig</span><span class="o">);</span>
</code></pre></div></div>
<p>不管是集群方式或者是连接池等,最终操作Redis的数据结构在客户端都是差不多的,接下来以Jedis为例来操作Redis的五种不同数据结构</p>
<p><strong>字符串(String)</strong></p>
<p>常用命令包括:<code class="highlighter-rouge">GET</code>、<code class="highlighter-rouge">SET</code>、<code class="highlighter-rouge">INCR</code>、<code class="highlighter-rouge">DECR</code>、<code class="highlighter-rouge">MGET</code>等等</p>
<p>字符串操作直接看以下代码:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Jedis</span> <span class="n">jedis</span><span class="o">=</span><span class="n">getSimpleJedis</span><span class="o">();</span>
<span class="c1">//获取string</span>
<span class="n">String</span> <span class="n">value</span><span class="o">=</span><span class="n">jedis</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="s">"answertoken_abc"</span><span class="o">);</span>
<span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">value</span><span class="o">);</span>
<span class="c1">//设置string类型的key-value</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">set</span><span class="o">(</span><span class="s">"mykey"</span><span class="o">,</span><span class="s">"myvalue"</span><span class="o">);</span>
<span class="c1">//根据SetParams设置,主要包含四种:ex\nx\xx\px</span>
<span class="c1">//1、ex:设置键key的过期时间,单位时秒</span>
<span class="c1">//2、px:设置键key的过期时间,单位毫秒</span>
<span class="c1">//3、nx:只有键key不存在的时候才会设置key的值</span>
<span class="c1">//4、xx:只有键key存在的时候才会设置key的值</span>
<span class="c1">//第一种情况,设置一个key值,同时指定过期时间</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">set</span><span class="o">(</span><span class="s">"mykey"</span><span class="o">,</span><span class="s">"myvalue1"</span><span class="o">,</span><span class="n">SetParams</span><span class="o">.</span><span class="na">setParams</span><span class="o">().</span><span class="na">ex</span><span class="o">(</span><span class="mi">10</span><span class="o">));</span>
<span class="c1">//第二种情况,当key值不存在时进行设置,并且设置过期时间</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">set</span><span class="o">(</span><span class="s">"mykey"</span><span class="o">,</span><span class="s">"myvalue2"</span><span class="o">,</span><span class="n">SetParams</span><span class="o">.</span><span class="na">setParams</span><span class="o">().</span><span class="na">nx</span><span class="o">().</span><span class="na">ex</span><span class="o">(</span><span class="mi">10</span><span class="o">));</span>
<span class="c1">//第三种情况,当key值存在时进行设置,并且设置过期时间(分布式锁场景)</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">set</span><span class="o">(</span><span class="s">"mykey"</span><span class="o">,</span><span class="s">"myvalue3"</span><span class="o">,</span><span class="n">SetParams</span><span class="o">.</span><span class="na">setParams</span><span class="o">().</span><span class="na">xx</span><span class="o">().</span><span class="na">ex</span><span class="o">(</span><span class="mi">10</span><span class="o">));</span>
<span class="c1">//自增操作</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">set</span><span class="o">(</span><span class="s">"mykey"</span><span class="o">,</span><span class="s">"1"</span><span class="o">);</span>
<span class="c1">//自增+1</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">incr</span><span class="o">(</span><span class="s">"mykey"</span><span class="o">);</span>
<span class="c1">//自增+n</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">incrBy</span><span class="o">(</span><span class="s">"mykey"</span><span class="o">,</span><span class="mi">10</span><span class="o">);</span>
<span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">jedis</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="s">"mykey"</span><span class="o">));</span>
<span class="c1">//自减操作</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">decr</span><span class="o">(</span><span class="s">"mykey"</span><span class="o">);</span>
<span class="c1">//自减-N</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">decrBy</span><span class="o">(</span><span class="s">"mykey"</span><span class="o">,</span><span class="mi">10</span><span class="o">);</span>
<span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">jedis</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="s">"mykey"</span><span class="o">));</span>
</code></pre></div></div>
<p>通过上面jedis对象对string结构提供的API方法进行操作还是非常方便的。</p>
<p><strong>散列(hash)</strong></p>
<p>常用命令:<code class="highlighter-rouge">HGET</code>、<code class="highlighter-rouge">HSET</code>、<code class="highlighter-rouge">HGETALL</code>等</p>
<p>在Jedis客户端中操作Redis的hash结构和在命令行中差不多,jedis封装的api命令几乎没有什么区别</p>
<p>java开发者在操作时,一个hash理解为在Java语言中的Map数据结构即可。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//操作hash</span>
<span class="n">Jedis</span> <span class="n">jedis</span><span class="o">=</span><span class="n">getSimpleJedis</span><span class="o">();</span>
<span class="c1">//赋值操作 </span>
<span class="n">String</span> <span class="n">hashKey</span><span class="o">=</span><span class="s">"myhashkey"</span><span class="o">;</span>
<span class="n">Map</span><span class="o"><</span><span class="n">String</span><span class="o">,</span><span class="n">String</span><span class="o">></span> <span class="n">values</span><span class="o">=</span><span class="k">new</span> <span class="n">HashMap</span><span class="o"><>();</span>
<span class="n">values</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"name"</span><span class="o">,</span><span class="s">"张三"</span><span class="o">);</span>
<span class="n">values</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"age"</span><span class="o">,</span><span class="s">"13"</span><span class="o">);</span>
<span class="c1">//批量一次性设置has的值</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">hset</span><span class="o">(</span><span class="n">hashKey</span><span class="o">,</span><span class="n">values</span><span class="o">);</span>
<span class="c1">//单个设置</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">hset</span><span class="o">(</span><span class="n">hashKey</span><span class="o">,</span><span class="s">"title"</span><span class="o">,</span><span class="s">"Jedis牛逼"</span><span class="o">);</span>
<span class="c1">//get操作</span>
<span class="c1">//获取单个</span>
<span class="n">String</span> <span class="n">hvalue</span><span class="o">=</span><span class="n">jedis</span><span class="o">.</span><span class="na">hget</span><span class="o">(</span><span class="n">hashKey</span><span class="o">,</span><span class="s">"name"</span><span class="o">);</span>
<span class="c1">//获取整个</span>
<span class="n">Map</span><span class="o"><</span><span class="n">String</span><span class="o">,</span><span class="n">String</span><span class="o">></span> <span class="n">hvalues</span><span class="o">=</span><span class="n">jedis</span><span class="o">.</span><span class="na">hgetAll</span><span class="o">(</span><span class="n">hashKey</span><span class="o">);</span>
<span class="c1">//删除操作</span>
<span class="c1">//提供可变数组,删除多个hash中的key值</span>
<span class="n">String</span><span class="o">[]</span> <span class="n">hdelKeys</span><span class="o">=</span><span class="k">new</span> <span class="n">String</span><span class="o">[]{</span><span class="s">"age"</span><span class="o">,</span><span class="s">"name"</span><span class="o">};</span>
<span class="c1">//删除某hash中的key</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">hdel</span><span class="o">(</span><span class="n">hashKey</span><span class="o">,</span><span class="n">hdelKeys</span><span class="o">);</span>
<span class="c1">//删除整个hash</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">del</span><span class="o">(</span><span class="n">hashKey</span><span class="o">);</span>
</code></pre></div></div>
<p>需要注意的是,针对批量设置hash值的操作,需要Redis版本大于等于4.0.0版本</p>
<blockquote>
<p>As of Redis 4.0.0, HSET is variadic and allows for multiple <code class="highlighter-rouge">field</code>/<code class="highlighter-rouge">value</code> pairs.</p>
</blockquote>
<p><strong>列表(list)</strong></p>
<p>列表中常用命令:<code class="highlighter-rouge">lpush</code>、<code class="highlighter-rouge">rpush</code>、<code class="highlighter-rouge">lpop</code>、<code class="highlighter-rouge">rpop</code>、<code class="highlighter-rouge">lrange</code>等</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//操作list</span>
<span class="n">Jedis</span> <span class="n">jedis</span><span class="o">=</span><span class="n">getSimpleJedis</span><span class="o">();</span>
<span class="n">String</span> <span class="n">listKey</span><span class="o">=</span><span class="s">"mylistkey"</span><span class="o">;</span>
<span class="n">String</span><span class="o">[]</span> <span class="n">listValues</span><span class="o">=</span><span class="k">new</span> <span class="n">String</span><span class="o">[]{</span><span class="s">"a"</span><span class="o">,</span><span class="s">"b"</span><span class="o">,</span><span class="s">"c"</span><span class="o">};</span>
<span class="c1">//放入集合中</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">lpush</span><span class="o">(</span><span class="n">listKey</span><span class="o">,</span><span class="n">listValues</span><span class="o">);</span>
<span class="c1">//根据区间获取集合中的元素集合</span>
<span class="n">List</span><span class="o"><</span><span class="n">String</span><span class="o">></span> <span class="n">rangeValues</span><span class="o">=</span><span class="n">jedis</span><span class="o">.</span><span class="na">lrange</span><span class="o">(</span><span class="n">listKey</span><span class="o">,</span><span class="mi">0</span><span class="o">,</span><span class="mi">2</span><span class="o">);</span>
<span class="n">rangeValues</span><span class="o">.</span><span class="na">forEach</span><span class="o">(</span><span class="n">s</span> <span class="o">-></span> <span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">s</span><span class="o">));</span>
<span class="c1">//从左侧链表取出一个值</span>
<span class="n">String</span> <span class="n">lvalue</span><span class="o">=</span><span class="n">jedis</span><span class="o">.</span><span class="na">lpop</span><span class="o">(</span><span class="n">listKey</span><span class="o">);</span>
<span class="c1">//此处值应该是c</span>
<span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"lpopValue:"</span><span class="o">+</span><span class="n">lvalue</span><span class="o">);</span>
<span class="n">String</span> <span class="n">rvalue</span><span class="o">=</span><span class="n">jedis</span><span class="o">.</span><span class="na">rpop</span><span class="o">(</span><span class="n">listKey</span><span class="o">);</span>
<span class="c1">//此处值应该是a</span>
<span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"rpopValue:"</span><span class="o">+</span><span class="n">rvalue</span><span class="o">);</span>
<span class="c1">//删除</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">del</span><span class="o">(</span><span class="n">listKey</span><span class="o">);</span>
</code></pre></div></div>
<p><strong>集合(Set)</strong></p>
<p>常用操作命令:<code class="highlighter-rouge">sadd</code>、<code class="highlighter-rouge">smembers</code>、<code class="highlighter-rouge">scard</code>、<code class="highlighter-rouge">sinter</code>、<code class="highlighter-rouge">sdiff</code>、<code class="highlighter-rouge">srem</code>等</p>
<p>集合(Set)和列表相比较而言,不会存在重复的值。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//操作Set</span>
<span class="n">Jedis</span> <span class="n">jedis</span> <span class="o">=</span> <span class="n">getSimpleJedis</span><span class="o">();</span>
<span class="n">String</span> <span class="n">setKey</span><span class="o">=</span><span class="s">"mysetKey"</span><span class="o">;</span>
<span class="c1">//此操作最终只会添加成功3个元素,因为存在abc重复</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">sadd</span><span class="o">(</span><span class="n">setKey</span><span class="o">,</span><span class="s">"abc"</span><span class="o">,</span><span class="s">"ddd"</span><span class="o">,</span><span class="s">"aaa"</span><span class="o">,</span><span class="s">"abc"</span><span class="o">);</span>
<span class="n">Set</span><span class="o"><</span><span class="n">String</span><span class="o">></span> <span class="n">stringSet</span><span class="o">=</span><span class="n">jedis</span><span class="o">.</span><span class="na">smembers</span><span class="o">(</span><span class="n">setKey</span><span class="o">);</span>
<span class="n">stringSet</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">forEach</span><span class="o">(</span><span class="n">s</span> <span class="o">-></span> <span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"SetValue:"</span><span class="o">+</span><span class="n">s</span><span class="o">));</span>
<span class="n">String</span> <span class="n">setKey1</span><span class="o">=</span><span class="s">"mysetKey1"</span><span class="o">;</span>
<span class="c1">//添加第二个集合</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">sadd</span><span class="o">(</span><span class="n">setKey1</span><span class="o">,</span><span class="s">"ab1c"</span><span class="o">,</span><span class="s">"ddd"</span><span class="o">,</span><span class="s">"aaa"</span><span class="o">,</span><span class="s">"2abc"</span><span class="o">);</span>
<span class="c1">//数量</span>
<span class="kt">long</span> <span class="n">set1Number</span><span class="o">=</span><span class="n">jedis</span><span class="o">.</span><span class="na">scard</span><span class="o">(</span><span class="n">setKey1</span><span class="o">);</span>
<span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"集合2数量:"</span><span class="o">+</span><span class="n">set1Number</span><span class="o">);</span>
<span class="c1">//比较第1个集合与其它集合的区别元素</span>
<span class="n">Set</span><span class="o"><</span><span class="n">String</span><span class="o">></span> <span class="n">diffSets</span><span class="o">=</span><span class="n">jedis</span><span class="o">.</span><span class="na">sdiff</span><span class="o">(</span><span class="n">setKey</span><span class="o">,</span><span class="n">setKey1</span><span class="o">);</span>
<span class="n">diffSets</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">forEach</span><span class="o">(</span><span class="n">s</span> <span class="o">-></span> <span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"diffValue:"</span><span class="o">+</span><span class="n">s</span><span class="o">));</span>
<span class="c1">//取交集</span>
<span class="n">Set</span><span class="o"><</span><span class="n">String</span><span class="o">></span> <span class="n">sinters</span><span class="o">=</span><span class="n">jedis</span><span class="o">.</span><span class="na">sinter</span><span class="o">(</span><span class="n">setKey</span><span class="o">,</span><span class="n">setKey1</span><span class="o">);</span>
<span class="n">sinters</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">forEach</span><span class="o">(</span><span class="n">s</span> <span class="o">-></span> <span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"sinterValue:"</span><span class="o">+</span><span class="n">s</span><span class="o">));</span>
<span class="c1">//取并集</span>
<span class="n">Set</span><span class="o"><</span><span class="n">String</span><span class="o">></span> <span class="n">sunions</span><span class="o">=</span><span class="n">jedis</span><span class="o">.</span><span class="na">sunion</span><span class="o">(</span><span class="n">setKey</span><span class="o">,</span><span class="n">setKey1</span><span class="o">);</span>
<span class="n">sunions</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">forEach</span><span class="o">(</span><span class="n">s</span> <span class="o">-></span> <span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"sunionsValue:"</span><span class="o">+</span><span class="n">s</span><span class="o">));</span>
<span class="c1">//获取两个集合的并集</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">del</span><span class="o">(</span><span class="n">setKey</span><span class="o">,</span><span class="n">setKey1</span><span class="o">);</span>
</code></pre></div></div>
<p><strong>有序集合(Sorted Set)</strong></p>
<p>常用操作命令:<code class="highlighter-rouge">zadd</code>、<code class="highlighter-rouge">zrange</code>、<code class="highlighter-rouge">zrank</code>、<code class="highlighter-rouge">zrangebyscore</code>、<code class="highlighter-rouge">zcard</code></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//操作Set</span>
<span class="n">Jedis</span> <span class="n">jedis</span> <span class="o">=</span> <span class="n">getSimpleJedis</span><span class="o">();</span>
<span class="n">String</span> <span class="n">zsetKey</span><span class="o">=</span><span class="s">"myzsetKey"</span><span class="o">;</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">zadd</span><span class="o">(</span><span class="n">zsetKey</span><span class="o">,</span><span class="mi">10</span><span class="o">,</span><span class="s">"abc"</span><span class="o">);</span>
<span class="c1">//多个一起添加</span>
<span class="n">Map</span><span class="o"><</span><span class="n">String</span><span class="o">,</span><span class="n">Double</span><span class="o">></span> <span class="n">zvalues</span><span class="o">=</span><span class="k">new</span> <span class="n">HashMap</span><span class="o"><>();</span>
<span class="n">zvalues</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"c1"</span><span class="o">,</span><span class="mi">230</span><span class="n">D</span><span class="o">);</span>
<span class="n">zvalues</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"c2"</span><span class="o">,</span><span class="mi">110</span><span class="n">D</span><span class="o">);</span>
<span class="n">zvalues</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"c3"</span><span class="o">,</span><span class="mi">130</span><span class="n">D</span><span class="o">);</span>
<span class="n">zvalues</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"c4"</span><span class="o">,</span><span class="mi">21</span><span class="n">D</span><span class="o">);</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">zadd</span><span class="o">(</span><span class="n">zsetKey</span><span class="o">,</span><span class="n">zvalues</span><span class="o">);</span>
<span class="c1">//统计数量</span>
<span class="kt">long</span> <span class="n">size</span><span class="o">=</span><span class="n">jedis</span><span class="o">.</span><span class="na">zcard</span><span class="o">(</span><span class="n">zsetKey</span><span class="o">);</span>
<span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"数据量:"</span><span class="o">+</span><span class="n">size</span><span class="o">);</span>
<span class="c1">//获取</span>
<span class="n">Set</span><span class="o"><</span><span class="n">String</span><span class="o">></span> <span class="n">stringSet</span><span class="o">=</span><span class="n">jedis</span><span class="o">.</span><span class="na">zrange</span><span class="o">(</span><span class="n">zsetKey</span><span class="o">,</span><span class="mi">0</span><span class="o">,</span><span class="mi">2</span><span class="o">);</span>
<span class="n">stringSet</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">forEach</span><span class="o">(</span><span class="n">s</span> <span class="o">-></span> <span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">s</span><span class="o">));</span>
<span class="c1">//倒序</span>
<span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"倒序"</span><span class="o">);</span>
<span class="n">stringSet</span><span class="o">=</span><span class="n">jedis</span><span class="o">.</span><span class="na">zrevrange</span><span class="o">(</span><span class="n">zsetKey</span><span class="o">,</span><span class="mi">0</span><span class="o">,</span><span class="mi">2</span><span class="o">);</span>
<span class="n">stringSet</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">forEach</span><span class="o">(</span><span class="n">s</span> <span class="o">-></span> <span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">s</span><span class="o">));</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">del</span><span class="o">(</span><span class="n">zsetKey</span><span class="o">);</span>
<span class="n">jedis</span><span class="o">.</span><span class="na">close</span><span class="o">();</span>
</code></pre></div></div>
<p>以上操作仅仅只是部分api的展示,更多的操作开发者需要自行探索,结合<a href="https://redis.io/commands">Redis的命令介绍</a>,才能更多的加深理解。</p>
<h4 id="122-使用lettuce操作redis">1.2.2 使用Lettuce操作Redis</h4>
<p>lettuce可能大家不常见,但是我们基于Spring Boot来操作的<code class="highlighter-rouge">StringRedisTemplate</code>大家肯定熟悉,如果开发者默认没有选择,那么<code class="highlighter-rouge">StringRedisTemplate</code>底层依赖的就是lettuce来作为默认依赖,最终来操作Redis。</p>肖玉民目前Redis支持的主要数据结构包含5种,分别是:跨语言跨平台聚合 OpenAPI 文档从来没有这么简单过2020-11-26T00:00:00+08:002020-11-26T00:00:00+08:00https://xiaoym.gitee.io/2020/11/26/easy-aggregation-openapi<p>Knife4j一直致力于将目前的Ui提供给更多的平台或者别的语言使用而努力,经过这么长时间的发展,Knife4j提供的轻量级聚合中间件终于诞生了,自<code class="highlighter-rouge">2.0.8</code>版本开始,Knife4j提供了<code class="highlighter-rouge">knife4j-aggregation-spring-boot-starter</code>组件,该组件是一个基于Spring Boot系统的starter,他提供了以下几种能力:</p>
<ul>
<li>最轻量级、最简单、最方便的聚合OpenApi规范的中间件</li>
<li>让所有的基于Spring Boot的Web体系拥有了轻松聚合OpenApi的能力</li>
<li>提供4种模式供开发者选择
<ul>
<li>基于本地静态JSON文件的方式聚合OpenAPI</li>
<li>基于云端HTTP接口的方式聚合</li>
<li>基于Eureka注册中心的方式聚合</li>
<li>基于Nacos注册中心的方式聚合</li>
</ul>
</li>
<li>基于该starter发布了Docker镜像,跨平台与语言让开发者基于此Docker镜像轻松进行聚合OpenAPI规范</li>
<li>完美兼容所有Spring Boot版本,没有兼容性问题</li>
<li>开发者可以彻底放弃基于Zuul、Spring Cloud Gateway等复杂的聚合方式</li>
<li>兼容OpenAPI2规范以及OpenAPI3规范</li>
</ul>
<p>本文主要介绍5种模式来使用Knife4j提供的聚合组件进行OpenAPI文档的聚合</p>
<ul>
<li>Disk本地文件模式</li>
<li>Cloud云端接口模式</li>
<li>Eureka注册中心</li>
<li>Nacos注册中心</li>
<li>Docker镜像跨语言</li>
</ul>
<h2 id="disk模式">Disk模式</h2>
<p>基于Disk模式聚合是最简单的,开发者只需要在Spring Boot的项目中存在OpenAPI规范的JSON文件即可进行聚合</p>
<p>完整代码请参考<a href="https://gitee.com/xiaoym/swagger-bootstrap-ui-demo/tree/master/knife4j-aggregation-disk-demo">knife4j-aggregation-disk-demo</a></p>
<p>主要步骤如下:</p>
<p>1、创建Spring Boot项目,引入<a href="https://xiaoym.gitee.io/knife4j/documentation/knife4jAggregation.html">Knife4jAggregation</a>的依赖包,完整pom文件如下:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><?xml version="1.0" encoding="UTF-8"?></span>
<span class="nt"><project</span> <span class="na">xmlns=</span><span class="s">"http://maven.apache.org/POM/4.0.0"</span> <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span>
<span class="na">xsi:schemaLocation=</span><span class="s">"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"</span><span class="nt">></span>
<span class="nt"><modelVersion></span>4.0.0<span class="nt"></modelVersion></span>
<span class="nt"><parent></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-starter-parent<span class="nt"></artifactId></span>
<span class="nt"><version></span>2.4.0<span class="nt"></version></span>
<span class="nt"><relativePath/></span> <span class="c"><!-- lookup parent from repository --></span>
<span class="nt"></parent></span>
<span class="nt"><groupId></span>com.github.xiaoymin<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>knife4j-aggregation-disk-demo<span class="nt"></artifactId></span>
<span class="nt"><version></span>0.0.1-SNAPSHOT<span class="nt"></version></span>
<span class="nt"><name></span>knife4j-aggregation-disk-demo<span class="nt"></name></span>
<span class="nt"><description></span>通过基于Spring Boot的工程聚合任意微服务接口文档<span class="nt"></description></span>
<span class="nt"><properties></span>
<span class="nt"><java.version></span>1.8<span class="nt"></java.version></span>
<span class="nt"></properties></span>
<span class="nt"><dependencies></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-starter-web<span class="nt"></artifactId></span>
<span class="nt"></dependency></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>com.github.xiaoymin<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>knife4j-aggregation-spring-boot-starter<span class="nt"></artifactId></span>
<span class="nt"><version></span>2.0.8<span class="nt"></version></span>
<span class="nt"></dependency></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-starter-test<span class="nt"></artifactId></span>
<span class="nt"><scope></span>test<span class="nt"></scope></span>
<span class="nt"></dependency></span>
<span class="nt"></dependencies></span>
<span class="nt"><build></span>
<span class="nt"><plugins></span>
<span class="nt"><plugin></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-maven-plugin<span class="nt"></artifactId></span>
<span class="nt"></plugin></span>
<span class="nt"></plugins></span>
<span class="nt"></build></span>
<span class="nt"></project></span>
</code></pre></div></div>
<p>2、配置yml配置文件,如下:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">server</span><span class="pi">:</span>
<span class="na">port</span><span class="pi">:</span> <span class="s">19081</span>
<span class="na">knife4j</span><span class="pi">:</span>
<span class="na">enableAggregation</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">disk</span><span class="pi">:</span>
<span class="na">enable</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">routes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">用户</span>
<span class="na">location</span><span class="pi">:</span> <span class="s">classpath:openapi/user.json</span>
</code></pre></div></div>
<p>工程目录如下图:</p>
<p><img src="https://xiaoym.gitee.io/knife4j/assert/aggregation/disk.png" alt="" /></p>
<p>3、启动项目,访问doc.html进行查看,效果图如下</p>
<p><img src="https://xiaoym.gitee.io/knife4j/assert/aggregation/disk-ui.png" alt="" /></p>
<h2 id="cloud模式聚合openapi文档">Cloud模式聚合OpenAPI文档</h2>
<p>Cloud(云端)模式和<a href="https://xiaoym.gitee.io/knife4j/action/aggregation-disk.html">Disk模式</a>大同小异,主要的区别是获取OpenAPI规范的方式换成了基于HTTP接口而已</p>
<p>完整代码请参考<a href="https://gitee.com/xiaoym/swagger-bootstrap-ui-demo/tree/master/knife4j-aggregation-cloud-demo">knife4j-aggregation-cloud-demo</a></p>
<p>本次Cloud聚合以Knife4j目前部署的线上demo为例,本地聚合在线的OpenAPI,并且可以本地调试,<a href="../documentation/knife4jAggregation.md">Knife4jAggregation</a>组件会自动帮助我们转发</p>
<p>任意取目前Knife4j的线上demo两个OpenAPI规范接口地址:</p>
<ul>
<li><a href="http://knife4j.xiaominfo.com/v2/api-docs?group=2.X%E7%89%88%E6%9C%AC">http://knife4j.xiaominfo.com/v2/api-docs?group=2.X版本</a></li>
<li><a href="http://knife4j.xiaominfo.com/v2/api-docs?group=3.%E9%BB%98%E8%AE%A4%E6%8E%A5%E5%8F%A3">http://knife4j.xiaominfo.com/v2/api-docs?group=3.默认接口</a></li>
</ul>
<p>主要步骤如下:</p>
<p>1、创建Spring Boot项目,引入<a href="../documentation/knife4jAggregation.md">Knife4jAggregation</a>的依赖包,完整pom文件如下:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><?xml version="1.0" encoding="UTF-8"?></span>
<span class="nt"><project</span> <span class="na">xmlns=</span><span class="s">"http://maven.apache.org/POM/4.0.0"</span> <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span>
<span class="na">xsi:schemaLocation=</span><span class="s">"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"</span><span class="nt">></span>
<span class="nt"><modelVersion></span>4.0.0<span class="nt"></modelVersion></span>
<span class="nt"><parent></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-starter-parent<span class="nt"></artifactId></span>
<span class="nt"><version></span>2.4.0<span class="nt"></version></span>
<span class="nt"><relativePath/></span> <span class="c"><!-- lookup parent from repository --></span>
<span class="nt"></parent></span>
<span class="nt"><groupId></span>com.github.xiaoymin<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>knife4j-aggregation-disk-demo<span class="nt"></artifactId></span>
<span class="nt"><version></span>0.0.1-SNAPSHOT<span class="nt"></version></span>
<span class="nt"><name></span>knife4j-aggregation-disk-demo<span class="nt"></name></span>
<span class="nt"><description></span>通过基于Spring Boot的工程聚合任意微服务接口文档<span class="nt"></description></span>
<span class="nt"><properties></span>
<span class="nt"><java.version></span>1.8<span class="nt"></java.version></span>
<span class="nt"></properties></span>
<span class="nt"><dependencies></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-starter-web<span class="nt"></artifactId></span>
<span class="nt"></dependency></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>com.github.xiaoymin<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>knife4j-aggregation-spring-boot-starter<span class="nt"></artifactId></span>
<span class="nt"><version></span>2.0.8<span class="nt"></version></span>
<span class="nt"></dependency></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-starter-test<span class="nt"></artifactId></span>
<span class="nt"><scope></span>test<span class="nt"></scope></span>
<span class="nt"></dependency></span>
<span class="nt"></dependencies></span>
<span class="nt"><build></span>
<span class="nt"><plugins></span>
<span class="nt"><plugin></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-maven-plugin<span class="nt"></artifactId></span>
<span class="nt"></plugin></span>
<span class="nt"></plugins></span>
<span class="nt"></build></span>
<span class="nt"></project></span>
</code></pre></div></div>
<p>2、配置yml配置文件,如下:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">server</span><span class="pi">:</span>
<span class="na">port</span><span class="pi">:</span> <span class="s">19081</span>
<span class="na">knife4j</span><span class="pi">:</span>
<span class="na">enableAggregation</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">cloud</span><span class="pi">:</span>
<span class="na">enable</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">routes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">测试分组1</span>
<span class="na">uri</span><span class="pi">:</span> <span class="s">knife4j.xiaominfo.com</span>
<span class="na">location</span><span class="pi">:</span> <span class="s">/v2/api-docs?group=2.X版本</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">测试分组2</span>
<span class="na">uri</span><span class="pi">:</span> <span class="s">knife4j.xiaominfo.com</span>
<span class="na">location</span><span class="pi">:</span> <span class="s">/v2/api-docs?group=3.默认接口</span>
</code></pre></div></div>
<p>3、启动项目,访问doc.html进行查看,效果图如下:</p>
<p>聚合效果:
<img src="https://xiaoym.gitee.io/knife4j/assert/aggregation/cloud1.png" alt="" /></p>
<p>在线调试:
<img src="https://xiaoym.gitee.io/knife4j/assert/aggregation/cloud.png" alt="" /></p>
<h2 id="eureka注册中心聚合openapi文档">Eureka注册中心聚合OpenAPI文档</h2>
<p>从Eureka注册中心进行聚合的模式和<a href="aggregation-cloud.md">Cloud模式</a>大同小异,主要的区别是通过<code class="highlighter-rouge">serviceName</code>来替代了真实的目标服务地址,而是从Eureka注册中心进行动态获取</p>
<p>完整代码请参考<a href="https://gitee.com/xiaoym/swagger-bootstrap-ui-demo/tree/master/knife4j-aggregation-eureka-demo">knife4j-aggregation-eureka-demo</a></p>
<p>先来看整个工程的目录:</p>
<p><img src="https://xiaoym.gitee.io/knife4j/assert/aggregation/eureka.png" alt="" /></p>
<p>工程目录说明如下:</p>
<table>
<thead>
<tr>
<th>工程</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>service-server</td>
<td>Eureka注册中心</td>
</tr>
<tr>
<td>service-user</td>
<td>一个非常简单的用户服务,包含用户接口</td>
</tr>
<tr>
<td>service-order</td>
<td>一个非常简单的订单服务,包含订单接口</td>
</tr>
<tr>
<td>service-doc</td>
<td>聚合文档工程,也是一个Spring Boot工程,不过需要注意的是基于web的,而非webflux</td>
</tr>
</tbody>
</table>
<p>Eureka注册中心以及service-user、order等都非常简单,按照注册中心、用户服务、订单服务依次进行启动即可</p>
<p>此时,我们访问Eureka的主页,最终能看到我们的注册中心存在两个服务,如下图:</p>
<p><img src="https://xiaoym.gitee.io/knife4j/assert/aggregation/eureka1.png" alt="" /></p>
<p>那么,我们的目标是什么呢?从Eureka注册中心直接进行聚合,也就是将用户服务、订单服务的OpenAPI文档聚合在一起进行展示</p>
<p>主要步骤如下:</p>
<p>1、在<code class="highlighter-rouge">service-doc</code>工程引入<code class="highlighter-rouge">knife4j-aggregation-spring-boot-starter</code>依赖</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><?xml version="1.0" encoding="UTF-8"?></span>
<span class="nt"><project</span> <span class="na">xmlns=</span><span class="s">"http://maven.apache.org/POM/4.0.0"</span> <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span>
<span class="na">xsi:schemaLocation=</span><span class="s">"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"</span><span class="nt">></span>
<span class="nt"><modelVersion></span>4.0.0<span class="nt"></modelVersion></span>
<span class="nt"><parent></span>
<span class="nt"><groupId></span>com.xiaominfo.swagger<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>knife4j-aggregation-eureka-demo<span class="nt"></artifactId></span>
<span class="nt"><version></span>1.0<span class="nt"></version></span>
<span class="nt"><relativePath></span>../pom.xml<span class="nt"></relativePath></span> <span class="c"><!-- lookup parent from repository --></span>
<span class="nt"></parent></span>
<span class="nt"><groupId></span>com.xiaominfo.swagger<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>service-doc<span class="nt"></artifactId></span>
<span class="nt"><version></span>0.0.1-SNAPSHOT<span class="nt"></version></span>
<span class="nt"><name></span>service-doc<span class="nt"></name></span>
<span class="nt"><description></span>Eureka聚合<span class="nt"></description></span>
<span class="nt"><properties></span>
<span class="nt"><java.version></span>1.8<span class="nt"></java.version></span>
<span class="nt"></properties></span>
<span class="nt"><dependencies></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-starter-web<span class="nt"></artifactId></span>
<span class="nt"></dependency></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>com.github.xiaoymin<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>knife4j-aggregation-spring-boot-starter<span class="nt"></artifactId></span>
<span class="nt"><version></span>2.0.8<span class="nt"></version></span>
<span class="nt"></dependency></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-starter-test<span class="nt"></artifactId></span>
<span class="nt"><scope></span>test<span class="nt"></scope></span>
<span class="nt"></dependency></span>
<span class="nt"></dependencies></span>
<span class="nt"><build></span>
<span class="nt"><plugins></span>
<span class="nt"><plugin></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-maven-plugin<span class="nt"></artifactId></span>
<span class="nt"></plugin></span>
<span class="nt"></plugins></span>
<span class="nt"></build></span>
<span class="nt"></project></span>
</code></pre></div></div>
<p>2、配置yml配置文件,如下:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">server</span><span class="pi">:</span>
<span class="na">port</span><span class="pi">:</span> <span class="s">10909</span>
<span class="na">knife4j</span><span class="pi">:</span>
<span class="na">enableAggregation</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">eureka</span><span class="pi">:</span>
<span class="na">enable</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">serviceUrl</span><span class="pi">:</span> <span class="s">http://localhost:10000/eureka/</span>
<span class="na">routes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">订单服务</span>
<span class="na">serviceName</span><span class="pi">:</span> <span class="s">service-order</span>
<span class="na">location</span><span class="pi">:</span> <span class="s">/v2/api-docs?group=default</span>
<span class="na">servicePath</span><span class="pi">:</span> <span class="s">/order</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">用户体系</span>
<span class="na">serviceName</span><span class="pi">:</span> <span class="s">service-user</span>
<span class="na">location</span><span class="pi">:</span> <span class="s">/aub/v2/api-docs?group=default</span>
<span class="na">servicePath</span><span class="pi">:</span> <span class="s">/</span>
</code></pre></div></div>
<p>3、启动项目,访问doc.html进行查看,效果图如下:</p>
<p>聚合效果:
<img src="https://xiaoym.gitee.io/knife4j/assert/aggregation/eureka2.png" alt="" /></p>
<p>在线调试:
<img src="https://xiaoym.gitee.io/knife4j/assert/aggregation/eureka3.png" alt="" /></p>
<p>只需要简单的配置,就轻松的将Eureka注册中心的各个服务进行了聚合,是不是比Spring Cloud Gateway、Zuul更加简单和轻量呢?</p>
<p>关于Eureka的更多配置需要开发者参考<a href="https://xiaoym.gitee.io/knife4j/documentation/knife4jAggregation.html">文档</a></p>
<h2 id="nacos注册中心聚合openapi文档">Nacos注册中心聚合OpenAPI文档</h2>
<p>Nacos的配置和<a href="https://xiaoym.gitee.io/knife4j/action/aggregation-eureka.html">Eureka</a>几乎一模一样,唯一不同的区别是在yml进行配置的时候,使用的是<code class="highlighter-rouge">knife4j.nacos</code>开头,其他基本都是一样</p>
<p>关于Nacos的更多配置需要开发者参考<a href="https://xiaoym.gitee.io/knife4j/documentation/knife4jAggregation.html">文档</a></p>
<h2 id="基于knife4j的docker镜像快速聚合openapi">基于Knife4j的Docker镜像快速聚合OpenAPI</h2>
<p>在前面的实战文章中,更多的是面向Java开发者,通过Spring Boot框架,快速聚合OpenAPI文档。</p>
<p>那么其他语言能否也能这么方便的使用Knife4j呢?</p>
<p>答案是肯定的,Knife4j为了让其他语言非常方便的使用Knife4j来渲染聚合OpenAPI文档,在DockerHub中推送了<a href="https://hub.docker.com/repository/docker/xiaoymin/knife4j">Knife4j的镜像</a>,</p>
<p>镜像地址:<a href="https://hub.docker.com/repository/docker/xiaoymin/knife4j">https://hub.docker.com/repository/docker/xiaoymin/knife4j</a></p>
<p>如果你的本机或者服务器安装了Docker,那么利用Knife4j的镜像来聚合OpenAPI将会非常方便</p>
<p>首先需要将镜像从DockerHub拉到本地,命令如下:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker pull xiaoymin/knife4j:latest
</code></pre></div></div>
<p>如果pull速度比较慢的话,开发者可以配置镜像源(<code class="highlighter-rouge">/etc/docker/daemon.json</code>)</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="s2">"registry-mirrors"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"https://registry.docker-cn.com"</span><span class="p">,</span><span class="w">
</span><span class="s2">"http://hub-mirror.c.163.com"</span><span class="p">,</span><span class="w">
</span><span class="s2">"https://3laho3y3.mirror.aliyuncs.com"</span><span class="p">,</span><span class="w">
</span><span class="s2">"https://mirror.ccs.tencentyun.com"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>镜像下载到本地机器后,下面将详细介绍如何通过Knife4j的镜像实现上面文章介绍的4中不同方式的聚合OpenAPI文档</p>
<h2 id="镜像说明">镜像说明</h2>
<p>Knife4j的镜像是一个基于Spring Boot框架开发的Web项目,对外默认端口<code class="highlighter-rouge">8888</code></p>
<p>源码地址:<a href="https://gitee.com/xiaoym/knife4j/tree/v2/knife4j-aggregation-docker">https://gitee.com/xiaoym/knife4j/tree/v2/knife4j-aggregation-docker</a></p>
<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> openjdk:8-jdk-alpine</span>
<span class="k">LABEL</span><span class="s"> version="2.0"</span>
<span class="k">LABEL</span><span class="s"> released-date=2020/11/25</span>
<span class="k">LABEL</span><span class="s"> author="xiaoymin@foxmail.com"</span>
<span class="k">LABEL</span><span class="s"> description="Knife4jAggregation OpenAPI,RunAnyWhere!!!"</span>
<span class="k">MAINTAINER</span><span class="s"> xiaoymin</span>
<span class="k">RUN </span>mkdir /app
<span class="c"># Disk模式数据挂载目录</span>
<span class="k">RUN </span>mkdir /app/data
<span class="k">ADD</span><span class="s"> src/main/resources/application.yml /app/app.yml</span>
<span class="k">ADD</span><span class="s"> target/knife4j-aggregation-docker-1.0.jar /app/app.jar</span>
<span class="k">ENTRYPOINT</span><span class="s"> ["java","-jar","-Djava.security.egd=file:/dev/./urandom","-Duser.timezone=Asia/Shanghai","/app/app.jar","--spring.config.location=/app/app.yml"]</span>
<span class="c">#EXPOSE 8888:</span>
</code></pre></div></div>
<p>从Knife4j的Dockerfile文件中,我们可以看到为Knife4j的应用创建了一个<code class="highlighter-rouge">/app</code>目录和<code class="highlighter-rouge">/app/data</code>目录,用来存放jar文件和yml配置文件,该目录是通过外部文件与Docker容器进行挂载关联的关键。</p>
<h3 id="disk模式-1">Disk模式</h3>
<p>Disk模式主要是从本地聚合OpenAPI规范,那么如何利用Knife4j的容器进行渲染呢?这里就要用到我们刚刚上面说的文件挂载</p>
<p>第一步:在服务器(宿主机)上创建相关目录,例如:<code class="highlighter-rouge">/home/openapi</code></p>
<p>我们在该目录下主要存放两种类型的文件目录,1种是Knife4j镜像文件需要的yml配置文件,第二种是存放OpenAPI的规范JSON,目录结构如下:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">[</span>root@izbpc3 openapi]# <span class="nb">pwd</span>
/home/openapi
<span class="o">[</span>root@izbpc3 openapi]# ll
total 8
<span class="nt">-rw-r--r--</span> 1 root root 241 Nov 25 19:42 app.yml
drwxr-xr-x 2 root root 4096 Nov 25 19:41 data
<span class="o">[</span>root@izbpc3 openapi]# <span class="nb">cd </span>data
<span class="o">[</span>root@izbpc3 data]# ll
total 256
<span class="nt">-rw-r--r--</span> 1 root root 21448 Nov 25 19:41 open-api.json
<span class="nt">-rw-r--r--</span> 1 root root 237303 Nov 25 19:41 openapi.json
</code></pre></div></div>
<p>Disk模式我们主要需要做的是修改app.yml配置文件中的配置,指定Knife4j的镜像从本地加载指定的openapi.json,通过界面显示</p>
<p><code class="highlighter-rouge">app.yml</code>配置修改如下:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">server</span><span class="pi">:</span>
<span class="na">port</span><span class="pi">:</span> <span class="s">8888</span>
<span class="na">knife4j</span><span class="pi">:</span>
<span class="na">enableAggregation</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">disk</span><span class="pi">:</span>
<span class="na">enable</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">routes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">用户AAAAAAAAAAA</span>
<span class="na">location</span><span class="pi">:</span> <span class="s">/app/data/open-api.json</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">用户BBBBBBBBBBBB</span>
<span class="na">location</span><span class="pi">:</span> <span class="s">/app/data/openapi.json</span>
</code></pre></div></div>
<p>这里需要注意的是</p>
<p>1、location我们使用的是容器的目录<code class="highlighter-rouge">/app</code>,我们最终创建容器的时候会将宿主机的目录(<code class="highlighter-rouge">/home/openapi/data</code>)挂载给容器,达到文件共享的目的</p>
<p>2、在<code class="highlighter-rouge">app.yml</code>配置中指定的端口是容器的端口,Knife4j默认端口8888,如果开发者使用该配置并且修改了端口,那么需要在端口映射的时候也相应的进行修改</p>
<p>第二步:启动Knife4j容器查看效果</p>
<p>通过Docker命令创建容器,命令如下:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">[</span>root@izbx23 app]# docker run <span class="nt">-itd</span> <span class="nt">--name</span> myopenapi <span class="nt">-p</span> 18002:8888 <span class="nt">-v</span> /home/openapi/app.yml:/app/app.yml <span class="nt">-v</span> /home/openapi/data:/app/data xiaoymin/knife4j
3f0ed4cde46dd8a625e0338bc8cb1688059c7169447bda5681a34d93e2ba7c3e
<span class="o">[</span>root@izbx23 app]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e678bccd4d66 xiaoymin/knife4j <span class="s2">"java -jar -Djava.se…"</span> 3 seconds ago Up 2 seconds 0.0.0.0:18002->8888/tcp myopenapi
</code></pre></div></div>
<ul>
<li>–name命令是指定一个别名</li>
<li>-p代表端口映射 18002是宿主机端口号,8888是容器的端口号,</li>
<li>-v参数则是将本地目录挂载和容器共享,此处主要挂载两个文件,一个是app.yml配置文件,一个是openapi.json文件</li>
</ul>
<p>此时,我们通过端口号进行访问:<code class="highlighter-rouge">http://localhost:18002/doc.html</code></p>
<p>效果图如下:</p>
<p><img src="https://xiaoym.gitee.io/knife4j/assert/aggregation/docker-disk.png" alt="" /></p>
<p>容器创建成功后,我们可以访问容器的文件系统,通过命令如下:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">[</span>root@izbx23 conf.d]# docker <span class="nb">exec</span> <span class="nt">-it</span> myopenapi sh
/ <span class="c"># ls</span>
app bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
/ <span class="c"># cd app</span>
/app <span class="c"># ls</span>
app.jar app.yml data
/app <span class="c"># cd data</span>
/app/data <span class="c"># ls</span>
open-api.json openapi.json
/app/data <span class="c"># </span>
</code></pre></div></div>
<p>我们在容器中的文件系统中<code class="highlighter-rouge">/app/data</code>目录中,其实可以看到,这个目录中的文件和我们通过创建容器时-v参数挂载的目录文件是一致的。</p>
<h3 id="cloud模式">Cloud模式</h3>
<p>Cloud模式就相对简单多了,我们只需要修改当前的app.yml配置文件即可,然后创建容器时进行覆盖即可</p>
<p>任意取目前Knife4j的线上demo两个OpenAPI规范接口地址:</p>
<ul>
<li><a href="http://knife4j.xiaominfo.com/v2/api-docs?group=2.X%E7%89%88%E6%9C%AC">http://knife4j.xiaominfo.com/v2/api-docs?group=2.X版本</a></li>
<li><a href="http://knife4j.xiaominfo.com/v2/api-docs?group=3.%E9%BB%98%E8%AE%A4%E6%8E%A5%E5%8F%A3">http://knife4j.xiaominfo.com/v2/api-docs?group=3.默认接口</a></li>
</ul>
<p>配置yml配置文件,如下:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">server</span><span class="pi">:</span>
<span class="na">port</span><span class="pi">:</span> <span class="s">8888</span>
<span class="na">knife4j</span><span class="pi">:</span>
<span class="na">enableAggregation</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">cloud</span><span class="pi">:</span>
<span class="na">enable</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">routes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">cloud1</span>
<span class="na">uri</span><span class="pi">:</span> <span class="s">knife4j.xiaominfo.com</span>
<span class="na">location</span><span class="pi">:</span> <span class="s">/v2/api-docs?group=2.X版本</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">cloud2</span>
<span class="na">uri</span><span class="pi">:</span> <span class="s">knife4j.xiaominfo.com</span>
<span class="na">location</span><span class="pi">:</span> <span class="s">/v2/api-docs?group=3.默认接口</span>
</code></pre></div></div>
<p>通过Docker命令创建容器,命令如下:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="o">[</span>root@izbx23 openapi]# docker run <span class="nt">-itd</span> <span class="nt">--name</span> cloudapi <span class="nt">-p</span> 18002:8888 <span class="nt">-v</span> /home/openapi/app.yml:/app/app.yml xiaoymin/knife4j
6b81844e0c605704eef3ffcb207e090a1139a9fbc8dcf0a43efdcb60f41d327c
<span class="o">[</span>root@izbx23 openapi]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6b81844e0c60 xiaoymin/knife4j <span class="s2">"java -jar -Djava.se…"</span> 4 seconds ago Up 3 seconds 0.0.0.0:18002->8888/tcp cloudapi
</code></pre></div></div>
<ul>
<li>–name命令是指定一个别名(<code class="highlighter-rouge">cloudapi</code>)</li>
<li>-p代表端口映射 18002是宿主机端口号,8888是容器的端口号,</li>
<li>-v参数则是将本地目录挂载和容器共享,此处只需要覆盖app.yml配置文件即可,因为我们的OpenAPI数据来源于HTTP接口</li>
</ul>
<p>此时,我们通过端口号进行访问:<code class="highlighter-rouge">http://localhost:18002/doc.html</code></p>
<p>效果图如下:</p>
<p><img src="https://xiaoym.gitee.io/knife4j/assert/aggregation/docker-cloud.png" alt="" /></p>
<h3 id="注册中心eureka--nacos">注册中心(Eureka && Nacos)</h3>
<p>至于从注册中心(Eureka && Nacos)进行OpenAPI的聚合和Cloud模式下一样,开发者只需要修改app.yml配置文件,然后通过-v参数进行挂载覆盖文件即可。更多的配置需要参考聚合组件的文档参数<a href="https://xiaoym.gitee.io/knife4j/documentation/knife4jAggregation.html">详细介绍文档</a></p>肖玉民Knife4j一直致力于将目前的Ui提供给更多的平台或者别的语言使用而努力,经过这么长时间的发展,Knife4j提供的轻量级聚合中间件终于诞生了,自2.0.8版本开始,Knife4j提供了knife4j-aggregation-spring-boot-starter组件,该组件是一个基于Spring Boot系统的starter,他提供了以下几种能力:Knife4j 2.0.8发布,轻量级微服务聚合文档中间件诞生2020-11-22T00:00:00+08:002020-11-22T00:00:00+08:00https://xiaoym.gitee.io/2020/11/22/knife4j-2.0.8-issue<p><code class="highlighter-rouge">Knife4j</code>前身是<code class="highlighter-rouge">swagger-bootstrap-ui</code>,是一个为Swagger接口文档赋能的工具</p>
<p><strong>文档</strong>:<a href="https://xiaoym.gitee.io/knife4j/">https://xiaoym.gitee.io/knife4j/</a></p>
<p><strong>效果(旧版)</strong>:<a href="http://swagger-bootstrap-ui.xiaominfo.com/doc.html">http://swagger-bootstrap-ui.xiaominfo.com/doc.html</a></p>
<p><strong>效果(2.X版)</strong>:<a href="http://knife4j.xiaominfo.com/doc.html">http://knife4j.xiaominfo.com/doc.html</a></p>
<p><strong>Gitee</strong>:<a href="https://gitee.com/xiaoym/knife4j">https://gitee.com/xiaoym/knife4j</a></p>
<p><strong>GitHub</strong>:<a href="https://github.com/xiaoymin/swagger-bootstrap-ui">https://github.com/xiaoymin/swagger-bootstrap-ui</a></p>
<p><strong>示例</strong>:<a href="https://gitee.com/xiaoym/swagger-bootstrap-ui-demo">https://gitee.com/xiaoym/swagger-bootstrap-ui-demo</a></p>
<h2 id="特性--优化">特性 & 优化</h2>
<p>1、构建响应curl时,去除Knife4j自定义添加的部分Header头</p>
<p>2、增加自定义主页的增强配置,开发者可以提供一个Markdown文档,用来自定义Home主页显示的内容<a href="https://gitee.com/xiaoym/knife4j/issues/I24ZXI">Gitee #I24ZXI</a></p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">knife4j</span><span class="pi">:</span>
<span class="err"> </span><span class="na">enable</span><span class="pi">:</span> <span class="no">true</span>
<span class="err"> </span><span class="na">setting</span><span class="pi">:</span>
<span class="err"> </span><span class="c1"># 是否自定义显示Home主页,默认为false</span>
<span class="err"> </span><span class="na">enableHomeCustom</span><span class="pi">:</span> <span class="no">true</span>
<span class="err"> </span><span class="c1"># 自定义主页Home的markdown文档路径,只能设置1个,如果设置为目录,则默认取第一个</span>
<span class="err"> </span><span class="na">homeCustomLocation</span><span class="pi">:</span> <span class="s">classpath:markdown/home.md</span>
</code></pre></div></div>
<p>3、OpenAPI开放接口可以通过增强配置是否显示<a href="https://gitee.com/xiaoym/knife4j/issues/I25273">Gitee #I25273</a></p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">knife4j</span><span class="pi">:</span>
<span class="err"> </span><span class="na">enable</span><span class="pi">:</span> <span class="no">true</span>
<span class="err"> </span><span class="na">setting</span><span class="pi">:</span>
<span class="err"> </span><span class="c1"># 是否显示文档中的Open标签栏,默认为true</span>
<span class="err"> </span><span class="na">enableOpenApi</span><span class="pi">:</span> <span class="no">false</span>
</code></pre></div></div>
<p>4、搜索框可以通过增强配置是否显示<a href="https://gitee.com/xiaoym/knife4j/issues/I24ZYY">Gitee #I24ZYY</a></p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">knife4j</span><span class="pi">:</span>
<span class="err"> </span><span class="na">enable</span><span class="pi">:</span> <span class="no">true</span>
<span class="err"> </span><span class="na">setting</span><span class="pi">:</span>
<span class="err"> </span><span class="c1"># 是否显示文档中的搜索框,默认为true,即显示</span>
<span class="err"> </span><span class="na">enableSearch</span><span class="pi">:</span> <span class="no">false</span>
</code></pre></div></div>
<p>5、文档最下边的footerkey通过增强配置是否显示,并且可以自定义显示内容<a href="https://gitee.com/xiaoym/knife4j/issues/I24ZYD">Gitee #I24ZYD</a></p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">knife4j</span><span class="pi">:</span>
<span class="err"> </span><span class="na">enable</span><span class="pi">:</span> <span class="no">true</span>
<span class="err"> </span><span class="na">setting</span><span class="pi">:</span>
<span class="err"> </span><span class="c1"># 是否不显示Knife4j默认的footer,默认为true(显示)</span>
<span class="err"> </span><span class="na">enableFooter</span><span class="pi">:</span> <span class="no">false</span>
<span class="err"> </span><span class="c1"># 是否自定义Footer,默认为false(非自定义)</span>
<span class="err"> </span><span class="na">enableFooterCustom</span><span class="pi">:</span> <span class="no">true</span>
<span class="err"> </span><span class="c1"># 自定义Footer内容,支持Markdown语法</span>
<span class="err"> </span><span class="na">footerCustomContent</span><span class="pi">:</span> <span class="s">中国XXX科技股份有限公司版权所有</span>
</code></pre></div></div>
<p>6、废弃springfox中的控制参数接口<code class="highlighter-rouge">/swagger-resources/configuration/ui</code>,针对是否开启Debug调试,通过Knife4j提供的个性化增强配置进行控制</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">knife4j</span><span class="pi">:</span>
<span class="err"> </span><span class="na">enable</span><span class="pi">:</span> <span class="no">true</span>
<span class="err"> </span><span class="na">setting</span><span class="pi">:</span>
<span class="err"> </span><span class="c1"># 是否显示调试Tab框架,默认为true(显示)</span>
<span class="err"> </span><span class="na">enableDebug</span><span class="pi">:</span> <span class="no">false</span>
</code></pre></div></div>
<p>7、解决微服务架构下,丢失basePath的问题<a href="https://gitee.com/xiaoym/knife4j/issues/I23NWM">Gitee #I23NWM</a>、<a href="https://gitee.com/xiaoym/knife4j/issues/I23N6L">Gitee #I23N6L</a>、<a href="https://gitee.com/xiaoym/knife4j/issues/I25ZTC">Gitee #I25ZTC</a>、<a href="https://github.com/xiaoymin/swagger-bootstrap-ui/issues/286">GitHub #286</a></p>
<p>8、自定义文档以及自定义Home主页的Markdown支持Html语法<a href="https://gitee.com/xiaoym/knife4j/issues/I24ZZA">Gitee #I24ZZA</a></p>
<p>9、去除文档右上角?号的文档显示<a href="https://gitee.com/xiaoym/knife4j/issues/I24ZYL">Gitee #I24ZYL</a></p>
<p>10、增强配置增加开启动态请求参数配置的配置<a href="https://gitee.com/xiaoym/knife4j/issues/I24EBO">Gitee #I24EBO</a></p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">knife4j</span><span class="pi">:</span>
<span class="err"> </span><span class="na">enable</span><span class="pi">:</span> <span class="no">true</span>
<span class="err"> </span><span class="na">setting</span><span class="pi">:</span>
<span class="err"> </span><span class="c1"># 开启动态请求参数调试,默认为false(不开启)</span>
<span class="err"> </span><span class="na">enableDynamicParameter</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>
<p>11、如果当前服务只有一个分组的情况下,开发者可以通过配置<code class="highlighter-rouge">enableGroup</code>项来控制界面的分组显示<a href="https://gitee.com/xiaoym/knife4j/issues/I25MQG">Gitee #I25MQG</a>,配置如下:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">knife4j</span><span class="pi">:</span>
<span class="err"> </span><span class="na">enable</span><span class="pi">:</span> <span class="no">true</span>
<span class="err"> </span><span class="na">setting</span><span class="pi">:</span>
<span class="err"> </span><span class="c1"># Ui界面不显示分组元素</span>
<span class="err"> </span><span class="na">enableGroup</span><span class="pi">:</span> <span class="no">false</span>
</code></pre></div></div>
<p>最终效果图如下:</p>
<p><img src="/images/blog/knife4j2.0.7/groupshow.png" alt="" /></p>
<p>12、基础类型的请求参数与响应参数示例显示优化<a href="https://gitee.com/xiaoym/knife4j/issues/I24YKT">Gitee #I24YKT</a></p>
<p>13、<code class="highlighter-rouge">@ApiOperationSupport</code>和<code class="highlighter-rouge">@DynamicParameters</code>注解不能同时使用的问题<a href="https://gitee.com/xiaoym/knife4j/issues/I24JWV">Gitee #I24JWV</a></p>
<p>14、解决V3版本中starter存在冲突的问题<a href="https://gitee.com/xiaoym/knife4j/issues/I2420J">Gitee #I2420J</a></p>
<p>15、优化markdown渲染的组件方式。</p>
<p>16、离线文档导出移除<strong>导出PDF</strong>项,导出pdf功能不管是基于markdown或者是word都能轻松实现,因此Knife4j废弃此功能</p>
<p>17、OpenAPI3结构中支持表单类型中scheme解析显示为json<a href="https://gitee.com/xiaoym/knife4j/issues/I24PCZ">Gitee #I24PCZ</a></p>
<p>18、针对Authorize标志的接口,添加锁的icon在接口中进行体现<a href="https://gitee.com/xiaoym/knife4j/issues/I23W0S">Gitee #I23W0S</a>
<img src="/images/blog/knife4j2.0.7/authorize1.png" alt="" /></p>
<p><img src="/images/blog/knife4j2.0.7/authorize2.png" alt="" /></p>
<p>19、增强配置本地缓存更新策略</p>
<p>20、针对禁用文档管理菜单项后,同步禁用右上角个性化菜单的显示。<a href="I262VN">Gitee #I262VN</a></p>
<p>21、请求OpenAPI规范实例接口默认发送一个<code class="highlighter-rouge">language</code>的header,如果服务端做了i18n的配置可以根据此header动态返回不同的语言释义。</p>
<p>21、解决根据路径设置界面i18n显示时,和服务端增强配置冲突的问题,如果开发者通过url路径来设置界面的i18n显示,则默认以路径中的为准,否则,取后端增强配置的<code class="highlighter-rouge">language</code></p>
<p>22、菜单收缩时显示存在异常的问题<a href="https://gitee.com/xiaoym/knife4j/issues/I2646F">Gitee #I2646F</a></p>
<p>23、OpenAPI3规范适配支持JSR303支持<a href="https://github.com/xiaoymin/swagger-bootstrap-ui/issues/283">GitHub #283</a></p>
<p>24、请求参数的数据类型为空的情况下优化,显示默认值<code class="highlighter-rouge">string</code></p>
<h2 id="使用方法">使用方法</h2>
<p>Java开发使用<code class="highlighter-rouge">Knife4j</code>目前有一些不同的版本变化,主要如下:</p>
<p>1、如果开发者继续使用OpenAPI2的规范结构,底层框架依赖springfox2.10.5版本,那么可以考虑<code class="highlighter-rouge">Knife4j</code>的2.x版本</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><dependency></span>
<span class="nt"><groupId></span>com.github.xiaoymin<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>knife4j-spring-boot-starter<span class="nt"></artifactId></span>
<span class="c"><!--在引用时请在maven中央仓库搜索2.X最新版本号--></span>
<span class="nt"><version></span>2.0.8<span class="nt"></version></span>
<span class="nt"></dependency></span>
</code></pre></div></div>
<p>2、如果开发者使用OpenAPI3的结构,底层框架依赖springfox3.0.0,可以考虑<code class="highlighter-rouge">Knife4j</code>的3.x版本</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><dependency></span>
<span class="nt"><groupId></span>com.github.xiaoymin<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>knife4j-spring-boot-starter<span class="nt"></artifactId></span>
<span class="c"><!--在引用时请在maven中央仓库搜索3.X最新版本号--></span>
<span class="nt"><version></span>3.0.2<span class="nt"></version></span>
<span class="nt"></dependency></span>
</code></pre></div></div>
<p>3、如果开发者底层框架使用的是<code class="highlighter-rouge">springdoc-openapi</code>框架,则需要使用<code class="highlighter-rouge">Knife4j</code>提供的对应版本,需要注意的是该版本没有<code class="highlighter-rouge">Knife4j</code>提供的增强功能,是一个纯Ui。</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><dependency></span>
<span class="nt"><groupId></span>com.github.xiaoymin<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>knife4j-springdoc-ui<span class="nt"></artifactId></span>
<span class="c"><!--在引用时请在maven中央仓库搜索3.X最新版本号--></span>
<span class="nt"><version></span>3.0.2<span class="nt"></version></span>
<span class="nt"></dependency></span>
</code></pre></div></div>
<h2 id="knife4jaggregation微服务聚合中间件">Knife4jAggregation微服务聚合中间件</h2>
<p>自<code class="highlighter-rouge">2.0.8</code>版本开始,Knife4j提供了轻量级的聚合微服务OpenAPI文档的中间件,可以在任意Spring Boot服务中聚合文档,最简单、最轻量级、最方便的聚合组件</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><dependency></span>
<span class="nt"><groupId></span>com.github.xiaoymin<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>knife4j-aggregation-spring-boot-starter<span class="nt"></artifactId></span>
<span class="c"><!--在引用时请在maven中央仓库搜索Knife4jAggregation最新版本号--></span>
<span class="nt"><version></span>2.0.8<span class="nt"></version></span>
<span class="nt"></dependency></span>
</code></pre></div></div>
<p>该组件提供了4种不同的模式以满足不同语言、不同模式的方式进行OpenAPI文档的聚合</p>
<p>四种不同的方式:</p>
<ul>
<li>Disk本地模式</li>
<li>Cloud云端接口模式</li>
<li>Eureka注册中心模式</li>
<li>Nacos注册中心模式</li>
</ul>
<p>更详细的介绍以及实战使用方法请参考<a href="https://xiaoym.gitee.io/knife4j/documentation/knife4jAggregation.html">文档</a></p>
<h2 id="特点">特点</h2>
<ul>
<li>基于Vue+Ant Design构建的文档,更强大、清晰的接口文档说明能力以及接口调试能力</li>
<li>左右布局,基于Tabs组件的多文档查阅风格</li>
<li>支持在线导出Html、Markdown、Word、PDF等多种格式的离线文档</li>
<li>接口排序,支持分组及接口的排序功能</li>
<li>支持接口全局在线搜索功能</li>
<li>提供Swagger资源保护策略,保护文档安全</li>
<li>接口调试支持无限参数,开发者调试非常灵活,动态增加、删除参数</li>
<li>全局缓存调试信息,页面刷新后依然存在,方便开发者调试</li>
<li>以更人性化的table树组件展示Swagger Models功能</li>
<li>文档以多tab方式可显示多个接口文档</li>
<li>请求参数栏请求类型、是否必填着颜色区分</li>
<li>主页中粗略统计接口不同类型数量</li>
<li>支持自定义全局参数功能,主页包括header及query两种类型</li>
<li>JSR-303 annotations 注解的支持</li>
<li>更多个性化设置功能</li>
</ul>
<h2 id="界面">界面</h2>
<p>接口文档显示界面如下:</p>
<p><img src="https://oscimg.oschina.net/oscnet/up-5b76509501c86174096f8b795d2aba8455b.png" alt="" /></p>
<p>接口调试界面如下:</p>
<p><img src="https://oscimg.oschina.net/oscnet/up-995a784219ea035cacd428d15d04e9cbcb3.png" alt="" /></p>
<p>Swagger Models功能</p>
<p><img src="https://oscimg.oschina.net/oscnet/up-98e1dbdf35ab957f5c05df126f9bae44ffd.png" alt="" /></p>
<p><img src="https://oscimg.oschina.net/oscnet/up-d9a030b06b76f9a4935205df453af149788.png" alt="" /></p>
<p>支持导出离线Markdown、Html功能,markdown的表格较原先版本通过缩减显示为树形结构,<a href="https://doc.xiaominfo.com/html/knife4j-export-html.html">点击预览导出离线Html效果</a>,效果图如下:</p>
<p><img src="https://oscimg.oschina.net/oscnet/up-cfb2256485835e29a39f96eaaa60251e08c.png" alt="" /></p>
<p>通过第三方Markdown软件导出的PDF效果如下图:</p>
<p><img src="https://oscimg.oschina.net/oscnet/up-44bb80532b9767a33650e178809f612c3e7.png" alt="" /></p>
<p>同时提供了导出离线Html功能,Html功能界面风格和在线几乎没有区别,美观、大方、简洁,<a href="https://doc.xiaominfo.com/Knife4j-Offline-Html.html">点击在线预览效果</a>,</p>
<p>界面效果如下图:</p>
<p><img src="https://oscimg.oschina.net/oscnet/up-4deb56c65774b4dc2ad54f5278f31e89a5d.png" alt="" /></p>
<h2 id="star--issue">Star & Issue</h2>
<p>感谢各位朋友的支持,前往<a href="https://gitee.com/xiaoym/knife4j">https://gitee.com/xiaoym/knife4j</a>点个Star吧~~ :)</p>肖玉民Knife4j前身是swagger-bootstrap-ui,是一个为Swagger接口文档赋能的工具Knife4j 2.0.7发布,细节处理2020-11-02T00:00:00+08:002020-11-02T00:00:00+08:00https://xiaoym.gitee.io/2020/11/02/knife4j-2.0.7-issue<p><code class="highlighter-rouge">Knife4j</code>前身是<code class="highlighter-rouge">swagger-bootstrap-ui</code>,是一个为Swagger接口文档赋能的工具</p>
<p><strong>文档</strong>:<a href="https://doc.xiaominfo.com/">https://doc.xiaominfo.com</a></p>
<p><strong>效果(旧版)</strong>:http://swagger-bootstrap-ui.xiaominfo.com/doc.html</p>
<p><strong>效果(2.X版)</strong>:<a href="http://knife4j.xiaominfo.com/doc.html">http://knife4j.xiaominfo.com/doc.html</a></p>
<p><strong>Gitee</strong>:https://gitee.com/xiaoym/knife4j</p>
<p><strong>GitHub</strong>:https://github.com/xiaoymin/swagger-bootstrap-ui</p>
<p><strong>示例</strong>:https://gitee.com/xiaoym/swagger-bootstrap-ui-demo</p>
<h2 id="特性--优化">特性 & 优化</h2>
<p>1、服务端创建Docket对象时配置<code class="highlighter-rouge">globalOperationParameters</code>参数时,header类型不选中或丢失的问题</p>
<p>2、如果服务端写会的json参数中包含base64的图片格式,在响应栏增加图片标签直接显示</p>
<p><img src="/images/blog/knife4j2.0.7/base1.png" alt="" /></p>
<p><img src="/images/blog/knife4j2.0.7/base2.png" alt="" /></p>
<p>3、springfox升级到2.10.5版本后,针对basePath会在解析时自动追加到path节点,因为以前的版本没有追加,所以会导致重复添加basePath的问题。<a href="https://gitee.com/xiaoym/knife4j/issues/I230K8">Gitee #I230K8</a>、<a href="https://gitee.com/xiaoym/knife4j/issues/I23G5V">Gitee #I23G5V</a></p>
<p>4、导出出md离线文档请求参数部分字段的设置和文档中同步<a href="https://gitee.com/xiaoym/knife4j/issues/I22UFA">Gitee #I22UFA</a></p>
<p>5、字段参数说明支持<code class="highlighter-rouge">html</code>标签样式。<a href="https://gitee.com/xiaoym/knife4j/issues/I22RZ2">Gitee #I22RZ2</a></p>
<p>示例代码:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@ApiModelProperty</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"奖金名称,记住:<br /><span style=\"color:red\">我很重要</span>"</span><span class="o">,</span><span class="n">example</span> <span class="o">=</span> <span class="s">"MVP奖杯"</span><span class="o">)</span>
<span class="kd">private</span> <span class="n">String</span> <span class="n">name</span><span class="o">;</span>
</code></pre></div></div>
<p>效果图:</p>
<p><img src="/images/blog/knife4j2.0.7/supporthtml.png" alt="" /></p>
<p>6、默认去除小蓝点的版本控制,开发者可以通过在服务端通过配置进行开启,详情请参考<a href="https://doc.xiaominfo.com/knife4j/enhance.html">增强文档</a>。</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">knife4j</span><span class="pi">:</span>
<span class="na">enable</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">setting</span><span class="pi">:</span>
<span class="err"> </span><span class="c1">#是否开启界面中对某接口的版本控制,如果开启,后端接口变化后Ui界面会存在小蓝点</span>
<span class="na">enableVersion</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>
<p>7、可以通过配置重命名界面Swagger Models的命名,详情请参考<a href="https://doc.xiaominfo.com/knife4j/enhance.html">增强文档</a>,例如:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">knife4j</span><span class="pi">:</span>
<span class="na">enable</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">setting</span><span class="pi">:</span>
<span class="na">enableSwaggerModels</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">swaggerModelName</span><span class="pi">:</span> <span class="s">实体类列表</span>
</code></pre></div></div>
<p><img src="/images/blog/knife4j2.0.7/swaggerModelName.png" alt="" /></p>
<p>8、可以通过配置是否显示调试栏中的<code class="highlighter-rouge">AfterScript</code>功能,该属性默认为<code class="highlighter-rouge">true</code>,详情请参考<a href="https://doc.xiaominfo.com/knife4j/enhance.html">增强文档</a>,例如:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">knife4j</span><span class="pi">:</span>
<span class="na">enable</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">setting</span><span class="pi">:</span>
<span class="na">enableAfterScript</span><span class="pi">:</span> <span class="no">false</span>
</code></pre></div></div>
<p><img src="/images/blog/knife4j2.0.7/afterScript.png" alt="" /></p>
<p>9、支持<code class="highlighter-rouge">@RequestMapping</code>注解中的<code class="highlighter-rouge">params</code>参数<a href="https://gitee.com/xiaoym/knife4j/issues/I22J5Q">Gitee #I22J5Q</a></p>
<p>10、<code class="highlighter-rouge">3.0</code>版本不支持<code class="highlighter-rouge">Authorize</code>的问题<a href="https://gitee.com/xiaoym/knife4j/issues/I22WVM">Gitee #I22WVM</a></p>
<p>11、增加局部刷新变量的按钮功能,可以通过服务端配置开启<a href="https://gitee.com/xiaoym/knife4j/issues/I22XXI">Gitee #I22XXI</a>,该属性默认为<code class="highlighter-rouge">false</code>,详情请参考<a href="https://doc.xiaominfo.com/knife4j/enhance.html">增强文档</a>,例如:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">knife4j</span><span class="pi">:</span>
<span class="na">enable</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">setting</span><span class="pi">:</span>
<span class="na">enableReloadCacheParameter</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>
<p><img src="/images/blog/knife4j2.0.7/reloadparameter.png" alt="" /></p>
<p>12、修复兼容性bug,当升级后,默认<code class="highlighter-rouge">Swagger Models</code>以及<code class="highlighter-rouge">文档管理</code>功能丢失的问题</p>
<h2 id="使用方法">使用方法</h2>
<p>Java开发使用<code class="highlighter-rouge">Knife4j</code>目前有一些不同的版本变化,主要如下:</p>
<p>1、如果开发者继续使用OpenAPI2的规范结构,底层框架依赖springfox2.10.5版本,那么可以考虑<code class="highlighter-rouge">Knife4j</code>的2.x版本</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><dependency></span>
<span class="nt"><groupId></span>com.github.xiaoymin<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>knife4j-spring-boot-starter<span class="nt"></artifactId></span>
<span class="c"><!--在引用时请在maven中央仓库搜索2.X最新版本号--></span>
<span class="nt"><version></span>2.0.7<span class="nt"></version></span>
<span class="nt"></dependency></span>
</code></pre></div></div>
<p>2、如果开发者使用OpenAPI3的结构,底层框架依赖springfox3.0.0,可以考虑<code class="highlighter-rouge">Knife4j</code>的3.x版本</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><dependency></span>
<span class="nt"><groupId></span>com.github.xiaoymin<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>knife4j-spring-boot-starter<span class="nt"></artifactId></span>
<span class="c"><!--在引用时请在maven中央仓库搜索3.X最新版本号--></span>
<span class="nt"><version></span>3.0.1<span class="nt"></version></span>
<span class="nt"></dependency></span>
</code></pre></div></div>
<p>3、如果开发者底层框架使用的是<code class="highlighter-rouge">springdoc-openapi</code>框架,则需要使用<code class="highlighter-rouge">Knife4j</code>提供的对应版本,需要注意的是该版本没有<code class="highlighter-rouge">Knife4j</code>提供的增强功能,是一个纯Ui。</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><dependency></span>
<span class="nt"><groupId></span>com.github.xiaoymin<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>knife4j-springdoc-ui<span class="nt"></artifactId></span>
<span class="c"><!--在引用时请在maven中央仓库搜索3.X最新版本号--></span>
<span class="nt"><version></span>3.0.1<span class="nt"></version></span>
<span class="nt"></dependency></span>
</code></pre></div></div>
<h2 id="特点">特点</h2>
<ul>
<li>基于Vue+Ant Design构建的文档,更强大、清晰的接口文档说明能力以及接口调试能力</li>
<li>左右布局,基于Tabs组件的多文档查阅风格</li>
<li>支持在线导出Html、Markdown、Word、PDF等多种格式的离线文档</li>
<li>接口排序,支持分组及接口的排序功能</li>
<li>支持接口全局在线搜索功能</li>
<li>提供Swagger资源保护策略,保护文档安全</li>
<li>接口调试支持无限参数,开发者调试非常灵活,动态增加、删除参数</li>
<li>全局缓存调试信息,页面刷新后依然存在,方便开发者调试</li>
<li>以更人性化的table树组件展示Swagger Models功能</li>
<li>文档以多tab方式可显示多个接口文档</li>
<li>请求参数栏请求类型、是否必填着颜色区分</li>
<li>主页中粗略统计接口不同类型数量</li>
<li>支持自定义全局参数功能,主页包括header及query两种类型</li>
<li>JSR-303 annotations 注解的支持</li>
<li>更多个性化设置功能</li>
</ul>
<h2 id="界面">界面</h2>
<p>接口文档显示界面如下:</p>
<p><img src="https://oscimg.oschina.net/oscnet/up-5b76509501c86174096f8b795d2aba8455b.png" alt="" /></p>
<p>接口调试界面如下:</p>
<p><img src="https://oscimg.oschina.net/oscnet/up-995a784219ea035cacd428d15d04e9cbcb3.png" alt="" /></p>
<p>Swagger Models功能</p>
<p><img src="https://oscimg.oschina.net/oscnet/up-98e1dbdf35ab957f5c05df126f9bae44ffd.png" alt="" /></p>
<p><img src="https://oscimg.oschina.net/oscnet/up-d9a030b06b76f9a4935205df453af149788.png" alt="" /></p>
<p>支持导出离线Markdown、Html功能,markdown的表格较原先版本通过缩减显示为树形结构,<a href="https://doc.xiaominfo.com/html/knife4j-export-html.html">点击预览导出离线Html效果</a>,效果图如下:</p>
<p><img src="https://oscimg.oschina.net/oscnet/up-cfb2256485835e29a39f96eaaa60251e08c.png" alt="" /></p>
<p>通过第三方Markdown软件导出的PDF效果如下图:</p>
<p><img src="https://oscimg.oschina.net/oscnet/up-44bb80532b9767a33650e178809f612c3e7.png" alt="" /></p>
<p>同时提供了导出离线Html功能,Html功能界面风格和在线几乎没有区别,美观、大方、简洁,<a href="https://doc.xiaominfo.com/Knife4j-Offline-Html.html">点击在线预览效果</a>,</p>
<p>界面效果如下图:</p>
<p><img src="https://oscimg.oschina.net/oscnet/up-4deb56c65774b4dc2ad54f5278f31e89a5d.png" alt="" /></p>
<h2 id="star--issue">Star & Issue</h2>
<p>感谢各位朋友的支持,前往<a href="https://gitee.com/xiaoym/knife4j">https://gitee.com/xiaoym/knife4j</a>点个Star吧~~ :)</p>肖玉民Knife4j前身是swagger-bootstrap-ui,是一个为Swagger接口文档赋能的工具