<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://springcamp.cn/feed.xml" rel="self" type="application/atom+xml" /><link href="https://springcamp.cn/" rel="alternate" type="text/html" /><updated>2026-09-02T09:16:36+00:00</updated><id>https://springcamp.cn/feed.xml</id><title type="html">海思</title><subtitle>my technical blog.</subtitle><entry><title type="html">Spring AI实现MCP Server</title><link href="https://springcamp.cn/spring-mcp/" rel="alternate" type="text/html" title="Spring AI实现MCP Server" /><published>2026-08-29T07:30:00+00:00</published><updated>2026-08-29T07:30:00+00:00</updated><id>https://springcamp.cn/spring-mcp</id><content type="html" xml:base="https://springcamp.cn/spring-mcp/"><![CDATA[<p>MCP（Model Context Protocol）是一个开放协议，用于让大模型连接外部的工具和数据源，Spring AI提供了MCP的server和client集成，把协议细节封装成开箱即用的starter，本文介绍如何基于Spring AI开发一个MCP Server，通过SSE对外暴露工具，供MCP客户端调用。</p>

<p>具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-mcp">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-mcp</a></p>

<h2 id="一概述">一、概述</h2>

<p>MCP协议定义了server和client两种角色，server负责提供具体的能力，比如查询天气、操作数据库，client负责连接server并把工具暴露给大模型使用，常见的MCP客户端有Claude Desktop、Cursor等。协议底层基于JSON-RPC，对传输方式做了抽象，常用的有STDIO和SSE两种，本示例基于webmvc使用SSE传输，server启动后通过 <code class="language-plaintext highlighter-rouge">http://localhost:8080/sse</code> 端点接入。</p>

<p>在MCP协议中，server可以对外提供tools、resources、prompts三类能力，其中tools是最常用的一类，本示例实现了三个工具：根据经纬度查询天气预报、查询美国某个州的气象预警、把文本转换为大写。其中天气数据为随机生成的mock数据，不需要申请api-key，不依赖任何外部服务，整个项目克隆下来启动即可使用。</p>

<h2 id="二项目依赖与配置">二、项目依赖与配置</h2>

<p>引入spring-ai的mcp server starter，基于webmvc实现：</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ext</span> <span class="o">{</span>
    <span class="n">set</span><span class="o">(</span><span class="s1">'springAiVersion'</span><span class="o">,</span> <span class="s2">"2.0.0"</span><span class="o">)</span>
<span class="o">}</span>

<span class="n">dependencies</span> <span class="o">{</span>
    <span class="n">implementation</span> <span class="s1">'org.springframework.ai:spring-ai-starter-mcp-server-webmvc'</span>
<span class="o">}</span>

<span class="n">dependencyManagement</span> <span class="o">{</span>
    <span class="n">imports</span> <span class="o">{</span>
        <span class="n">mavenBom</span> <span class="s2">"org.springframework.ai:spring-ai-bom:${springAiVersion}"</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>starter会自动配置好MCP server所需的全部组件，包括SSE端点、JSON-RPC消息的编解码和工具的注册分发，我们不需要编写任何协议相关的代码。</p>

<p>在application.properties中对server进行配置：</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">server.port</span><span class="p">=</span><span class="s">8080</span>
<span class="py">spring.ai.mcp.server.name</span><span class="p">=</span><span class="s">my-weather-server</span>

<span class="c"># Server type (SYNC/ASYNC)
</span><span class="py">spring.ai.mcp.server.type</span><span class="p">=</span><span class="s">SYNC</span>

<span class="py">spring.main.banner-mode</span><span class="p">=</span><span class="s">off</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">spring.ai.mcp.server.name</code> 是server在握手时上报给客户端的名称，<code class="language-plaintext highlighter-rouge">spring.ai.mcp.server.type</code> 指定同步还是异步模式，默认为SYNC。SSE相关的端点也有默认值，SSE握手端点为 <code class="language-plaintext highlighter-rouge">/sse</code>，客户端建立连接后，server会通过endpoint事件告知消息端点及会话id，默认为 <code class="language-plaintext highlighter-rouge">/mcp/message</code>，可以通过 <code class="language-plaintext highlighter-rouge">spring.ai.mcp.server.sse-message-endpoint</code> 修改。</p>

<p><code class="language-plaintext highlighter-rouge">spring.main.banner-mode=off</code> 关闭启动banner，在示例的SSE传输下不是必须的，但如果使用STDIO传输，banner会输出到标准输出，污染JSON-RPC消息通道，导致客户端无法解析，所以使用STDIO时必须关闭banner。</p>

<h2 id="三注册mcp-tool的两种方式">三、注册MCP Tool的两种方式</h2>

<p>spring-ai提供了两种把java方法注册为MCP工具的方式，示例项目在启动类中同时演示了这两种方式：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@SpringBootApplication</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">McpServerApplication</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="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">SpringApplication</span><span class="o">.</span><span class="na">run</span><span class="o">(</span><span class="nc">McpServerApplication</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="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">ToolCallbackProvider</span> <span class="nf">weatherTools</span><span class="o">(</span><span class="nc">WeatherService</span> <span class="n">weatherService</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">MethodToolCallbackProvider</span><span class="o">.</span><span class="na">builder</span><span class="o">().</span><span class="na">toolObjects</span><span class="o">(</span><span class="n">weatherService</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="n">record</span> <span class="nf">TextInput</span><span class="o">(</span><span class="nc">String</span> <span class="n">input</span><span class="o">)</span> <span class="o">{</span>
    <span class="o">}</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">ToolCallback</span> <span class="nf">toUpperCase</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">FunctionToolCallback</span><span class="o">.</span><span class="na">builder</span><span class="o">(</span><span class="s">"toUpperCase"</span><span class="o">,</span> <span class="o">(</span><span class="nc">TextInput</span> <span class="n">input</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="n">input</span><span class="o">.</span><span class="na">input</span><span class="o">().</span><span class="na">toUpperCase</span><span class="o">())</span>
                <span class="o">.</span><span class="na">inputType</span><span class="o">(</span><span class="nc">TextInput</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
                <span class="o">.</span><span class="na">description</span><span class="o">(</span><span class="s">"Put the text to upper case"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>第一种是声明式，通过 <code class="language-plaintext highlighter-rouge">MethodToolCallbackProvider</code> 把 WeatherService 注册为工具对象，spring-ai会扫描其中所有带 <code class="language-plaintext highlighter-rouge">@Tool</code> 注解的方法，自动转换为MCP工具，适合工具方法比较多、集中在一个service中的场景。</p>

<p>第二种是编程式，通过 <code class="language-plaintext highlighter-rouge">FunctionToolCallback.builder()</code> 手工构造单个工具，依次指定工具名、处理逻辑、输入类型和描述，输入类型用一个record定义，spring-ai会根据它生成工具入参的json schema，适合逻辑简单、不值得单独建一个类的场景。</p>

<p>两种方式注册的bean都会被starter自动收集，通过MCP协议的 <code class="language-plaintext highlighter-rouge">tools/list</code> 和 <code class="language-plaintext highlighter-rouge">tools/call</code> 暴露给客户端，客户端无需区分工具是用哪种方式注册的。</p>

<h2 id="四实现天气查询工具">四、实现天气查询工具</h2>

<p>WeatherService 提供了两个天气查询工具，为了便于演示，天气数据使用mock数据随机生成，不依赖任何外部服务：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">WeatherService</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span><span class="o">[]</span> <span class="no">CONDITIONS</span> <span class="o">=</span> <span class="o">{</span><span class="s">"晴"</span><span class="o">,</span> <span class="s">"多云"</span><span class="o">,</span> <span class="s">"小雨"</span><span class="o">,</span> <span class="s">"小雪"</span><span class="o">};</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span><span class="o">[]</span> <span class="no">WIND_DIRECTIONS</span> <span class="o">=</span> <span class="o">{</span><span class="s">"东风"</span><span class="o">,</span> <span class="s">"南风"</span><span class="o">,</span> <span class="s">"西风"</span><span class="o">,</span> <span class="s">"北风"</span><span class="o">};</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span><span class="o">[]</span> <span class="no">ALERT_EVENTS</span> <span class="o">=</span> <span class="o">{</span><span class="s">"暴雨橙色预警"</span><span class="o">,</span> <span class="s">"高温黄色预警"</span><span class="o">,</span> <span class="s">"大风蓝色预警"</span><span class="o">,</span> <span class="s">"寒潮蓝色预警"</span><span class="o">};</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span><span class="o">[]</span> <span class="no">SEVERITIES</span> <span class="o">=</span> <span class="o">{</span><span class="s">"低"</span><span class="o">,</span> <span class="s">"中等"</span><span class="o">,</span> <span class="s">"高"</span><span class="o">,</span> <span class="s">"严重"</span><span class="o">};</span>

    <span class="nd">@Tool</span><span class="o">(</span><span class="n">description</span> <span class="o">=</span> <span class="s">"Get weather forecast for a specific latitude/longitude"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getWeatherForecastByLocation</span><span class="o">(</span><span class="kt">double</span> <span class="n">latitude</span><span class="o">,</span> <span class="kt">double</span> <span class="n">longitude</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">StringBuilder</span> <span class="n">forecast</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">StringBuilder</span><span class="o">(</span><span class="nc">String</span><span class="o">.</span><span class="na">format</span><span class="o">(</span><span class="s">"坐标（%s, %s）未来三天预报：\n"</span><span class="o">,</span> <span class="n">latitude</span><span class="o">,</span> <span class="n">longitude</span><span class="o">));</span>
        <span class="nc">ThreadLocalRandom</span> <span class="n">random</span> <span class="o">=</span> <span class="nc">ThreadLocalRandom</span><span class="o">.</span><span class="na">current</span><span class="o">();</span>
        <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">day</span> <span class="o">=</span> <span class="mi">1</span><span class="o">;</span> <span class="n">day</span> <span class="o">&lt;=</span> <span class="mi">3</span><span class="o">;</span> <span class="n">day</span><span class="o">++)</span> <span class="o">{</span>
            <span class="n">forecast</span><span class="o">.</span><span class="na">append</span><span class="o">(</span><span class="nc">String</span><span class="o">.</span><span class="na">format</span><span class="o">(</span><span class="s">"""
                    第%d天:
                    温度: %d°C
                    风力: %d级 %s
                    天气: %s
                    """</span><span class="o">,</span> <span class="n">day</span><span class="o">,</span> <span class="n">random</span><span class="o">.</span><span class="na">nextInt</span><span class="o">(-</span><span class="mi">5</span><span class="o">,</span> <span class="mi">36</span><span class="o">),</span> <span class="n">random</span><span class="o">.</span><span class="na">nextInt</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">9</span><span class="o">),</span>
                    <span class="no">WIND_DIRECTIONS</span><span class="o">[</span><span class="n">random</span><span class="o">.</span><span class="na">nextInt</span><span class="o">(</span><span class="no">WIND_DIRECTIONS</span><span class="o">.</span><span class="na">length</span><span class="o">)],</span>
                    <span class="no">CONDITIONS</span><span class="o">[</span><span class="n">random</span><span class="o">.</span><span class="na">nextInt</span><span class="o">(</span><span class="no">CONDITIONS</span><span class="o">.</span><span class="na">length</span><span class="o">)]));</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="n">forecast</span><span class="o">.</span><span class="na">toString</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Tool</span><span class="o">(</span><span class="n">description</span> <span class="o">=</span> <span class="s">"Get weather alerts for a US state. Input is Two-letter US state code (e.g. CA, NY)"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getAlerts</span><span class="o">(</span><span class="nc">String</span> <span class="n">state</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">ThreadLocalRandom</span> <span class="n">random</span> <span class="o">=</span> <span class="nc">ThreadLocalRandom</span><span class="o">.</span><span class="na">current</span><span class="o">();</span>
        <span class="nc">String</span> <span class="n">event</span> <span class="o">=</span> <span class="no">ALERT_EVENTS</span><span class="o">[</span><span class="n">random</span><span class="o">.</span><span class="na">nextInt</span><span class="o">(</span><span class="no">ALERT_EVENTS</span><span class="o">.</span><span class="na">length</span><span class="o">)];</span>
        <span class="k">return</span> <span class="nc">String</span><span class="o">.</span><span class="na">format</span><span class="o">(</span><span class="s">"""
                Event: %s
                Area: %s
                Severity: %s
                Description: %s 生效中，请注意防范。
                """</span><span class="o">,</span> <span class="n">event</span><span class="o">,</span> <span class="n">state</span><span class="o">,</span> <span class="no">SEVERITIES</span><span class="o">[</span><span class="n">random</span><span class="o">.</span><span class="na">nextInt</span><span class="o">(</span><span class="no">SEVERITIES</span><span class="o">.</span><span class="na">length</span><span class="o">)],</span> <span class="n">event</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">@Tool</code> 的description是给大模型看的，模型根据它来判断什么时候该调用这个工具、参数是什么含义，所以要描述清楚工具的用途和参数格式。</p>

<p>工具的返回值是对大模型友好的格式化纯文本，mock数据生成的天气信息会带上入参中的坐标和州代码，让工具调用看起来更加真实。在实际项目中，工具方法内部可以是调用数据库、第三方接口等任意数据源，MCP只关心方法的入参和返回值。</p>

<p>还有一个容易被忽略的细节，<code class="language-plaintext highlighter-rouge">@Tool</code> 方法入参的参数名会用于生成json schema，编译时需要开启 <code class="language-plaintext highlighter-rouge">-parameters</code> 参数保留方法参数名，否则生成的schema里参数名会变成arg0、arg1，模型无法正确传参。示例项目在build.gradle中全局开启了这个编译参数：</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">tasks</span><span class="o">.</span><span class="na">withType</span><span class="o">(</span><span class="n">JavaCompile</span><span class="o">).</span><span class="na">configureEach</span> <span class="o">{</span>
    <span class="n">options</span><span class="o">.</span><span class="na">compilerArgs</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="s2">"-parameters"</span><span class="o">)</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="五运行与验证">五、运行与验证</h2>

<p>启动项目：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./gradlew :spring-mcp:bootRun
</code></pre></div></div>

<p>启动后先建立一个SSE连接，观察server返回的握手信息：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-N</span> http://localhost:8080/sse
</code></pre></div></div>

<p>server会返回类似下面的内容，endpoint事件告知了消息端点和本次会话的id：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>event: endpoint
data: /mcp/message?sessionId=6e3a1c2e-8f7b-4a5d-9c1e-2b3d4e5f6a7b
</code></pre></div></div>

<p>验证工具更方便的方式是使用MCP官方提供的调试工具Inspector：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npx @modelcontextprotocol/inspector
</code></pre></div></div>

<p>在Inspector的界面中，Transport类型选择SSE，URL填写 <code class="language-plaintext highlighter-rouge">http://localhost:8080/sse</code>，连接后可以在Tools面板中看到server暴露的三个工具：getWeatherForecastByLocation、getAlerts和toUpperCase，直接填入参数调用即可，比如state填入NY，就能看到getAlerts返回的模拟预警信息。</p>

<p>除了MCP的SSE端点，示例中还实现了一个 <code class="language-plaintext highlighter-rouge">/sse-mock</code> 接口，用SseEmitter每秒推送一条消息，共推送10条，可以用来直观了解MCP的SSE传输所依赖的服务端推送机制：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-N</span> http://localhost:8080/sse-mock
</code></pre></div></div>

<p>搭建好MCP Server后，在Claude Desktop等MCP客户端中以SSE方式配置 <code class="language-plaintext highlighter-rouge">http://localhost:8080/sse</code>，大模型就可以在对话中调用这些天气工具了。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><category term="ai" /><category term="mcp" /><category term="featured" /><summary type="html"><![CDATA[MCP（Model Context Protocol）是一个开放协议，用于让大模型连接外部的工具和数据源，Spring AI提供了MCP的server和client集成，把协议细节封装成开箱即用的starter，本文介绍如何基于Spring AI开发一个MCP Server，通过SSE对外暴露工具，供MCP客户端调用。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/spring-mcp.jpg" /><media:content medium="image" url="https://springcamp.cn/assets/images/spring-mcp.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">通过Spring Authorization Server对微信小程序应用进行授权防护</title><link href="https://springcamp.cn/spring-oauth-wechat-miniprogram/" rel="alternate" type="text/html" title="通过Spring Authorization Server对微信小程序应用进行授权防护" /><published>2026-08-22T07:30:00+00:00</published><updated>2026-08-22T07:30:00+00:00</updated><id>https://springcamp.cn/spring-oauth-wechat-miniprogram</id><content type="html" xml:base="https://springcamp.cn/spring-oauth-wechat-miniprogram/"><![CDATA[<p>微信小程序是一种典型的原生客户端应用：没有浏览器地址栏，无法完成 OAuth2 授权码流程所依赖的重定向交互；代码包可以被反编译，客户端没有安全存放密钥的地方，标准的授权码流程并不能直接套用。本文介绍如何利用 Spring Authorization Server 的扩展授权类型（Extension Grant）机制，为小程序定制一个 <code class="language-plaintext highlighter-rouge">wechat-code</code> 授权模式，用 <code class="language-plaintext highlighter-rouge">wx.login()</code> 产生的 code 换取标准的 OAuth2 令牌，实现对后端资源的统一授权防护。</p>

<p>具体的代码参照 <a href="https://github.com/qihaiyan/ng-boot-oauth">示例项目 https://github.com/qihaiyan/ng-boot-oauth</a></p>

<p>网页端微信登录：<a href="https://xj.hiease.cn">小记</a></p>

<p>对应的小程序demo可以扫码查看</p>

<p><img src="../assets/images/gh_fdd613f3b7e0_258.jpg" alt="gh_fdd613f3b7e0_258.jpg" /></p>

<h2 id="一概述">一、概述</h2>

<p>微信小程序的授权防护面临几个特殊问题：</p>

<ol>
  <li><strong>无法重定向</strong>：授权码流程要求客户端引导用户浏览器跳转到授权服务器，而小程序运行在微信的容器里，没有标准的浏览器环境（<code class="language-plaintext highlighter-rouge">web-view</code> 内嵌页面体验差且限制多）。</li>
  <li><strong>无法保密</strong>：小程序代码包随安装分发，可以被反编译，任何写死在代码里的 secret 都会泄露，所以小程序只能作为公开客户端（public client）。</li>
  <li><strong>微信登录不等于接口防护</strong>：<code class="language-plaintext highlighter-rouge">wx.login()</code> 只解决”微信身份”问题，自己的后端接口的访问控制、令牌生命周期管理、多端统一认证，仍然需要一套标准的 OAuth2 令牌体系。</li>
</ol>

<p>本文的方案是：用微信官方的 <code class="language-plaintext highlighter-rouge">wx.login()</code> 完成用户认证，用 Spring Authorization Server 的<strong>扩展授权类型</strong>机制定制一个 <code class="language-plaintext highlighter-rouge">wechat-code</code> 授权模式完成令牌签发。整体思路：</p>

<ul>
  <li>小程序调用 <code class="language-plaintext highlighter-rouge">wx.login()</code> 拿到一次性临时 code；</li>
  <li>小程序把 code 提交到授权服务器的 <code class="language-plaintext highlighter-rouge">/oauth2/token</code> 端点（grant_type 为自定义的 <code class="language-plaintext highlighter-rouge">wechat-code</code>）；</li>
  <li>授权服务器拿着 code 调用微信的 <code class="language-plaintext highlighter-rouge">jscode2session</code> 接口换取 <code class="language-plaintext highlighter-rouge">openid</code>，加载（或自动注册）对应的用户；</li>
  <li>授权服务器签发标准的 <code class="language-plaintext highlighter-rouge">access_token</code>（JWT）和 <code class="language-plaintext highlighter-rouge">refresh_token</code>；</li>
  <li>小程序后续请求携带 <code class="language-plaintext highlighter-rouge">Bearer</code> 令牌访问资源服务器，资源服务器本地校验 JWT，完全无状态。</li>
</ul>

<p>这样微信登录与令牌体系解耦，资源服务器不感知微信，仍然是标准的 OAuth2 防护。</p>

<h2 id="二核心交互流程">二、核心交互流程</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>微信小程序                      授权服务器 authserver               微信开放平台
    |                                  |                                |
    |--- wx.login() 获取临时 code -------------------------------------&gt;|
    |&lt;-- code ---------------------------------------------------------|
    |                                  |                                |
    |--- POST /oauth2/token ---------&gt;|                                |
    |    grant_type=wechat-code       |--- jscode2session -------------&gt;|
    |    client_id=miniapp-client     |    (appid, secret, code)       |
    |    code=xxx                     |&lt;-- openid / session_key --------|
    |                                  |                                |
    |                                  |  根据 openid 加载(或注册)用户     |
    |                                  |  生成 access_token/refresh_token|
    |&lt;-- access_token / refresh_token |                                |
    |                                  |                                |
    |--- GET /api/messages ----------&gt;|  资源服务器本地校验 JWT           |
    |    Authorization: Bearer xxx    |                                |

 access_token 过期后：
    |--- POST /oauth2/token ---------&gt;|  标准的 refresh_token 授权       |
    |&lt;-- 新的 access_token / refresh_token ------------------------------|
</code></pre></div></div>

<p>流程中的几个关键点：</p>

<ul>
  <li>微信的 code 是<strong>一次性</strong>的，有效期约 5 分钟，天然防重放；</li>
  <li><code class="language-plaintext highlighter-rouge">appid</code> 和 <code class="language-plaintext highlighter-rouge">secret</code> 只保存在 authserver 端，小程序不持有任何密钥；</li>
  <li><code class="language-plaintext highlighter-rouge">access_token</code> 是自包含的 JWT，资源服务器不查库、不调 authserver，即可完成校验；</li>
  <li>token 过期后走标准的 <code class="language-plaintext highlighter-rouge">refresh_token</code> 授权续期，Spring Authorization Server 刷新时会自动轮换（rotation）refresh_token。</li>
</ul>

<h2 id="三authserver端实现扩展授权类型">三、authserver端：实现扩展授权类型</h2>

<p>Spring Authorization Server 是一个开发库而不是开箱即用的服务，它原生支持授权码、客户端凭证、刷新令牌这几个标准授权类型，同时提供了扩展机制：通过自定义 <code class="language-plaintext highlighter-rouge">AuthenticationConverter</code> 和 <code class="language-plaintext highlighter-rouge">AuthenticationProvider</code>，就能增加自己的授权类型，接入 <code class="language-plaintext highlighter-rouge">/oauth2/token</code> 这个标准端点。</p>

<h3 id="1-定义授权类型">1. 定义授权类型</h3>

<p>grant_type 采用 RFC 推荐的 URN 格式，避免与标准类型冲突：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">final</span> <span class="kd">class</span> <span class="nc">WechatGrantTypes</span> <span class="o">{</span>

    <span class="cm">/**
     * 微信小程序登录授权类型
     */</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">WECHAT_CODE</span> <span class="o">=</span> <span class="s">"urn:springcamp:params:oauth:grant-type:wechat-code"</span><span class="o">;</span>

    <span class="kd">private</span> <span class="nf">WechatGrantTypes</span><span class="o">()</span> <span class="o">{</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="2-自定义-authentication-令牌">2. 自定义 Authentication 令牌</h3>

<p>继承 <code class="language-plaintext highlighter-rouge">OAuth2AuthorizationGrantAuthenticationToken</code>，把微信 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">WechatCodeGrantAuthenticationToken</span> <span class="kd">extends</span> <span class="nc">OAuth2AuthorizationGrantAuthenticationToken</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">String</span> <span class="n">code</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">WechatCodeGrantAuthenticationToken</span><span class="o">(</span><span class="nc">String</span> <span class="n">code</span><span class="o">,</span> <span class="nc">Authentication</span> <span class="n">clientPrincipal</span><span class="o">,</span>
            <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;</span> <span class="n">additionalParameters</span><span class="o">)</span> <span class="o">{</span>
        <span class="kd">super</span><span class="o">(</span><span class="k">new</span> <span class="nc">AuthorizationGrantType</span><span class="o">(</span><span class="nc">WechatGrantTypes</span><span class="o">.</span><span class="na">WECHAT_CODE</span><span class="o">),</span> <span class="n">clientPrincipal</span><span class="o">,</span> <span class="n">additionalParameters</span><span class="o">);</span>
        <span class="k">this</span><span class="o">.</span><span class="na">code</span> <span class="o">=</span> <span class="n">code</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getCode</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">code</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="3-converter从请求中提取参数">3. Converter：从请求中提取参数</h3>

<p>token 端点收到请求后，先由 <code class="language-plaintext highlighter-rouge">AuthenticationConverter</code> 把 HTTP 请求转换成上一步的 Authentication。注意 <code class="language-plaintext highlighter-rouge">grant_type</code> 不匹配时要返回 <code class="language-plaintext highlighter-rouge">null</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">WechatCodeGrantAuthenticationConverter</span> <span class="kd">implements</span> <span class="nc">AuthenticationConverter</span> <span class="o">{</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">Authentication</span> <span class="nf">convert</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">grantType</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="nc">OAuth2ParameterNames</span><span class="o">.</span><span class="na">GRANT_TYPE</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(!</span><span class="nc">WechatGrantTypes</span><span class="o">.</span><span class="na">WECHAT_CODE</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">grantType</span><span class="o">))</span> <span class="o">{</span>
            <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
        <span class="o">}</span>

        <span class="c1">// clientPrincipal 是 ClientAuthenticationFilter 认证后的客户端身份</span>
        <span class="nc">Authentication</span> <span class="n">clientPrincipal</span> <span class="o">=</span> <span class="nc">SecurityContextHolder</span><span class="o">.</span><span class="na">getContext</span><span class="o">().</span><span class="na">getAuthentication</span><span class="o">();</span>

        <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;</span> <span class="n">additionalParameters</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HashMap</span><span class="o">&lt;&gt;();</span>
        <span class="n">request</span><span class="o">.</span><span class="na">getParameterNames</span><span class="o">().</span><span class="na">forEach</span><span class="o">(</span><span class="n">parameter</span> <span class="o">-&gt;</span> <span class="o">{</span>
            <span class="k">if</span> <span class="o">(!</span><span class="n">parameter</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="nc">OAuth2ParameterNames</span><span class="o">.</span><span class="na">GRANT_TYPE</span><span class="o">)</span>
                    <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">parameter</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="nc">OAuth2ParameterNames</span><span class="o">.</span><span class="na">CLIENT_ID</span><span class="o">)</span>
                    <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">parameter</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="nc">OAuth2ParameterNames</span><span class="o">.</span><span class="na">CODE</span><span class="o">))</span> <span class="o">{</span>
                <span class="n">additionalParameters</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="n">parameter</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">parameter</span><span class="o">));</span>
            <span class="o">}</span>
        <span class="o">});</span>

        <span class="nc">String</span> <span class="n">code</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="nc">OAuth2ParameterNames</span><span class="o">.</span><span class="na">CODE</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(!</span><span class="nc">StringUtils</span><span class="o">.</span><span class="na">hasText</span><span class="o">(</span><span class="n">code</span><span class="o">))</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">OAuth2AuthenticationException</span><span class="o">(</span>
                    <span class="k">new</span> <span class="nf">OAuth2Error</span><span class="o">(</span><span class="nc">OAuth2ErrorCodes</span><span class="o">.</span><span class="na">INVALID_REQUEST</span><span class="o">,</span> <span class="s">"code 参数缺失"</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="k">new</span> <span class="nf">WechatCodeGrantAuthenticationToken</span><span class="o">(</span><span class="n">code</span><span class="o">,</span> <span class="n">clientPrincipal</span><span class="o">,</span> <span class="n">additionalParameters</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="4-调用微信接口校验-code">4. 调用微信接口校验 code</h3>

<p>authserver 使用 <code class="language-plaintext highlighter-rouge">RestClient</code> 调用微信的 <code class="language-plaintext highlighter-rouge">jscode2session</code> 接口，<code class="language-plaintext highlighter-rouge">appid</code> 和 <code class="language-plaintext highlighter-rouge">secret</code> 通过配置注入，只在服务端使用：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">WechatApiService</span> <span class="o">{</span>

    <span class="nd">@Value</span><span class="o">(</span><span class="s">"${wechat.appid}"</span><span class="o">)</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">appid</span><span class="o">;</span>

    <span class="nd">@Value</span><span class="o">(</span><span class="s">"${wechat.secret}"</span><span class="o">)</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">secret</span><span class="o">;</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">RestClient</span> <span class="n">restClient</span> <span class="o">=</span> <span class="nc">RestClient</span><span class="o">.</span><span class="na">create</span><span class="o">();</span>

    <span class="kd">public</span> <span class="nc">WechatSession</span> <span class="nf">code2Session</span><span class="o">(</span><span class="nc">String</span> <span class="n">jsCode</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">WechatSession</span> <span class="n">session</span> <span class="o">=</span> <span class="n">restClient</span><span class="o">.</span><span class="na">get</span><span class="o">()</span>
                <span class="o">.</span><span class="na">uri</span><span class="o">(</span><span class="s">"https://api.weixin.qq.com/sns/jscode2session?appid={appid}&amp;secret={secret}&amp;js_code={code}&amp;grant_type=authorization_code"</span><span class="o">,</span>
                        <span class="n">appid</span><span class="o">,</span> <span class="n">secret</span><span class="o">,</span> <span class="n">jsCode</span><span class="o">)</span>
                <span class="o">.</span><span class="na">retrieve</span><span class="o">()</span>
                <span class="o">.</span><span class="na">body</span><span class="o">(</span><span class="nc">WechatSession</span><span class="o">.</span><span class="na">class</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="n">session</span><span class="o">.</span><span class="na">getErrcode</span><span class="o">()</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="n">session</span><span class="o">.</span><span class="na">getErrcode</span><span class="o">()</span> <span class="o">!=</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span>
            <span class="c1">// 常见错误：40029 code 无效、45011 频率限制</span>
            <span class="nc">String</span> <span class="n">errmsg</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="s">"empty response"</span> <span class="o">:</span> <span class="n">session</span><span class="o">.</span><span class="na">getErrmsg</span><span class="o">();</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">OAuth2AuthenticationException</span><span class="o">(</span>
                    <span class="k">new</span> <span class="nf">OAuth2Error</span><span class="o">(</span><span class="nc">OAuth2ErrorCodes</span><span class="o">.</span><span class="na">INVALID_GRANT</span><span class="o">,</span> <span class="s">"微信登录失败: "</span> <span class="o">+</span> <span class="n">errmsg</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">session</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="kd">public</span> <span class="kd">class</span> <span class="nc">WechatSession</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">openid</span><span class="o">;</span>       <span class="c1">// 用户在当前小程序下的唯一标识</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">session_key</span><span class="o">;</span>  <span class="c1">// 会话密钥，仅服务端使用，不能下发给小程序</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">unionid</span><span class="o">;</span>      <span class="c1">// 同一开放平台账号下的统一标识</span>
    <span class="kd">private</span> <span class="nc">Integer</span> <span class="n">errcode</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">errmsg</span><span class="o">;</span>
    <span class="c1">// getter/setter 省略</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="5-provider核心认证逻辑">5. Provider：核心认证逻辑</h3>

<p><code class="language-plaintext highlighter-rouge">AuthenticationProvider</code> 是整个扩展授权的核心，负责：校验客户端是否允许使用该授权类型、用 code 换 openid、加载用户、生成并保存令牌：</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">WechatCodeGrantAuthenticationProvider</span> <span class="kd">implements</span> <span class="nc">AuthenticationProvider</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">WechatApiService</span> <span class="n">wechatApiService</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">CustomUserDetailsService</span> <span class="n">userDetailsService</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">OAuth2AuthorizationService</span> <span class="n">authorizationService</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">OAuth2TokenGenerator</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">OAuth2Token</span><span class="o">&gt;</span> <span class="n">tokenGenerator</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">WechatCodeGrantAuthenticationProvider</span><span class="o">(</span><span class="nc">WechatApiService</span> <span class="n">wechatApiService</span><span class="o">,</span>
            <span class="nc">CustomUserDetailsService</span> <span class="n">userDetailsService</span><span class="o">,</span>
            <span class="nc">OAuth2AuthorizationService</span> <span class="n">authorizationService</span><span class="o">,</span>
            <span class="nc">OAuth2TokenGenerator</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">OAuth2Token</span><span class="o">&gt;</span> <span class="n">tokenGenerator</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">wechatApiService</span> <span class="o">=</span> <span class="n">wechatApiService</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">userDetailsService</span> <span class="o">=</span> <span class="n">userDetailsService</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">authorizationService</span> <span class="o">=</span> <span class="n">authorizationService</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">tokenGenerator</span> <span class="o">=</span> <span class="n">tokenGenerator</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">Authentication</span> <span class="nf">authenticate</span><span class="o">(</span><span class="nc">Authentication</span> <span class="n">authentication</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">AuthenticationException</span> <span class="o">{</span>
        <span class="nc">WechatCodeGrantAuthenticationToken</span> <span class="n">wechatGrant</span> <span class="o">=</span> <span class="o">(</span><span class="nc">WechatCodeGrantAuthenticationToken</span><span class="o">)</span> <span class="n">authentication</span><span class="o">;</span>

        <span class="nc">OAuth2ClientAuthenticationToken</span> <span class="n">clientPrincipal</span> <span class="o">=</span> <span class="n">getAuthenticatedClientElseThrowInvalidClient</span><span class="o">(</span><span class="n">wechatGrant</span><span class="o">);</span>
        <span class="nc">RegisteredClient</span> <span class="n">registeredClient</span> <span class="o">=</span> <span class="n">clientPrincipal</span><span class="o">.</span><span class="na">getRegisteredClient</span><span class="o">();</span>

        <span class="c1">// client 必须注册了 wechat-code 授权类型</span>
        <span class="k">if</span> <span class="o">(!</span><span class="n">registeredClient</span><span class="o">.</span><span class="na">getAuthorizationGrantTypes</span><span class="o">()</span>
                <span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="k">new</span> <span class="nc">AuthorizationGrantType</span><span class="o">(</span><span class="nc">WechatGrantTypes</span><span class="o">.</span><span class="na">WECHAT_CODE</span><span class="o">)))</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">OAuth2AuthenticationException</span><span class="o">(</span><span class="nc">OAuth2ErrorCodes</span><span class="o">.</span><span class="na">UNAUTHORIZED_CLIENT</span><span class="o">);</span>
        <span class="o">}</span>

        <span class="c1">// 1. 调用微信接口校验 code，换取 openid</span>
        <span class="nc">WechatSession</span> <span class="n">session</span> <span class="o">=</span> <span class="n">wechatApiService</span><span class="o">.</span><span class="na">code2Session</span><span class="o">(</span><span class="n">wechatGrant</span><span class="o">.</span><span class="na">getCode</span><span class="o">());</span>

        <span class="c1">// 2. 根据 openid 加载用户，首次登录自动注册</span>
        <span class="nc">UserDetails</span> <span class="n">userDetails</span> <span class="o">=</span> <span class="n">userDetailsService</span><span class="o">.</span><span class="na">loadUserByWechatOpenid</span><span class="o">(</span><span class="n">session</span><span class="o">.</span><span class="na">getOpenid</span><span class="o">());</span>
        <span class="nc">Authentication</span> <span class="n">principal</span> <span class="o">=</span> <span class="nc">UsernamePasswordAuthenticationToken</span><span class="o">.</span><span class="na">authenticated</span><span class="o">(</span>
                <span class="n">userDetails</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="n">userDetails</span><span class="o">.</span><span class="na">getAuthorities</span><span class="o">());</span>

        <span class="c1">// 3. 生成 access_token（默认授予 client 注册的全部 scope）</span>
        <span class="nc">Set</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">authorizedScopes</span> <span class="o">=</span> <span class="n">registeredClient</span><span class="o">.</span><span class="na">getScopes</span><span class="o">();</span>

        <span class="nc">OAuth2TokenContext</span> <span class="n">tokenContext</span> <span class="o">=</span> <span class="nc">DefaultOAuth2TokenContext</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
                <span class="o">.</span><span class="na">registeredClient</span><span class="o">(</span><span class="n">registeredClient</span><span class="o">)</span>
                <span class="o">.</span><span class="na">principal</span><span class="o">(</span><span class="n">principal</span><span class="o">)</span>
                <span class="o">.</span><span class="na">authorizationServerContext</span><span class="o">(</span><span class="nc">AuthorizationServerContextHolder</span><span class="o">.</span><span class="na">getContext</span><span class="o">())</span>
                <span class="o">.</span><span class="na">authorizedScopes</span><span class="o">(</span><span class="n">authorizedScopes</span><span class="o">)</span>
                <span class="o">.</span><span class="na">authorizationGrantType</span><span class="o">(</span><span class="k">new</span> <span class="nc">AuthorizationGrantType</span><span class="o">(</span><span class="nc">WechatGrantTypes</span><span class="o">.</span><span class="na">WECHAT_CODE</span><span class="o">))</span>
                <span class="o">.</span><span class="na">authorizationGrant</span><span class="o">(</span><span class="n">wechatGrant</span><span class="o">)</span>
                <span class="o">.</span><span class="na">tokenType</span><span class="o">(</span><span class="nc">OAuth2TokenType</span><span class="o">.</span><span class="na">ACCESS_TOKEN</span><span class="o">)</span>
                <span class="o">.</span><span class="na">build</span><span class="o">();</span>

        <span class="nc">OAuth2AccessToken</span> <span class="n">accessToken</span> <span class="o">=</span> <span class="k">this</span><span class="o">.</span><span class="na">tokenGenerator</span><span class="o">.</span><span class="na">generate</span><span class="o">(</span><span class="n">tokenContext</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">accessToken</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">OAuth2AuthenticationException</span><span class="o">(</span>
                    <span class="k">new</span> <span class="nf">OAuth2Error</span><span class="o">(</span><span class="nc">OAuth2ErrorCodes</span><span class="o">.</span><span class="na">SERVER_ERROR</span><span class="o">,</span> <span class="s">"令牌生成失败"</span><span class="o">,</span> <span class="kc">null</span><span class="o">));</span>
        <span class="o">}</span>

        <span class="c1">// client 同时注册了 refresh_token 授权类型时，签发 refresh_token</span>
        <span class="nc">OAuth2RefreshToken</span> <span class="n">refreshToken</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">registeredClient</span><span class="o">.</span><span class="na">getAuthorizationGrantTypes</span><span class="o">().</span><span class="na">contains</span><span class="o">(</span><span class="nc">AuthorizationGrantType</span><span class="o">.</span><span class="na">REFRESH_TOKEN</span><span class="o">))</span> <span class="o">{</span>
            <span class="nc">OAuth2TokenContext</span> <span class="n">refreshContext</span> <span class="o">=</span> <span class="nc">DefaultOAuth2TokenContext</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
                    <span class="o">.</span><span class="na">registeredClient</span><span class="o">(</span><span class="n">registeredClient</span><span class="o">)</span>
                    <span class="o">.</span><span class="na">principal</span><span class="o">(</span><span class="n">principal</span><span class="o">)</span>
                    <span class="o">.</span><span class="na">authorizationServerContext</span><span class="o">(</span><span class="nc">AuthorizationServerContextHolder</span><span class="o">.</span><span class="na">getContext</span><span class="o">())</span>
                    <span class="o">.</span><span class="na">authorizedScopes</span><span class="o">(</span><span class="n">authorizedScopes</span><span class="o">)</span>
                    <span class="o">.</span><span class="na">authorizationGrantType</span><span class="o">(</span><span class="k">new</span> <span class="nc">AuthorizationGrantType</span><span class="o">(</span><span class="nc">WechatGrantTypes</span><span class="o">.</span><span class="na">WECHAT_CODE</span><span class="o">))</span>
                    <span class="o">.</span><span class="na">authorizationGrant</span><span class="o">(</span><span class="n">wechatGrant</span><span class="o">)</span>
                    <span class="o">.</span><span class="na">tokenType</span><span class="o">(</span><span class="nc">OAuth2TokenType</span><span class="o">.</span><span class="na">REFRESH_TOKEN</span><span class="o">)</span>
                    <span class="o">.</span><span class="na">build</span><span class="o">();</span>
            <span class="n">refreshToken</span> <span class="o">=</span> <span class="o">(</span><span class="nc">OAuth2RefreshToken</span><span class="o">)</span> <span class="k">this</span><span class="o">.</span><span class="na">tokenGenerator</span><span class="o">.</span><span class="na">generate</span><span class="o">(</span><span class="n">refreshContext</span><span class="o">);</span>
        <span class="o">}</span>

        <span class="c1">// 4. 保存授权信息，供后续 refresh_token、撤销、内省使用</span>
        <span class="nc">OAuth2Authorization</span><span class="o">.</span><span class="na">Builder</span> <span class="n">builder</span> <span class="o">=</span> <span class="nc">OAuth2Authorization</span><span class="o">.</span><span class="na">withRegisteredClient</span><span class="o">(</span><span class="n">registeredClient</span><span class="o">)</span>
                <span class="o">.</span><span class="na">principalName</span><span class="o">(</span><span class="n">principal</span><span class="o">.</span><span class="na">getName</span><span class="o">())</span>
                <span class="o">.</span><span class="na">authorizationGrantType</span><span class="o">(</span><span class="k">new</span> <span class="nc">AuthorizationGrantType</span><span class="o">(</span><span class="nc">WechatGrantTypes</span><span class="o">.</span><span class="na">WECHAT_CODE</span><span class="o">))</span>
                <span class="o">.</span><span class="na">authorizedScopes</span><span class="o">(</span><span class="n">authorizedScopes</span><span class="o">)</span>
                <span class="o">.</span><span class="na">attribute</span><span class="o">(</span><span class="nc">Principal</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="n">principal</span><span class="o">)</span>
                <span class="o">.</span><span class="na">attribute</span><span class="o">(</span><span class="nc">OAuth2Authorization</span><span class="o">.</span><span class="na">AUTHORIZED_SCOPE_ATTRIBUTE_NAME</span><span class="o">,</span> <span class="n">authorizedScopes</span><span class="o">);</span>

        <span class="k">if</span> <span class="o">(</span><span class="n">accessToken</span> <span class="k">instanceof</span> <span class="nc">ClaimAccessor</span> <span class="n">claimAccessor</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">builder</span><span class="o">.</span><span class="na">token</span><span class="o">(</span><span class="n">accessToken</span><span class="o">,</span> <span class="n">metadata</span> <span class="o">-&gt;</span>
                    <span class="n">metadata</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="nc">OAuth2Authorization</span><span class="o">.</span><span class="na">Token</span><span class="o">.</span><span class="na">CLAIMS_METADATA_NAME</span><span class="o">,</span> <span class="n">claimAccessor</span><span class="o">.</span><span class="na">getClaims</span><span class="o">()));</span>
        <span class="o">}</span>
        <span class="k">else</span> <span class="o">{</span>
            <span class="n">builder</span><span class="o">.</span><span class="na">accessToken</span><span class="o">(</span><span class="n">accessToken</span><span class="o">);</span>
        <span class="o">}</span>

        <span class="k">this</span><span class="o">.</span><span class="na">authorizationService</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">builder</span><span class="o">.</span><span class="na">build</span><span class="o">());</span>

        <span class="k">return</span> <span class="k">new</span> <span class="nf">OAuth2AccessTokenAuthenticationToken</span><span class="o">(</span>
                <span class="n">registeredClient</span><span class="o">,</span> <span class="n">clientPrincipal</span><span class="o">,</span> <span class="n">accessToken</span><span class="o">,</span> <span class="n">refreshToken</span><span class="o">,</span> <span class="nc">Map</span><span class="o">.</span><span class="na">of</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">supports</span><span class="o">(</span><span class="nc">Class</span><span class="o">&lt;?&gt;</span> <span class="n">authentication</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">WechatCodeGrantAuthenticationToken</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">isAssignableFrom</span><span class="o">(</span><span class="n">authentication</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="nc">OAuth2ClientAuthenticationToken</span> <span class="nf">getAuthenticatedClientElseThrowInvalidClient</span><span class="o">(</span>
            <span class="nc">Authentication</span> <span class="n">authentication</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">OAuth2ClientAuthenticationToken</span> <span class="n">clientPrincipal</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
        <span class="k">if</span> <span class="o">(</span><span class="nc">OAuth2ClientAuthenticationToken</span><span class="o">.</span><span class="na">class</span>
                <span class="o">.</span><span class="na">isAssignableFrom</span><span class="o">(</span><span class="n">authentication</span><span class="o">.</span><span class="na">getPrincipal</span><span class="o">().</span><span class="na">getClass</span><span class="o">()))</span> <span class="o">{</span>
            <span class="n">clientPrincipal</span> <span class="o">=</span> <span class="o">(</span><span class="nc">OAuth2ClientAuthenticationToken</span><span class="o">)</span> <span class="n">authentication</span><span class="o">.</span><span class="na">getPrincipal</span><span class="o">();</span>
        <span class="o">}</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">clientPrincipal</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="n">clientPrincipal</span><span class="o">.</span><span class="na">isAuthenticated</span><span class="o">())</span> <span class="o">{</span>
            <span class="k">return</span> <span class="n">clientPrincipal</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nf">OAuth2AuthenticationException</span><span class="o">(</span><span class="nc">OAuth2ErrorCodes</span><span class="o">.</span><span class="na">INVALID_CLIENT</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>用户加载的逻辑很简单，首次登录用 openid 自动注册一个用户：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">CustomUserDetailsService</span> <span class="kd">implements</span> <span class="nc">UserDetailsService</span> <span class="o">{</span>

    <span class="kd">public</span> <span class="nc">UserDetails</span> <span class="nf">loadUserByWechatOpenid</span><span class="o">(</span><span class="nc">String</span> <span class="n">openid</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="n">userRepository</span><span class="o">.</span><span class="na">findByOpenid</span><span class="o">(</span><span class="n">openid</span><span class="o">)</span>
                <span class="o">.</span><span class="na">orElseGet</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">userRepository</span><span class="o">.</span><span class="na">createByOpenid</span><span class="o">(</span><span class="n">openid</span><span class="o">));</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">WechatUserDetails</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">UserDetails</span> <span class="nf">loadUserByUsername</span><span class="o">(</span><span class="nc">String</span> <span class="n">username</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nf">UnsupportedOperationException</span><span class="o">(</span><span class="s">"仅支持微信登录"</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="6-注册小程序-client">6. 注册小程序 client</h3>

<p>小程序注册为公开客户端（<code class="language-plaintext highlighter-rouge">ClientAuthenticationMethod.NONE</code>），不持有 secret，身份凭证就是那个一次性的微信 code。同时注册 <code class="language-plaintext highlighter-rouge">refresh_token</code> 授权类型，令牌过期后才能续期：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RegisteredClient</span> <span class="n">miniappClient</span> <span class="o">=</span> <span class="nc">RegisteredClient</span><span class="o">.</span><span class="na">withId</span><span class="o">(</span><span class="no">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="na">clientId</span><span class="o">(</span><span class="s">"miniapp-client"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">clientAuthenticationMethod</span><span class="o">(</span><span class="nc">ClientAuthenticationMethod</span><span class="o">.</span><span class="na">NONE</span><span class="o">)</span>
        <span class="o">.</span><span class="na">authorizationGrantType</span><span class="o">(</span><span class="k">new</span> <span class="nc">AuthorizationGrantType</span><span class="o">(</span><span class="nc">WechatGrantTypes</span><span class="o">.</span><span class="na">WECHAT_CODE</span><span class="o">))</span>
        <span class="o">.</span><span class="na">authorizationGrantType</span><span class="o">(</span><span class="nc">AuthorizationGrantType</span><span class="o">.</span><span class="na">REFRESH_TOKEN</span><span class="o">)</span>
        <span class="o">.</span><span class="na">scope</span><span class="o">(</span><span class="nc">OidcScopes</span><span class="o">.</span><span class="na">OPENID</span><span class="o">)</span>
        <span class="o">.</span><span class="na">scope</span><span class="o">(</span><span class="s">"message.read"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">build</span><span class="o">();</span>
</code></pre></div></div>

<h3 id="7-接入-token-端点">7. 接入 token 端点</h3>

<p>通过 <code class="language-plaintext highlighter-rouge">AuthorizationServerConfigurer</code> 的 <code class="language-plaintext highlighter-rouge">tokenEndpoint</code> 把 Converter 和 Provider 注册进去，<code class="language-plaintext highlighter-rouge">/oauth2/token</code> 端点就同时支持标准和自定义授权类型了：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Configuration</span>
<span class="nd">@EnableWebSecurity</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">AuthorizationServerConfig</span> <span class="o">{</span>

    <span class="nd">@Bean</span>
    <span class="nd">@Order</span><span class="o">(</span><span class="nc">Ordered</span><span class="o">.</span><span class="na">HIGHEST_PRECEDENCE</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">SecurityFilterChain</span> <span class="nf">authorizationServerSecurityFilterChain</span><span class="o">(</span>
            <span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">,</span>
            <span class="nc">WechatApiService</span> <span class="n">wechatApiService</span><span class="o">,</span>
            <span class="nc">CustomUserDetailsService</span> <span class="n">userDetailsService</span><span class="o">,</span>
            <span class="nc">OAuth2AuthorizationService</span> <span class="n">authorizationService</span><span class="o">,</span>
            <span class="nc">OAuth2TokenGenerator</span><span class="o">&lt;?&gt;</span> <span class="n">tokenGenerator</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>

        <span class="nc">OAuth2AuthorizationServerConfigurer</span> <span class="n">authorizationServerConfigurer</span> <span class="o">=</span>
                <span class="nc">OAuth2AuthorizationServerConfigurer</span><span class="o">.</span><span class="na">authorizationServer</span><span class="o">();</span>

        <span class="n">http</span>
            <span class="o">.</span><span class="na">securityMatcher</span><span class="o">(</span><span class="n">authorizationServerConfigurer</span><span class="o">.</span><span class="na">getEndpointsMatcher</span><span class="o">())</span>
            <span class="o">.</span><span class="na">with</span><span class="o">(</span><span class="n">authorizationServerConfigurer</span><span class="o">,</span> <span class="n">authorizationServer</span> <span class="o">-&gt;</span> <span class="n">authorizationServer</span>
                <span class="o">.</span><span class="na">oidc</span><span class="o">(</span><span class="nc">Customizer</span><span class="o">.</span><span class="na">withDefaults</span><span class="o">())</span>
                <span class="o">.</span><span class="na">tokenEndpoint</span><span class="o">(</span><span class="n">tokenEndpoint</span> <span class="o">-&gt;</span> <span class="n">tokenEndpoint</span>
                    <span class="o">.</span><span class="na">accessTokenRequestConverter</span><span class="o">(</span><span class="k">new</span> <span class="nc">WechatCodeGrantAuthenticationConverter</span><span class="o">())</span>
                    <span class="o">.</span><span class="na">authenticationProvider</span><span class="o">(</span><span class="k">new</span> <span class="nc">WechatCodeGrantAuthenticationProvider</span><span class="o">(</span>
                            <span class="n">wechatApiService</span><span class="o">,</span> <span class="n">userDetailsService</span><span class="o">,</span> <span class="n">authorizationService</span><span class="o">,</span> <span class="n">tokenGenerator</span><span class="o">))))</span>
            <span class="o">.</span><span class="na">authorizeHttpRequests</span><span class="o">(</span><span class="n">authorize</span> <span class="o">-&gt;</span> <span class="n">authorize</span><span class="o">.</span><span class="na">anyRequest</span><span class="o">().</span><span class="na">authenticated</span><span class="o">())</span>
            <span class="o">.</span><span class="na">exceptionHandling</span><span class="o">(</span><span class="n">exceptions</span> <span class="o">-&gt;</span> <span class="n">exceptions</span>
                <span class="o">.</span><span class="na">defaultAuthenticationEntryPointFor</span><span class="o">(</span>
                    <span class="k">new</span> <span class="nf">LoginUrlAuthenticationEntryPoint</span><span class="o">(</span><span class="s">"/login"</span><span class="o">),</span>
                    <span class="k">new</span> <span class="nf">MediaTypeRequestMatcher</span><span class="o">(</span><span class="nc">MediaType</span><span class="o">.</span><span class="na">TEXT_HTML</span><span class="o">)));</span>

        <span class="k">return</span> <span class="n">http</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="8-jwt-中携带-openid">8. JWT 中携带 openid</h3>

<p>自定义 <code class="language-plaintext highlighter-rouge">OAuth2TokenCustomizer</code>，把 openid 写进 access_token 的 claims，资源服务器就能直接从令牌里拿到微信身份，不需要查库：</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="nc">OAuth2TokenGenerator</span><span class="o">&lt;?&gt;</span> <span class="n">tokenGenerator</span><span class="o">(</span><span class="nc">JWKSource</span><span class="o">&lt;</span><span class="nc">SecurityContext</span><span class="o">&gt;</span> <span class="n">jwkSource</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">JwtGenerator</span> <span class="n">jwtGenerator</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">JwtGenerator</span><span class="o">(</span><span class="k">new</span> <span class="nc">NimbusJwtEncoder</span><span class="o">(</span><span class="n">jwkSource</span><span class="o">));</span>
    <span class="n">jwtGenerator</span><span class="o">.</span><span class="na">setJwtCustomizer</span><span class="o">(</span><span class="n">tokenCustomizer</span><span class="o">());</span>
    <span class="nc">OAuth2AccessTokenGenerator</span> <span class="n">accessTokenGenerator</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">OAuth2AccessTokenGenerator</span><span class="o">();</span>
    <span class="nc">OAuth2RefreshTokenGenerator</span> <span class="n">refreshTokenGenerator</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">OAuth2RefreshTokenGenerator</span><span class="o">();</span>
    <span class="k">return</span> <span class="k">new</span> <span class="nf">DelegatingOAuth2TokenGenerator</span><span class="o">(</span>
            <span class="n">jwtGenerator</span><span class="o">,</span> <span class="n">accessTokenGenerator</span><span class="o">,</span> <span class="n">refreshTokenGenerator</span><span class="o">);</span>
<span class="o">}</span>

<span class="kd">private</span> <span class="nc">OAuth2TokenCustomizer</span><span class="o">&lt;</span><span class="nc">JwtEncodingContext</span><span class="o">&gt;</span> <span class="nf">tokenCustomizer</span><span class="o">()</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">context</span> <span class="o">-&gt;</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="nc">OAuth2TokenType</span><span class="o">.</span><span class="na">ACCESS_TOKEN</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">context</span><span class="o">.</span><span class="na">getTokenType</span><span class="o">())</span>
                <span class="o">&amp;&amp;</span> <span class="n">context</span><span class="o">.</span><span class="na">getPrincipal</span><span class="o">().</span><span class="na">getPrincipal</span><span class="o">()</span> <span class="k">instanceof</span> <span class="nc">WechatUserDetails</span> <span class="n">wechatUser</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">context</span><span class="o">.</span><span class="na">getClaims</span><span class="o">().</span><span class="na">claim</span><span class="o">(</span><span class="s">"openid"</span><span class="o">,</span> <span class="n">wechatUser</span><span class="o">.</span><span class="na">getOpenid</span><span class="o">());</span>
        <span class="o">}</span>
    <span class="o">};</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="四小程序端实现">四、小程序端实现</h2>

<h3 id="1-登录并获取令牌">1. 登录并获取令牌</h3>

<p>把 <code class="language-plaintext highlighter-rouge">wx.request</code> 封装成 Promise，登录时先调 <code class="language-plaintext highlighter-rouge">wx.login()</code> 拿 code，再向 authserver 申请令牌：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">AUTH_SERVER</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">https://auth.example.com</span><span class="dl">'</span>

<span class="kd">function</span> <span class="nx">wxRequest</span><span class="p">(</span><span class="nx">options</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">wx</span><span class="p">.</span><span class="nx">request</span><span class="p">({</span>
      <span class="p">...</span><span class="nx">options</span><span class="p">,</span>
      <span class="na">success</span><span class="p">:</span> <span class="p">(</span><span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">res</span><span class="p">.</span><span class="nx">statusCode</span> <span class="o">&gt;=</span> <span class="mi">200</span> <span class="o">&amp;&amp;</span> <span class="nx">res</span><span class="p">.</span><span class="nx">statusCode</span> <span class="o">&lt;</span> <span class="mi">300</span><span class="p">)</span> <span class="nx">resolve</span><span class="p">(</span><span class="nx">res</span><span class="p">.</span><span class="nx">data</span><span class="p">)</span>
        <span class="k">else</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">res</span><span class="p">)</span>
      <span class="p">},</span>
      <span class="na">fail</span><span class="p">:</span> <span class="nx">reject</span>
    <span class="p">})</span>
  <span class="p">})</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">login</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">wx</span><span class="p">.</span><span class="nx">login</span><span class="p">({</span>
      <span class="na">success</span><span class="p">:</span> <span class="k">async</span> <span class="p">(</span><span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">res</span><span class="p">.</span><span class="nx">code</span><span class="p">)</span> <span class="p">{</span>
          <span class="nx">reject</span><span class="p">(</span><span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">wx.login 获取 code 失败</span><span class="dl">'</span><span class="p">))</span>
          <span class="k">return</span>
        <span class="p">}</span>
        <span class="kd">const</span> <span class="nx">tokenResp</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">wxRequest</span><span class="p">({</span>
          <span class="na">url</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">AUTH_SERVER</span><span class="p">}</span><span class="s2">/oauth2/token`</span><span class="p">,</span>
          <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
          <span class="na">header</span><span class="p">:</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/x-www-form-urlencoded</span><span class="dl">'</span> <span class="p">},</span>
          <span class="na">data</span><span class="p">:</span> <span class="p">{</span>
            <span class="na">grant_type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">urn:springcamp:params:oauth:grant-type:wechat-code</span><span class="dl">'</span><span class="p">,</span>
            <span class="na">client_id</span><span class="p">:</span> <span class="dl">'</span><span class="s1">miniapp-client</span><span class="dl">'</span><span class="p">,</span>
            <span class="na">code</span><span class="p">:</span> <span class="nx">res</span><span class="p">.</span><span class="nx">code</span>
          <span class="p">}</span>
        <span class="p">})</span>
        <span class="nx">saveTokens</span><span class="p">(</span><span class="nx">tokenResp</span><span class="p">)</span>
        <span class="nx">resolve</span><span class="p">(</span><span class="nx">tokenResp</span><span class="p">)</span>
      <span class="p">},</span>
      <span class="na">fail</span><span class="p">:</span> <span class="nx">reject</span>
    <span class="p">})</span>
  <span class="p">})</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">saveTokens</span><span class="p">(</span><span class="nx">tokenResp</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">wx</span><span class="p">.</span><span class="nx">setStorageSync</span><span class="p">(</span><span class="dl">'</span><span class="s1">access_token</span><span class="dl">'</span><span class="p">,</span> <span class="nx">tokenResp</span><span class="p">.</span><span class="nx">access_token</span><span class="p">)</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">tokenResp</span><span class="p">.</span><span class="nx">refresh_token</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">wx</span><span class="p">.</span><span class="nx">setStorageSync</span><span class="p">(</span><span class="dl">'</span><span class="s1">refresh_token</span><span class="dl">'</span><span class="p">,</span> <span class="nx">tokenResp</span><span class="p">.</span><span class="nx">refresh_token</span><span class="p">)</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="2-请求封装与令牌自动刷新">2. 请求封装与令牌自动刷新</h3>

<p>访问受保护接口时带上 <code class="language-plaintext highlighter-rouge">Bearer</code> 令牌，收到 401 说明 access_token 过期，用 refresh_token 刷新后重试一次：</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="kd">function</span> <span class="nx">requestWithAuth</span><span class="p">(</span><span class="nx">options</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">accessToken</span> <span class="o">=</span> <span class="nx">wx</span><span class="p">.</span><span class="nx">getStorageSync</span><span class="p">(</span><span class="dl">'</span><span class="s1">access_token</span><span class="dl">'</span><span class="p">)</span>
  <span class="k">try</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">await</span> <span class="nx">wxRequest</span><span class="p">({</span>
      <span class="p">...</span><span class="nx">options</span><span class="p">,</span>
      <span class="na">header</span><span class="p">:</span> <span class="p">{</span> <span class="p">...</span><span class="nx">options</span><span class="p">.</span><span class="nx">header</span><span class="p">,</span> <span class="na">Authorization</span><span class="p">:</span> <span class="s2">`Bearer </span><span class="p">${</span><span class="nx">accessToken</span><span class="p">}</span><span class="s2">`</span> <span class="p">}</span>
    <span class="p">})</span>
  <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">resp</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">resp</span><span class="p">.</span><span class="nx">statusCode</span> <span class="o">===</span> <span class="mi">401</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">await</span> <span class="nx">refreshToken</span><span class="p">()</span>
      <span class="k">return</span> <span class="nx">wxRequest</span><span class="p">({</span>
        <span class="p">...</span><span class="nx">options</span><span class="p">,</span>
        <span class="na">header</span><span class="p">:</span> <span class="p">{</span>
          <span class="p">...</span><span class="nx">options</span><span class="p">.</span><span class="nx">header</span><span class="p">,</span>
          <span class="na">Authorization</span><span class="p">:</span> <span class="s2">`Bearer </span><span class="p">${</span><span class="nx">wx</span><span class="p">.</span><span class="nx">getStorageSync</span><span class="p">(</span><span class="dl">'</span><span class="s1">access_token</span><span class="dl">'</span><span class="p">)}</span><span class="s2">`</span>
        <span class="p">}</span>
      <span class="p">})</span>
    <span class="p">}</span>
    <span class="k">throw</span> <span class="nx">resp</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">async</span> <span class="kd">function</span> <span class="nx">refreshToken</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">tokenResp</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">wxRequest</span><span class="p">({</span>
    <span class="na">url</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">AUTH_SERVER</span><span class="p">}</span><span class="s2">/oauth2/token`</span><span class="p">,</span>
    <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">header</span><span class="p">:</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/x-www-form-urlencoded</span><span class="dl">'</span> <span class="p">},</span>
    <span class="na">data</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">grant_type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">refresh_token</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">client_id</span><span class="p">:</span> <span class="dl">'</span><span class="s1">miniapp-client</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">refresh_token</span><span class="p">:</span> <span class="nx">wx</span><span class="p">.</span><span class="nx">getStorageSync</span><span class="p">(</span><span class="dl">'</span><span class="s1">refresh_token</span><span class="dl">'</span><span class="p">)</span>
    <span class="p">}</span>
  <span class="p">})</span>
  <span class="nx">saveTokens</span><span class="p">(</span><span class="nx">tokenResp</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>注意两点：</p>

<ul>
  <li>并发请求同时收到 401 时会触发多次刷新，而 Spring Authorization Server 默认轮换 refresh_token（旧令牌随即失效），所以需要加一把锁保证同一时刻只有一个刷新请求，刷新失败（refresh_token 也过期了）则重新走 <code class="language-plaintext highlighter-rouge">login()</code>；</li>
  <li><code class="language-plaintext highlighter-rouge">login()</code> 应该在 app 启动时（如 <code class="language-plaintext highlighter-rouge">App.onLaunch</code>）执行一次，并处理”code 只能用一次”的问题：静默登录失败时提示用户。</li>
</ul>

<h3 id="3-调用受保护接口">3. 调用受保护接口</h3>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">requestWithAuth</span><span class="p">({</span>
  <span class="na">url</span><span class="p">:</span> <span class="dl">'</span><span class="s1">https://api.example.com/messages</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">GET</span><span class="dl">'</span>
<span class="p">}).</span><span class="nx">then</span><span class="p">(</span><span class="nx">data</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">openid:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">data</span><span class="p">.</span><span class="nx">openid</span><span class="p">,</span> <span class="dl">'</span><span class="s1">messages:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">data</span><span class="p">.</span><span class="nx">messages</span><span class="p">)</span>
<span class="p">})</span>
</code></pre></div></div>

<h2 id="五资源服务器防护">五、资源服务器防护</h2>

<p>资源服务器是标准的 JWT 校验配置，只需要知道 authserver 的签发地址，启动时自动获取公钥（JWKS）并在本地验签：</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">spring.security.oauth2.resourceserver.jwt.issuer-uri</span><span class="p">=</span><span class="s">http://localhost:9000</span>
</code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Configuration</span>
<span class="nd">@EnableWebSecurity</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ResourceServerConfig</span> <span class="o">{</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">SecurityFilterChain</span> <span class="nf">securityFilterChain</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="n">http</span>
            <span class="o">.</span><span class="na">authorizeHttpRequests</span><span class="o">(</span><span class="n">authorize</span> <span class="o">-&gt;</span> <span class="n">authorize</span><span class="o">.</span><span class="na">anyRequest</span><span class="o">().</span><span class="na">authenticated</span><span class="o">())</span>
            <span class="o">.</span><span class="na">oauth2ResourceServer</span><span class="o">(</span><span class="n">resourceServer</span> <span class="o">-&gt;</span> <span class="n">resourceServer</span><span class="o">.</span><span class="na">jwt</span><span class="o">(</span><span class="nc">Customizer</span><span class="o">.</span><span class="na">withDefaults</span><span class="o">()));</span>
        <span class="k">return</span> <span class="n">http</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>业务代码里通过 <code class="language-plaintext highlighter-rouge">@AuthenticationPrincipal</code> 直接拿到 <code class="language-plaintext highlighter-rouge">Jwt</code>，读取其中的 openid：</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">MessageController</span> <span class="o">{</span>

    <span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/messages"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;</span> <span class="nf">messages</span><span class="o">(</span><span class="nd">@AuthenticationPrincipal</span> <span class="nc">Jwt</span> <span class="n">jwt</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">Map</span><span class="o">.</span><span class="na">of</span><span class="o">(</span>
                <span class="s">"openid"</span><span class="o">,</span> <span class="n">jwt</span><span class="o">.</span><span class="na">getClaimAsString</span><span class="o">(</span><span class="s">"openid"</span><span class="o">),</span>
                <span class="s">"messages"</span><span class="o">,</span> <span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"Hello WeChat Mini Program"</span><span class="o">));</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="六验证与测试">六、验证与测试</h2>

<p>code 只能从小程序运行环境中真实获取，拿到 code 后可以直接用 curl 模拟令牌申请：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> POST http://localhost:9000/oauth2/token <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Content-Type: application/x-www-form-urlencoded"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s2">"grant_type=urn:springcamp:params:oauth:grant-type:wechat-code"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s2">"client_id=miniapp-client"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s2">"code=0c3lxY000xxxxxx1"</span>
</code></pre></div></div>

<p>正常返回标准的 OAuth2 令牌响应：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"access_token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"eyJraWQiOiJ3ZWMta2V5IiwiYWxnIjoiUlMyNTYifQ..."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"refresh_token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"RIbbJH4qSMewx8yXkCs6vhXl0kvBNnsO..."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"scope"</span><span class="p">:</span><span class="w"> </span><span class="s2">"openid message.read"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"token_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Bearer"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"expires_in"</span><span class="p">:</span><span class="w"> </span><span class="mi">299</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>携带令牌访问资源接口：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl http://localhost:8081/messages <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Authorization: Bearer eyJraWQiOiJ3ZWMta2V5IiwiYWxnIjoiUlMyNTYifQ..."</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="nl">"openid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"oX7t85Z0xxxxxxxxxxxx"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"messages"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"Hello WeChat Mini Program"</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>本地开发调试时，如果没有真实的小程序账号，可以在 <code class="language-plaintext highlighter-rouge">WechatApiService</code> 里加一个开关，用 mock 的 openid 代替 <code class="language-plaintext highlighter-rouge">jscode2session</code> 调用，验证整个授权链路时使用mock，后续如果有真实的小程序可以修改为接真实的微信环境。</p>

<h2 id="七安全要点">七、安全要点</h2>

<ul>
  <li><strong>secret 只在服务端</strong>：<code class="language-plaintext highlighter-rouge">appid</code>/<code class="language-plaintext highlighter-rouge">secret</code> 通过环境变量或配置中心注入 authserver，绝不能打进小程序包（小程序包可被反编译）；</li>
  <li><strong>公开客户端</strong>：小程序 client 使用 <code class="language-plaintext highlighter-rouge">ClientAuthenticationMethod.NONE</code>，不使用 client_secret，身份凭证就是一次性的微信 code；</li>
  <li><strong>code 天然防重放</strong>：5 分钟有效、一次性使用，重放攻击会得到 40029 错误；</li>
  <li><strong>令牌短生命周期 + 轮换</strong>：access_token 保持短有效期（如 5 分钟），refresh_token 刷新时自动轮换，泄露后的影响窗口很小；</li>
  <li><strong>session_key 不下发</strong>：session_key 只在服务端使用（如解密手机号），绝不能返回给小程序端；</li>
  <li><strong>全链路 HTTPS</strong>：生产环境 authserver 和资源服务器必须启用 HTTPS，并在微信公众平台配置 request 合法域名；</li>
  <li><strong>无状态资源服务</strong>：JWT 自包含，资源服务器不共享任何会话状态，可以随意水平扩展。</li>
</ul>

<h2 id="八总结">八、总结</h2>

<p>微信小程序虽然不能走标准的授权码流程，但借助 Spring Authorization Server 的扩展授权类型机制，只需要实现一个 Converter 和一个 Provider，就能把 <code class="language-plaintext highlighter-rouge">wx.login()</code> 的微信登录接入标准 OAuth2 令牌体系：小程序作为公开客户端用 code 换 JWT，资源服务器按标准方式校验 Bearer 令牌，微信身份被隔离在 authserver 一侧。</p>

<p>这个方案的价值在于：授权服务器同时服务 Web（授权码 + PKCE）和小程序（wechat-code）等多种客户端形态，令牌签发、刷新、撤销全部作为统一逻辑实现，业务资源服务器则完全不感知客户端差异。示例项目里也包含了 Web 端授权码流程的配置，两种客户端可以共用同一个 authserver。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><category term="oauth2" /><category term="wechat" /><category term="featured" /><summary type="html"><![CDATA[微信小程序是一种典型的原生客户端应用：没有浏览器地址栏，无法完成 OAuth2 授权码流程所依赖的重定向交互；代码包可以被反编译，客户端没有安全存放密钥的地方，标准的授权码流程并不能直接套用。本文介绍如何利用 Spring Authorization Server 的扩展授权类型（Extension Grant）机制，为小程序定制一个 wechat-code 授权模式，用 wx.login() 产生的 code 换取标准的 OAuth2 令牌，实现对后端资源的统一授权防护。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/spring-oauth-wechat-miniprogram.jpg" /><media:content medium="image" url="https://springcamp.cn/assets/images/spring-oauth-wechat-miniprogram.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Spring AI集成DeepSeek大模型</title><link href="https://springcamp.cn/spring-ai-deepseek/" rel="alternate" type="text/html" title="Spring AI集成DeepSeek大模型" /><published>2026-08-01T07:30:00+00:00</published><updated>2026-08-01T07:30:00+00:00</updated><id>https://springcamp.cn/spring-ai-deepseek</id><content type="html" xml:base="https://springcamp.cn/spring-ai-deepseek/"><![CDATA[<p>Spring AI 是spring官方提供的大模型集成框架，采用统一的抽象接口对接各类大模型，DeepSeek 是国内热门的开源大模型，本文介绍如何通过Spring AI集成DeepSeek，实现基础对话、流式响应、推理模型思维链以及Function Calling等功能。</p>

<p>具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-ai-deepseek">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-ai-deepseek</a></p>

<h2 id="一概述">一、概述</h2>

<p>Spring AI 遵循了spring生态一贯的设计理念，通过starter自动装配的方式，把不同厂商的大模型统一封装到 ChatClient 和 ChatModel 这套抽象接口之下，开发者只需要引入对应的starter、配置api-key，就可以像调用普通bean一样调用大模型，无需关心底层的http请求和协议细节。</p>

<p>DeepSeek 提供了与OpenAI兼容的接口，同时具备普通对话模型和推理模型（思维链）两种能力。本示例项目基于Spring Boot 4.1.0 和 Spring AI 2.0.0，用极简的代码演示了四种典型用法：</p>

<ul>
  <li>基础同步对话</li>
  <li>流式响应（SSE）</li>
  <li>推理模型的思维链输出</li>
  <li>Function Calling（工具调用）</li>
</ul>

<h2 id="二项目依赖与配置">二、项目依赖与配置</h2>

<p>首先引入spring-ai的deepseek starter，整个模块只需要两个依赖：</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ext</span> <span class="o">{</span>
    <span class="n">set</span><span class="o">(</span><span class="s1">'springAiVersion'</span><span class="o">,</span> <span class="s2">"2.0.0"</span><span class="o">)</span>
<span class="o">}</span>

<span class="n">dependencies</span> <span class="o">{</span>
    <span class="n">implementation</span> <span class="s1">'org.springframework.ai:spring-ai-starter-model-deepseek'</span>
    <span class="n">implementation</span> <span class="s1">'org.springframework.boot:spring-boot-starter-web'</span>
<span class="o">}</span>

<span class="n">dependencyManagement</span> <span class="o">{</span>
    <span class="n">imports</span> <span class="o">{</span>
        <span class="n">mavenBom</span> <span class="s2">"org.springframework.ai:spring-ai-bom:${springAiVersion}"</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>通过 spring-ai-bom 统一管理spring-ai相关依赖的版本，starter会自动装配好 DeepSeekChatModel 和 ChatClient.Builder，我们直接注入即可使用。</p>

<p>接下来在 application.properties 中配置DeepSeek的api-key和模型参数：</p>

<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">server.port</span><span class="p">=</span><span class="s">8080</span>
<span class="py">spring.main.banner-mode</span><span class="p">=</span><span class="s">off</span>
<span class="py">logging.level.root</span><span class="p">=</span><span class="s">INFO</span>

<span class="c"># DeepSeek configuration
</span><span class="py">spring.ai.deepseek.api-key</span><span class="p">=</span><span class="s">your-api-key</span>
<span class="py">spring.ai.deepseek.chat.model</span><span class="p">=</span><span class="s">deepseek-chat</span>
<span class="py">spring.ai.deepseek.chat.temperature</span><span class="p">=</span><span class="s">0.8</span>
</code></pre></div></div>

<p>api-key 需要在DeepSeek开放平台（https://platform.deepseek.com）申请，示例中写死在配置文件里只是为了演示，生产环境应该通过环境变量或配置中心注入，避免泄露。</p>

<h2 id="三配置chatclient">三、配置ChatClient</h2>

<p>spring-ai-deepseek starter 会自动提供一个 ChatClient.Builder，我们只需要在启动类中构建出 ChatClient 这个bean：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@SpringBootApplication</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Application</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="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">SpringApplication</span><span class="o">.</span><span class="na">run</span><span class="o">(</span><span class="nc">Application</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="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">ChatClient</span> <span class="nf">chatClient</span><span class="o">(</span><span class="nc">ChatClient</span><span class="o">.</span><span class="na">Builder</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">build</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>ChatClient 是spring-ai提供的模型无关的高层门面，后续的对话、流式响应、工具调用都基于它完成。</p>

<h2 id="四基础对话与流式响应">四、基础对话与流式响应</h2>

<p>基础对话是调用大模型最简单的方式，通过 ChatClient 的链式api一行代码就能完成：</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="n">value</span> <span class="o">=</span> <span class="s">"/chat"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">String</span> <span class="nf">chat</span><span class="o">(</span><span class="nd">@RequestParam</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"message"</span><span class="o">,</span> <span class="n">defaultValue</span> <span class="o">=</span> <span class="s">"Tell me a joke"</span><span class="o">)</span> <span class="nc">String</span> <span class="n">message</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">chatClient</span><span class="o">.</span><span class="na">prompt</span><span class="o">().</span><span class="na">user</span><span class="o">(</span><span class="n">message</span><span class="o">).</span><span class="na">call</span><span class="o">().</span><span class="na">content</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<p>调用 <code class="language-plaintext highlighter-rouge">/ai/chat?message=讲个笑话</code> 接口，会同步等待大模型返回完整结果后再响应。</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="n">value</span> <span class="o">=</span> <span class="s">"/chat/stream"</span><span class="o">,</span> <span class="n">produces</span> <span class="o">=</span> <span class="nc">MediaType</span><span class="o">.</span><span class="na">TEXT_PLAIN_VALUE</span> <span class="o">+</span> <span class="s">";charset=UTF-8"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">Flux</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="nf">stream</span><span class="o">(</span><span class="nd">@RequestParam</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"message"</span><span class="o">,</span> <span class="n">defaultValue</span> <span class="o">=</span> <span class="s">"Tell me a joke"</span><span class="o">)</span> <span class="nc">String</span> <span class="n">message</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">chatClient</span><span class="o">.</span><span class="na">prompt</span><span class="o">().</span><span class="na">user</span><span class="o">(</span><span class="n">message</span><span class="o">).</span><span class="na">stream</span><span class="o">().</span><span class="na">content</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<p>只需要把 <code class="language-plaintext highlighter-rouge">call()</code> 换成 <code class="language-plaintext highlighter-rouge">stream()</code>，返回值由 String 变成 <code class="language-plaintext highlighter-rouge">Flux&lt;String&gt;</code>，spring会以SSE（Server-Sent Events）的方式把大模型逐个生成的token推送给客户端，这里显式设置了 <code class="language-plaintext highlighter-rouge">text/plain;charset=UTF-8</code>，避免中文出现乱码。</p>

<h2 id="五推理模型思维链">五、推理模型思维链</h2>

<p>DeepSeek 的推理在给出答案之前会先输出一段思考过程，也就是思维链（Chain of Thought），这是推理模型区别于普通对话模型的关键能力。</p>

<p>ChatClient 是模型无关的通用接口，无法直接获取DeepSeek专属的思考过程，因此这里我们绕过ChatClient，直接注入底层的 DeepSeekChatModel：</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">"/reasoning"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="nf">reasoning</span><span class="o">(</span><span class="nd">@RequestParam</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"message"</span><span class="o">,</span> <span class="n">defaultValue</span> <span class="o">=</span> <span class="s">"9.11 和 9.8 哪个大？"</span><span class="o">)</span> <span class="nc">String</span> <span class="n">message</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">ChatResponse</span> <span class="n">response</span> <span class="o">=</span> <span class="n">chatModel</span><span class="o">.</span><span class="na">call</span><span class="o">(</span><span class="k">new</span> <span class="nc">Prompt</span><span class="o">(</span><span class="n">message</span><span class="o">));</span>
    <span class="nc">DeepSeekAssistantMessage</span> <span class="n">output</span> <span class="o">=</span> <span class="o">(</span><span class="nc">DeepSeekAssistantMessage</span><span class="o">)</span> <span class="nc">Objects</span><span class="o">.</span><span class="na">requireNonNull</span><span class="o">(</span><span class="n">response</span><span class="o">.</span><span class="na">getResult</span><span class="o">()).</span><span class="na">getOutput</span><span class="o">();</span>
    <span class="k">return</span> <span class="nc">Map</span><span class="o">.</span><span class="na">of</span><span class="o">(</span>
            <span class="s">"reasoning"</span><span class="o">,</span> <span class="n">output</span><span class="o">.</span><span class="na">getReasoningContent</span><span class="o">()</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">?</span> <span class="n">output</span><span class="o">.</span><span class="na">getReasoningContent</span><span class="o">()</span> <span class="o">:</span> <span class="s">""</span><span class="o">,</span>
            <span class="s">"answer"</span><span class="o">,</span> <span class="n">output</span><span class="o">.</span><span class="na">getText</span><span class="o">()</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">?</span> <span class="n">output</span><span class="o">.</span><span class="na">getText</span><span class="o">()</span> <span class="o">:</span> <span class="s">""</span>
    <span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>把返回结果强转为 DeepSeekAssistantMessage 后，就可以分别通过 <code class="language-plaintext highlighter-rouge">getReasoningContent()</code> 拿到思考过程，通过 <code class="language-plaintext highlighter-rouge">getText()</code> 拿到最终答案。代码里对这两个字段都做了非空判断，因为只有推理模型才会返回 <code class="language-plaintext highlighter-rouge">reasoningContent</code>，普通对话模型这个字段为null。</p>

<p>需要注意的是，要真正拿到思维链需要模型具备思维能力，DeepSeek当前的两个模型（deepseek-v4-flash和deepseek-v4-pro）都具备思维能力，普通对话模型不会返回思考过程。默认示例问题”9.11 和 9.8 哪个大？”正是检验模型推理能力的经典测试题。</p>

<h2 id="六function-calling">六、Function Calling</h2>

<p>Function Calling 让大模型能够调用外部的java方法，从而获取实时数据或执行具体操作。spring-ai通过 <code class="language-plaintext highlighter-rouge">@Tool</code> 注解把一个普通的java方法声明为可被模型调用的工具，无需手写json schema。</p>

<p>定义一个模拟的天气查询服务：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">WeatherService</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="no">CONDITIONS</span> <span class="o">=</span> <span class="nc">Map</span><span class="o">.</span><span class="na">of</span><span class="o">(</span>
            <span class="mi">0</span><span class="o">,</span> <span class="s">"晴"</span><span class="o">,</span> <span class="mi">1</span><span class="o">,</span> <span class="s">"多云"</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="s">"小雨"</span><span class="o">,</span> <span class="mi">3</span><span class="o">,</span> <span class="s">"小雪"</span>
    <span class="o">);</span>

    <span class="nd">@Tool</span><span class="o">(</span><span class="n">description</span> <span class="o">=</span> <span class="s">"查询指定城市的当前天气情况，返回温度和天气状况"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getCurrentWeather</span><span class="o">(</span><span class="nc">String</span> <span class="n">city</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">int</span> <span class="n">temp</span> <span class="o">=</span> <span class="nc">ThreadLocalRandom</span><span class="o">.</span><span class="na">current</span><span class="o">().</span><span class="na">nextInt</span><span class="o">(-</span><span class="mi">5</span><span class="o">,</span> <span class="mi">35</span><span class="o">);</span>
        <span class="nc">String</span> <span class="n">condition</span> <span class="o">=</span> <span class="no">CONDITIONS</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="nc">ThreadLocalRandom</span><span class="o">.</span><span class="na">current</span><span class="o">().</span><span class="na">nextInt</span><span class="o">(</span><span class="no">CONDITIONS</span><span class="o">.</span><span class="na">size</span><span class="o">()));</span>
        <span class="k">return</span> <span class="nc">String</span><span class="o">.</span><span class="na">format</span><span class="o">(</span><span class="s">"%s 当前天气：%s，气温 %d°C"</span><span class="o">,</span> <span class="n">city</span><span class="o">,</span> <span class="n">condition</span><span class="o">,</span> <span class="n">temp</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>在调用ChatClient时，通过 <code class="language-plaintext highlighter-rouge">tools()</code> 方法把这个服务传给模型：</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="n">value</span> <span class="o">=</span> <span class="s">"/tool"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">String</span> <span class="nf">tool</span><span class="o">(</span><span class="nd">@RequestParam</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"message"</span><span class="o">,</span> <span class="n">defaultValue</span> <span class="o">=</span> <span class="s">"北京和上海今天天气怎么样？"</span><span class="o">)</span> <span class="nc">String</span> <span class="n">message</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">chatClient</span><span class="o">.</span><span class="na">prompt</span><span class="o">().</span><span class="na">user</span><span class="o">(</span><span class="n">message</span><span class="o">).</span><span class="na">tools</span><span class="o">(</span><span class="n">weatherService</span><span class="o">).</span><span class="na">call</span><span class="o">().</span><span class="na">content</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<p>当用户询问”北京和上海今天天气怎么样？”时，模型会自主判断需要查询天气，自动调用 <code class="language-plaintext highlighter-rouge">getCurrentWeather</code> 方法两次，分别查询北京和上海的天气，再把结果组织成自然语言返回。整个调用过程由模型决定是否调用、调用几次，这就是Function Calling的核心能力。</p>

<h2 id="七查看function-calling的调用过程">七、查看Function Calling的调用过程</h2>

<p>上一节的 <code class="language-plaintext highlighter-rouge">/ai/tool</code> 接口只返回了模型的最终答案，但模型在背后到底调用了几次工具、每次传了什么参数、工具返回了什么，从结果里是看不出来的。实际开发和调试中，把这些调用过程暴露出来，能帮助我们观察模型的行为，判断它是否按预期调用了工具。</p>

<p>我们可以定义一个请求作用域的 ToolCallRecorder 来记录单次请求内的工具调用：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="nd">@RequestScope</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ToolCallRecorder</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">ToolInvocation</span><span class="o">&gt;</span> <span class="n">invocations</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;();</span>

    <span class="kd">public</span> <span class="n">record</span> <span class="nf">ToolInvocation</span><span class="o">(</span><span class="nc">String</span> <span class="n">tool</span><span class="o">,</span> <span class="nc">String</span> <span class="n">arguments</span><span class="o">,</span> <span class="nc">String</span> <span class="n">result</span><span class="o">)</span> <span class="o">{</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">record</span><span class="o">(</span><span class="nc">String</span> <span class="n">tool</span><span class="o">,</span> <span class="nc">String</span> <span class="n">arguments</span><span class="o">,</span> <span class="nc">String</span> <span class="n">result</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">invocations</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="k">new</span> <span class="nc">ToolInvocation</span><span class="o">(</span><span class="n">tool</span><span class="o">,</span> <span class="n">arguments</span><span class="o">,</span> <span class="n">result</span><span class="o">));</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">ToolInvocation</span><span class="o">&gt;</span> <span class="nf">get</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">Collections</span><span class="o">.</span><span class="na">unmodifiableList</span><span class="o">(</span><span class="n">invocations</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>这里用 <code class="language-plaintext highlighter-rouge">@RequestScope</code> 把作用域限定在单个HTTP请求内，每个请求都会创建一个独立的实例，请求结束后随之销毁。借助这种请求级别的隔离，无需 ThreadLocal 就能让不同请求的调用记录互不干扰，对虚拟线程也很友好。每条调用过程用 record 封装了工具名、入参和返回值三个字段。</p>

<p>接下来在 WeatherService 中注入 ToolCallRecorder，在工具方法内部把每次调用记录下来：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">WeatherService</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="no">CONDITIONS</span> <span class="o">=</span> <span class="nc">Map</span><span class="o">.</span><span class="na">of</span><span class="o">(</span>
            <span class="mi">0</span><span class="o">,</span> <span class="s">"晴"</span><span class="o">,</span> <span class="mi">1</span><span class="o">,</span> <span class="s">"多云"</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="s">"小雨"</span><span class="o">,</span> <span class="mi">3</span><span class="o">,</span> <span class="s">"小雪"</span>
    <span class="o">);</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">ToolCallRecorder</span> <span class="n">recorder</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">WeatherService</span><span class="o">(</span><span class="nc">ToolCallRecorder</span> <span class="n">recorder</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">recorder</span> <span class="o">=</span> <span class="n">recorder</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Tool</span><span class="o">(</span><span class="n">description</span> <span class="o">=</span> <span class="s">"查询指定城市的当前天气情况，返回温度和天气状况"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getCurrentWeather</span><span class="o">(</span><span class="nc">String</span> <span class="n">city</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">int</span> <span class="n">temp</span> <span class="o">=</span> <span class="nc">ThreadLocalRandom</span><span class="o">.</span><span class="na">current</span><span class="o">().</span><span class="na">nextInt</span><span class="o">(-</span><span class="mi">5</span><span class="o">,</span> <span class="mi">35</span><span class="o">);</span>
        <span class="nc">String</span> <span class="n">condition</span> <span class="o">=</span> <span class="no">CONDITIONS</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="nc">ThreadLocalRandom</span><span class="o">.</span><span class="na">current</span><span class="o">().</span><span class="na">nextInt</span><span class="o">(</span><span class="no">CONDITIONS</span><span class="o">.</span><span class="na">size</span><span class="o">()));</span>
        <span class="nc">String</span> <span class="n">result</span> <span class="o">=</span> <span class="nc">String</span><span class="o">.</span><span class="na">format</span><span class="o">(</span><span class="s">"%s 当前天气：%s，气温 %d°C"</span><span class="o">,</span> <span class="n">city</span><span class="o">,</span> <span class="n">condition</span><span class="o">,</span> <span class="n">temp</span><span class="o">);</span>
        <span class="n">recorder</span><span class="o">.</span><span class="na">record</span><span class="o">(</span><span class="s">"getCurrentWeather"</span><span class="o">,</span> <span class="n">city</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="o">}</span>
</code></pre></div></div>

<p>相比上一节，只是在方法返回前增加了一行 <code class="language-plaintext highlighter-rouge">recorder.record(...)</code>，把工具名、入参 city 和返回值记录下来。</p>

<p>最后改造 <code class="language-plaintext highlighter-rouge">/ai/tool</code> 接口，把返回值从 String 改为 Map，同时返回模型的最终答案和完整的工具调用过程：</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="n">value</span> <span class="o">=</span> <span class="s">"/tool"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;</span> <span class="nf">tool</span><span class="o">(</span><span class="nd">@RequestParam</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"message"</span><span class="o">,</span> <span class="n">defaultValue</span> <span class="o">=</span> <span class="s">"北京和上海今天天气怎么样？"</span><span class="o">)</span> <span class="nc">String</span> <span class="n">message</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">answer</span> <span class="o">=</span> <span class="n">chatClient</span><span class="o">.</span><span class="na">prompt</span><span class="o">().</span><span class="na">user</span><span class="o">(</span><span class="n">message</span><span class="o">).</span><span class="na">tools</span><span class="o">(</span><span class="n">weatherService</span><span class="o">).</span><span class="na">call</span><span class="o">().</span><span class="na">content</span><span class="o">();</span>
    <span class="k">return</span> <span class="nc">Map</span><span class="o">.</span><span class="na">of</span><span class="o">(</span>
            <span class="s">"answer"</span><span class="o">,</span> <span class="n">answer</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">?</span> <span class="n">answer</span> <span class="o">:</span> <span class="s">""</span><span class="o">,</span>
            <span class="s">"toolCalls"</span><span class="o">,</span> <span class="n">toolCallRecorder</span><span class="o">.</span><span class="na">get</span><span class="o">()</span>
    <span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>再次调用 <code class="language-plaintext highlighter-rouge">/ai/tool?message=北京和上海今天天气怎么样？</code>，返回结果里除了 answer，还多了一个 toolCalls 数组，里面记录了模型调用工具的完整过程：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"answer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"北京当前多云，气温20°C；上海当前小雨，气温25°C。"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"toolCalls"</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="nl">"tool"</span><span class="p">:</span><span class="w"> </span><span class="s2">"getCurrentWeather"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"arguments"</span><span class="p">:</span><span class="w"> </span><span class="s2">"北京"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"result"</span><span class="p">:</span><span class="w"> </span><span class="s2">"北京 当前天气：多云，气温 20°C"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"tool"</span><span class="p">:</span><span class="w"> </span><span class="s2">"getCurrentWeather"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"arguments"</span><span class="p">:</span><span class="w"> </span><span class="s2">"上海"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"result"</span><span class="p">:</span><span class="w"> </span><span class="s2">"上海 当前天气：小雨，气温 25°C"</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>从 toolCalls 可以清楚看到，模型针对北京和上海分别调用了一次 <code class="language-plaintext highlighter-rouge">getCurrentWeather</code>，每次的入参和返回值都被完整记录下来，这样我们就能直观地观察模型是如何使用工具的。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><category term="featured" /><summary type="html"><![CDATA[Spring AI 是spring官方提供的大模型集成框架，采用统一的抽象接口对接各类大模型，DeepSeek 是国内热门的开源大模型，本文介绍如何通过Spring AI集成DeepSeek，实现基础对话、流式响应、推理模型思维链以及Function Calling等功能。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/spring-ai-deepseek.jpg" /><media:content medium="image" url="https://springcamp.cn/assets/images/spring-ai-deepseek.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Vue对接Spring Authorization Server</title><link href="https://springcamp.cn/spring-oauth-vue/" rel="alternate" type="text/html" title="Vue对接Spring Authorization Server" /><published>2025-12-31T07:30:00+00:00</published><updated>2025-12-31T07:30:00+00:00</updated><id>https://springcamp.cn/spring-oauth-vue</id><content type="html" xml:base="https://springcamp.cn/spring-oauth-vue/"><![CDATA[<p>通过Spring Authorization Server对vue应用进行授权防护。</p>

<p><a href="https://github.com/qihaiyan/ng-boot-oauth">示例项目 https://github.com/qihaiyan/ng-boot-oauth</a></p>

<p><img src="../assets/images/login.png" alt="login" /></p>

<p>运行该示例代码时，要先启动 <code class="language-plaintext highlighter-rouge">authserver</code>，再启动 <code class="language-plaintext highlighter-rouge">ui-spa-vue</code>。</p>

<h2 id="一概述">一、概述</h2>

<p>vue对接oauth进行认证防护的介绍已经很多了，但是跟Spring Authorization Server集成的示例比较少，并且很多还都是vue2的代码，本文主要是介绍vue3跟Spring Authorization Server集成的方法和示例。</p>

<h2 id="二核心交互流程">二、核心交互流程</h2>

<p>这个流程图展示了完整的 OAuth 2.0 + OpenID Connect 授权码流程，包括应用初始化、用户登录、令牌管理和登出等关键步骤。</p>

<p><img src="../assets/images/vue-oauth2-flow.png" alt="login" /></p>

<h3 id="1-初始化流程-maints17">1. 初始化流程 (main.ts:17)</h3>
<ul>
  <li>创建 OAuthClient 实例并配置参数</li>
  <li>调用 initialize() 执行服务发现</li>
  <li>自动处理令牌刷新或授权码回调
    <h3 id="2-授权码流程-oauthclientts240">2. 授权码流程 (OAuthClient.ts:240)</h3>
  </li>
  <li>使用 PKCE (Proof Key for Code Exchange) 增强安全性</li>
  <li>生成 code_verifier 和 code_challenge</li>
  <li>重定向到授权服务器获取授权码</li>
  <li>使用授权码换取访问令牌
    <h3 id="3-令牌存储-oauthclientts130-145">3. 令牌存储 (OAuthClient.ts:130-145)</h3>
  </li>
  <li>access_token : 访问受保护资源的令牌</li>
  <li>refresh_token : 用于刷新访问令牌</li>
  <li>id_token : OpenID Connect 身份令牌</li>
  <li>code_verifier : PKCE 验证码
    <h3 id="4-登出流程-oauthclientts369">4. 登出流程 (OAuthClient.ts:369)</h3>
  </li>
  <li>清除本地存储的令牌</li>
  <li>使用 id_token_hint 参数通知授权服务器</li>
  <li>重定向到授权服务器的登出端点
    <h3 id="5-安全特性">5. 安全特性</h3>
  </li>
  <li>✅ PKCE (S256) 防止授权码拦截攻击</li>
  <li>✅ HTTPS 限制（开发环境可通过 allowInsecureRequests 绕过）</li>
  <li>✅ 令牌自动刷新机制</li>
  <li>✅ 本地存储持久化</li>
</ul>

<h2 id="三vue端的核心配置">三、vue端的核心配置</h2>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">authClient</span> <span class="o">=</span> <span class="nx">createOAuthClient</span><span class="p">({</span>
    <span class="na">url</span><span class="p">:</span> <span class="dl">'</span><span class="s1">http://localhost:9000</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">clientId</span><span class="p">:</span> <span class="dl">'</span><span class="s1">public-client</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">scopes</span><span class="p">:</span> <span class="dl">'</span><span class="s1">openid profile</span><span class="dl">'</span><span class="p">,</span>
<span class="p">})</span>
</code></pre></div></div>

<p>在main.ts中初始化<code class="language-plaintext highlighter-rouge">authClient</code>，提供三个核心参数：</p>
<ol>
  <li>url: 认证服务的根路径url，authClient会通过该根路径调用认证服务的相关接口，比如会首先调用<code class="language-plaintext highlighter-rouge">http://localhost:9000/.well-known/openid-configuration</code>接口来获取认证服务的完整接口列表。</li>
  <li>clientId: 认证服务设置的clientId，便于演示方便，authserver中写死了几个client，其中<code class="language-plaintext highlighter-rouge">public-client</code>是用于对vue程序提供认证服务的client。</li>
  <li>scopes: 用户数据范围，这个范围列表也是在<code class="language-plaintext highlighter-rouge">authserver</code>中写死的。</li>
</ol>

<h2 id="四authserver端的核心配置">四、authserver端的核心配置</h2>

<p>authserver端使用了Spring的Authorization Server，该框架提供的完整的oauth2认证服务功能，不过是一个开发库，不是开箱即用的服务，需要自己编写服务集成这个库。文档只介绍比较关键的client的配置，其它完整配置可以看示例代码。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RegisteredClient</span> <span class="n">publicClient</span> <span class="o">=</span> <span class="nc">RegisteredClient</span><span class="o">.</span><span class="na">withId</span><span class="o">(</span><span class="no">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="na">clientId</span><span class="o">(</span><span class="s">"public-client"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">clientAuthenticationMethod</span><span class="o">(</span><span class="nc">ClientAuthenticationMethod</span><span class="o">.</span><span class="na">NONE</span><span class="o">)</span>
                <span class="o">.</span><span class="na">authorizationGrantType</span><span class="o">(</span><span class="nc">AuthorizationGrantType</span><span class="o">.</span><span class="na">AUTHORIZATION_CODE</span><span class="o">)</span>
                <span class="o">.</span><span class="na">scope</span><span class="o">(</span><span class="nc">OidcScopes</span><span class="o">.</span><span class="na">OPENID</span><span class="o">)</span>
                <span class="o">.</span><span class="na">scope</span><span class="o">(</span><span class="nc">OidcScopes</span><span class="o">.</span><span class="na">PROFILE</span><span class="o">)</span>
                <span class="o">.</span><span class="na">clientSettings</span><span class="o">(</span><span class="nc">ClientSettings</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
                        <span class="o">.</span><span class="na">requireAuthorizationConsent</span><span class="o">(</span><span class="kc">true</span><span class="o">)</span>
                        <span class="o">.</span><span class="na">requireProofKey</span><span class="o">(</span><span class="kc">true</span><span class="o">)</span>
                        <span class="o">.</span><span class="na">build</span><span class="o">()</span>
                <span class="o">)</span>
                <span class="o">.</span><span class="na">redirectUris</span><span class="o">((</span><span class="n">uris</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="n">uris</span><span class="o">.</span><span class="na">addAll</span><span class="o">(</span><span class="nc">Set</span><span class="o">.</span><span class="na">of</span><span class="o">(</span>
                        <span class="s">"http://127.0.0.1:4200"</span><span class="o">,</span>
                        <span class="s">"http://localhost:4200"</span><span class="o">,</span>
                        <span class="s">"http://localhost:5173"</span>
                        <span class="o">))</span>
                <span class="o">)</span>
                <span class="o">.</span><span class="na">postLogoutRedirectUris</span><span class="o">((</span><span class="n">uris</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="n">uris</span><span class="o">.</span><span class="na">addAll</span><span class="o">(</span><span class="nc">Set</span><span class="o">.</span><span class="na">of</span><span class="o">(</span>
                        <span class="s">"http://127.0.0.1:4200"</span><span class="o">,</span>
                        <span class="s">"http://localhost:4200"</span><span class="o">,</span>
                        <span class="s">"http://localhost:5173"</span>
                <span class="o">)))</span>
                <span class="o">.</span><span class="na">build</span><span class="o">();</span>
</code></pre></div></div>

<p>以上代码是注册了一个<code class="language-plaintext highlighter-rouge">client</code>，指定了<code class="language-plaintext highlighter-rouge">scope</code>，其中的<code class="language-plaintext highlighter-rouge">clientId: public-client</code>就是前面介绍vue认证配置代码中提到的clientId，这两个id必须一致，否则authserver会报非法client的错误。</p>

<p>同时还设置了登录成功重定向url，登出重定向url。这些参数都是oauth2认证流程的关键参数。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><category term="vue" /><summary type="html"><![CDATA[通过Spring Authorization Server对vue应用进行授权防护。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/spring-oauth-vue.png" /><media:content medium="image" url="https://springcamp.cn/assets/images/spring-oauth-vue.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">SpringSecurity高级用法</title><link href="https://springcamp.cn/spring-advanced-security/" rel="alternate" type="text/html" title="SpringSecurity高级用法" /><published>2025-01-27T07:30:00+00:00</published><updated>2025-01-27T07:30:00+00:00</updated><id>https://springcamp.cn/spring-advanced-security</id><content type="html" xml:base="https://springcamp.cn/spring-advanced-security/"><![CDATA[<p>SpringSecurity的高级用法，包括自定义loginUrl携带参数，自定义认证校验逻辑，自定义权限校验逻辑。</p>

<p><a href="https://github.com/qihaiyan/springcamp/tree/main/spring-advanced-security">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-advanced-security</a></p>

<p><a href="https://github.com/qihaiyan/ng-boot-oauth">完整独立示例工程可查看 https://github.com/qihaiyan/ng-boot-oauth，带有vue/react/angular三个前端登录实现 </a></p>

<p><img src="../assets/images/login.png" alt="login" /></p>

<h2 id="一概述">一、概述</h2>

<p>在项目实际开发过程中，SpringSecurity默认的认证和权限校验逻辑不能实现很高的业务复杂度，这种情况下我们需要自定义这些逻辑，包括自定义loginUrl携带参数，自定义认证校验逻辑，自定义权限校验逻辑。</p>

<h2 id="二自定义loginurl携带参数">二、自定义loginUrl携带参数</h2>

<p>SpringSecurity在跳转login页面时，虽然可以指定login的url，但是无法让url中携带动态参数，比如跳转到login?param=foo，其中foo需要根据特定条件动态变化，要实现这种效果，我们需要通过exceptionHandling指定自定义LoginUrlAuthenticationEntryPoint。</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">CustomLoginUrlAuthenticationEntryPoint</span> <span class="kd">extends</span> <span class="nc">LoginUrlAuthenticationEntryPoint</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">RedirectStrategy</span> <span class="n">redirectStrategy</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DefaultRedirectStrategy</span><span class="o">();</span>

    <span class="kd">public</span> <span class="nf">CustomLoginUrlAuthenticationEntryPoint</span><span class="o">(</span><span class="nc">String</span> <span class="n">loginFormUrl</span><span class="o">)</span> <span class="o">{</span>
        <span class="kd">super</span><span class="o">(</span><span class="n">loginFormUrl</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">commence</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span> <span class="nc">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span> <span class="nc">AuthenticationException</span> <span class="n">authException</span><span class="o">)</span>
            <span class="kd">throws</span> <span class="nc">IOException</span><span class="o">,</span> <span class="nc">ServletException</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(!</span><span class="kd">super</span><span class="o">.</span><span class="na">isUseForward</span><span class="o">())</span> <span class="o">{</span>
            <span class="nc">String</span> <span class="n">redirectUrl</span> <span class="o">=</span> <span class="k">this</span><span class="o">.</span><span class="na">buildRedirectUrlToLoginPage</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">authException</span><span class="o">);</span>
            <span class="c1">// change login url</span>
            <span class="n">redirectUrl</span> <span class="o">=</span> <span class="n">redirectUrl</span> <span class="o">+</span> <span class="s">"?param=test"</span><span class="o">;</span>
            <span class="k">this</span><span class="o">.</span><span class="na">redirectStrategy</span><span class="o">.</span><span class="na">sendRedirect</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">redirectUrl</span><span class="o">);</span>
        <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
            <span class="nc">String</span> <span class="n">redirectUrl</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
            <span class="k">if</span> <span class="o">(</span><span class="kd">super</span><span class="o">.</span><span class="na">isForceHttps</span><span class="o">()</span> <span class="o">&amp;&amp;</span> <span class="s">"http"</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getScheme</span><span class="o">()))</span> <span class="o">{</span>
                <span class="n">redirectUrl</span> <span class="o">=</span> <span class="k">this</span><span class="o">.</span><span class="na">buildHttpsRedirectUrlForRequest</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
            <span class="o">}</span>

            <span class="k">if</span> <span class="o">(</span><span class="n">redirectUrl</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                <span class="k">this</span><span class="o">.</span><span class="na">redirectStrategy</span><span class="o">.</span><span class="na">sendRedirect</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">redirectUrl</span><span class="o">);</span>
            <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
                <span class="nc">String</span> <span class="n">loginForm</span> <span class="o">=</span> <span class="k">this</span><span class="o">.</span><span class="na">determineUrlToUseForThisRequest</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">authException</span><span class="o">);</span>
                <span class="nc">RequestDispatcher</span> <span class="n">dispatcher</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getRequestDispatcher</span><span class="o">(</span><span class="n">loginForm</span><span class="o">);</span>
                <span class="n">dispatcher</span><span class="o">.</span><span class="na">forward</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="o">}</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>以上代码自定义CustomLoginUrlAuthenticationEntryPoint，在commence方法中我们可以按照业务需要实现自己的跳转逻辑，通过修改redirectUrl实现。</p>

<p>在SpringSecurity配置中通过exceptionHandling引用CustomLoginUrlAuthenticationEntryPoint：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">exceptionHandling</span><span class="o">(</span><span class="n">customizer</span> <span class="o">-&gt;</span>
                        <span class="n">customizer</span><span class="o">.</span><span class="na">authenticationEntryPoint</span><span class="o">(</span><span class="k">new</span> <span class="nc">CustomLoginUrlAuthenticationEntryPoint</span><span class="o">(</span><span class="s">"/login"</span><span class="o">)))</span>
</code></pre></div></div>

<h2 id="三自定义认证校验逻辑">三、自定义认证校验逻辑</h2>

<p>SpringSecurity默认的认证逻辑是校验用户名密码是否合法，如果想增加其它的校验逻辑，需要实现AuthenticationProvider，然后在authenticationManager中指定我们自己实现的AuthenticationProvider。</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">CustomAuthenticationProvider</span> <span class="kd">extends</span> <span class="nc">DaoAuthenticationProvider</span> <span class="o">{</span>
    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">CustomUserDetailsService</span> <span class="n">customUserDetailsService</span><span class="o">;</span>

    <span class="nd">@PostConstruct</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">init</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">setUserDetailsService</span><span class="o">(</span><span class="n">customUserDetailsService</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">additionalAuthenticationChecks</span><span class="o">(</span><span class="nc">UserDetails</span> <span class="n">userDetails</span><span class="o">,</span> <span class="nc">UsernamePasswordAuthenticationToken</span> <span class="n">authentication</span><span class="o">)</span> <span class="o">{</span>
        <span class="kd">super</span><span class="o">.</span><span class="na">additionalAuthenticationChecks</span><span class="o">(</span><span class="n">userDetails</span><span class="o">,</span> <span class="n">authentication</span><span class="o">);</span>
        <span class="nc">HttpServletRequest</span> <span class="n">req</span> <span class="o">=</span> <span class="o">((</span><span class="nc">ServletRequestAttributes</span><span class="o">)</span> <span class="nc">RequestContextHolder</span><span class="o">.</span><span class="na">getRequestAttributes</span><span class="o">()).</span><span class="na">getRequest</span><span class="o">();</span>
        <span class="nc">String</span> <span class="n">username</span> <span class="o">=</span> <span class="n">userDetails</span><span class="o">.</span><span class="na">getUsername</span><span class="o">();</span>

        <span class="c1">// 自定义认证校验逻辑</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">username</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="s">"need approval"</span><span class="o">))</span> <span class="o">{</span>
            <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"invalid request is: {}"</span><span class="o">,</span> <span class="n">req</span><span class="o">);</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">AuthenticationServiceException</span><span class="o">(</span><span class="s">"Your account is pending approval for access"</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>在上面自定义的CustomAuthenticationProvider中，通过重写additionalAuthenticationChecks方法进行自定义认证逻辑的实现。</p>

<p>然后在authenticationManager中指定CustomAuthenticationProvider：</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="nc">AuthenticationManager</span> <span class="nf">authenticationManager</span><span class="o">()</span> <span class="o">{</span>
    <span class="k">return</span> <span class="k">new</span> <span class="nf">ProviderManager</span><span class="o">(</span><span class="n">customAuthenticationProvider</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="四自定义权限校验逻辑">四、自定义权限校验逻辑</h2>

<p>SpringSecurity可以通过在配置中通过requestMatchers指定较为灵活的权限校验策略，但是缺少一些动态特性，比如对 /foo/{param} 这种rest风格的带变量的url就处理不了，
这种情况我们可以通过自定义AuthorizationManager来实现，然后在requestMatchers中通过access方法来指定我们自定义的AuthorizationManager。</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">MyRequestAuthorizationManager</span> <span class="kd">implements</span> <span class="nc">AuthorizationManager</span><span class="o">&lt;</span><span class="nc">RequestAuthorizationContext</span><span class="o">&gt;</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">SecurityExpressionHandler</span><span class="o">&lt;</span><span class="nc">RequestAuthorizationContext</span><span class="o">&gt;</span> <span class="n">expressionHandler</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DefaultHttpSecurityExpressionHandler</span><span class="o">();</span>

    <span class="kd">public</span> <span class="nf">MyRequestAuthorizationManager</span><span class="o">()</span> <span class="o">{</span>
    <span class="o">}</span>

    <span class="c1">// 自定义授权校验逻辑</span>
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">AuthorizationDecision</span> <span class="nf">check</span><span class="o">(</span><span class="nc">Supplier</span><span class="o">&lt;</span><span class="nc">Authentication</span><span class="o">&gt;</span> <span class="n">authentication</span><span class="o">,</span> <span class="nc">RequestAuthorizationContext</span> <span class="n">context</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">EvaluationContext</span> <span class="n">ctx</span> <span class="o">=</span> <span class="k">this</span><span class="o">.</span><span class="na">expressionHandler</span><span class="o">.</span><span class="na">createEvaluationContext</span><span class="o">(</span><span class="n">authentication</span><span class="o">,</span> <span class="n">context</span><span class="o">);</span>
        <span class="nc">String</span> <span class="n">checkParam</span> <span class="o">=</span> <span class="nc">Optional</span><span class="o">.</span><span class="na">ofNullable</span><span class="o">(</span><span class="n">ctx</span><span class="o">.</span><span class="na">lookupVariable</span><span class="o">(</span><span class="s">"param"</span><span class="o">)).</span><span class="na">map</span><span class="o">(</span><span class="nl">String:</span><span class="o">:</span><span class="n">valueOf</span><span class="o">).</span><span class="na">orElse</span><span class="o">(</span><span class="kc">null</span><span class="o">);</span>

        <span class="c1">// the '/public' url doesn't need authentication</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">checkParam</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="o">(</span><span class="n">checkParam</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="s">"public"</span><span class="o">)))</span> <span class="o">{</span>
            <span class="k">return</span> <span class="k">new</span> <span class="nf">AuthorizationDecision</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">AuthorizationDecision</span><span class="o">(!</span><span class="nc">ObjectUtils</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">(</span><span class="n">authentication</span><span class="o">.</span><span class="na">get</span><span class="o">().</span><span class="na">getCredentials</span><span class="o">()));</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>在MyRequestAuthorizationManager中通过重写check方法来实现自定义权限校验，rest风格的带变量的url中的变量，可以通过EvaluationContext的lookupVariable方法获取变量值。</p>

<p>指定自定义AuthorizationManager:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">MyRequestAuthorizationManager</span> <span class="n">myRequestAuthorizationManager</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MyRequestAuthorizationManager</span><span class="o">();</span>
<span class="n">http</span>
    <span class="o">.</span><span class="na">authorizeHttpRequests</span><span class="o">(</span><span class="n">authorize</span> <span class="o">-&gt;</span> <span class="n">authorize</span>
        <span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="s">"/{param}"</span><span class="o">).</span><span class="na">access</span><span class="o">(</span><span class="n">myRequestAuthorizationManager</span><span class="o">)</span>
    <span class="o">)</span>
</code></pre></div></div>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><summary type="html"><![CDATA[SpringSecurity的高级用法，包括自定义loginUrl携带参数，自定义认证校验逻辑，自定义权限校验逻辑。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/spring-security.png" /><media:content medium="image" url="https://springcamp.cn/assets/images/spring-security.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Spring统一修改RequestBody</title><link href="https://springcamp.cn/spring-controller-modify-request-body/" rel="alternate" type="text/html" title="Spring统一修改RequestBody" /><published>2024-07-06T07:30:00+00:00</published><updated>2024-07-06T07:30:00+00:00</updated><id>https://springcamp.cn/spring-controller-modify-request-body</id><content type="html" xml:base="https://springcamp.cn/spring-controller-modify-request-body/"><![CDATA[<p>我们编写RestController时，有可能多个接口使用了相同的RequestBody，在一些场景下需求修改传入的RequestBody的值，如果是每个controller中都去修改，代码会比较繁琐，最好的方式是在一个地方统一修改，比如将header中的某个值赋值给RequestBody对象的某个属性。 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-modify-request-body">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-modify-request-body</a></p>

<h2 id="一概述">一、概述</h2>

<p>在spring中可以使用RequestBodyAdviceAdapter修改RestController的请求参数。</p>

<h2 id="二自定义-requestbodyadviceadapter">二、自定义 RequestBodyAdviceAdapter</h2>

<p>以下代码为自定义 ModifyBodyAdvice 实现 RequestBodyAdviceAdapter</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@ControllerAdvice</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ModifyBodyAdvice</span> <span class="kd">extends</span> <span class="nc">RequestBodyAdviceAdapter</span> <span class="o">{</span>
    <span class="nd">@Autowired</span>
    <span class="nc">HttpServletRequest</span> <span class="n">httpServletRequest</span><span class="o">;</span>

    <span class="nd">@Override</span>
    <span class="nd">@NonNull</span>
    <span class="kd">public</span> <span class="nc">Object</span> <span class="nf">afterBodyRead</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">Object</span> <span class="n">body</span><span class="o">,</span> <span class="nd">@NonNull</span> <span class="nc">HttpInputMessage</span> <span class="n">inputMessage</span><span class="o">,</span>
                                <span class="nd">@NonNull</span> <span class="nc">MethodParameter</span> <span class="n">parameter</span><span class="o">,</span> <span class="nd">@NonNull</span> <span class="nc">Type</span> <span class="n">targetType</span><span class="o">,</span>
                                <span class="nd">@NonNull</span> <span class="nc">Class</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">HttpMessageConverter</span><span class="o">&lt;?&gt;&gt;</span> <span class="n">converterType</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">requestMethod</span> <span class="o">=</span> <span class="n">httpServletRequest</span><span class="o">.</span><span class="na">getMethod</span><span class="o">();</span>
        <span class="nc">String</span> <span class="n">fieldName</span> <span class="o">=</span> <span class="s">"foo"</span><span class="o">;</span>

        <span class="k">if</span> <span class="o">(</span><span class="nc">StringUtils</span><span class="o">.</span><span class="na">startsWithIgnoreCase</span><span class="o">(</span><span class="n">requestMethod</span><span class="o">,</span> <span class="nc">HttpMethod</span><span class="o">.</span><span class="na">PUT</span><span class="o">.</span><span class="na">name</span><span class="o">())</span>
                <span class="o">||</span> <span class="nc">StringUtils</span><span class="o">.</span><span class="na">startsWithIgnoreCase</span><span class="o">(</span><span class="n">requestMethod</span><span class="o">,</span> <span class="nc">HttpMethod</span><span class="o">.</span><span class="na">POST</span><span class="o">.</span><span class="na">name</span><span class="o">())</span>
        <span class="o">)</span> <span class="o">{</span>
            <span class="nc">Field</span> <span class="n">field</span> <span class="o">=</span> <span class="nc">ReflectionUtils</span><span class="o">.</span><span class="na">findField</span><span class="o">(</span><span class="n">body</span><span class="o">.</span><span class="na">getClass</span><span class="o">(),</span> <span class="n">fieldName</span><span class="o">);</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">field</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                <span class="nc">ReflectionUtils</span><span class="o">.</span><span class="na">makeAccessible</span><span class="o">(</span><span class="n">field</span><span class="o">);</span>
                <span class="nc">String</span> <span class="n">paramValue</span> <span class="o">=</span> <span class="nc">Optional</span><span class="o">.</span><span class="na">ofNullable</span><span class="o">(</span><span class="n">httpServletRequest</span><span class="o">.</span><span class="na">getHeader</span><span class="o">(</span><span class="n">fieldName</span><span class="o">)).</span><span class="na">orElse</span><span class="o">(</span><span class="s">""</span><span class="o">);</span>
                <span class="nc">Method</span> <span class="n">method</span> <span class="o">=</span> <span class="nc">ReflectionUtils</span><span class="o">.</span><span class="na">findMethod</span><span class="o">(</span><span class="n">body</span><span class="o">.</span><span class="na">getClass</span><span class="o">(),</span> <span class="s">"set"</span> <span class="o">+</span>
                        <span class="nc">StringUtils</span><span class="o">.</span><span class="na">capitalize</span><span class="o">(</span><span class="n">fieldName</span><span class="o">),</span> <span class="n">field</span><span class="o">.</span><span class="na">getType</span><span class="o">());</span>
                <span class="k">if</span> <span class="o">(</span><span class="n">method</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                    <span class="nc">ReflectionUtils</span><span class="o">.</span><span class="na">invokeMethod</span><span class="o">(</span><span class="n">method</span><span class="o">,</span> <span class="n">body</span><span class="o">,</span> <span class="n">paramValue</span><span class="o">);</span>
                <span class="o">}</span>
            <span class="o">}</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="kd">super</span><span class="o">.</span><span class="na">afterBodyRead</span><span class="o">(</span><span class="n">body</span><span class="o">,</span> <span class="n">inputMessage</span><span class="o">,</span> <span class="n">parameter</span><span class="o">,</span> <span class="n">targetType</span><span class="o">,</span> <span class="n">converterType</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">supports</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">MethodParameter</span> <span class="n">methodParameter</span><span class="o">,</span>
                            <span class="nd">@NonNull</span> <span class="nc">Type</span> <span class="n">targetType</span><span class="o">,</span>
                            <span class="nd">@NonNull</span> <span class="nc">Class</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">HttpMessageConverter</span><span class="o">&lt;?&gt;&gt;</span> <span class="n">converterType</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>便于演示处理过程，我们在代码中写死了要修改的请求对象的属性为 foo ，从请求header中获取foo这个header的值，然后通过反射赋值到请求对象的foo属性。</p>

<h2 id="三验证统一修改逻辑">三、验证统一修改逻辑</h2>

<p>我们通过编写单元测试的方式验证RequestBody的值是否能够正常修改。
在DemoApplicationTest这个单元测试程序中进行接口调用，并验证返回结果。</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">test</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">ReqBody</span> <span class="n">reqBody</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ReqBody</span><span class="o">();</span>
        <span class="nc">ResponseEntity</span><span class="o">&lt;</span><span class="nc">ReqBody</span><span class="o">&gt;</span> <span class="n">resp</span> <span class="o">=</span> <span class="n">testRestTemplate</span><span class="o">.</span><span class="na">exchange</span><span class="o">(</span><span class="nc">RequestEntity</span><span class="o">.</span><span class="na">post</span><span class="o">(</span><span class="s">"/test"</span><span class="o">).</span><span class="na">header</span><span class="o">(</span><span class="s">"foo"</span><span class="o">,</span> <span class="s">"test"</span><span class="o">).</span><span class="na">body</span><span class="o">(</span><span class="n">reqBody</span><span class="o">),</span> <span class="nc">ReqBody</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"result : {}"</span><span class="o">,</span> <span class="n">resp</span><span class="o">);</span>
        <span class="n">assertThat</span><span class="o">(</span><span class="n">resp</span><span class="o">.</span><span class="na">getBody</span><span class="o">().</span><span class="na">getFoo</span><span class="o">(),</span> <span class="n">is</span><span class="o">(</span><span class="s">"test"</span><span class="o">));</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>我们调用controller时传入了的RequestBody为 ReqBody的一个对象，这个对象没有对属性进行赋值，在请求header中发送了foo这个header，按照处理逻辑，controller中接收到的ReqBody对象的foo的值应该是header的值。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><summary type="html"><![CDATA[我们编写RestController时，有可能多个接口使用了相同的RequestBody，在一些场景下需求修改传入的RequestBody的值，如果是每个controller中都去修改，代码会比较繁琐，最好的方式是在一个地方统一修改，比如将header中的某个值赋值给RequestBody对象的某个属性。 示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-modify-request-body]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/rest-controller-advice.png" /><media:content medium="image" url="https://springcamp.cn/assets/images/rest-controller-advice.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Spring RestTemplate 怎么设置 DNS 解析超时</title><link href="https://springcamp.cn/spring-resttemplate-dns-timeout/" rel="alternate" type="text/html" title="Spring RestTemplate 怎么设置 DNS 解析超时" /><published>2024-06-16T07:30:00+00:00</published><updated>2024-06-16T07:30:00+00:00</updated><id>https://springcamp.cn/spring-resttemplate-dns-timeout</id><content type="html" xml:base="https://springcamp.cn/spring-resttemplate-dns-timeout/"><![CDATA[<p>RestTemplate 常用的超时设置方法可以设置连接超时、接口请求超时、接口响应超时，但是对于DNS解析超时往往没有简单的方法可以设置。本文介绍设置DNS解析超时时间的方法，具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-rest-template-log">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-rest-template-log</a></p>

<blockquote class="en-abstract">
RestTemplate's built-in timeout settings cover connect, request and response timeouts, but offer no direct way to set a DNS resolution timeout. This post shows how to add one by implementing a custom <code>DnsResolver</code> on top of Apache HttpClient, with a runnable example at <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-rest-template-log">springcamp/spring-rest-template-log</a>.
</blockquote>

<h2 id="一概述">一、概述</h2>

<p>在spring中使用RestTemplate调用远程接口时往往需要设置超时时间，否则当对方接口过慢时，很容易把自己系统堵死。连接超时、发送请求超时、接收响应超时都有直接的方法可以进行调用设置，设置DNS解析超时需要通过相对复杂的方法进行处理。</p>

<h2 id="二自定义-customdnsresolver">二、自定义 CustomDnsResolver</h2>

<p>首先RestTemplate配置使用apache httpclient进行http接口调用，apache httpclient内部通过默认的DnsResolver进行DNS解析，我们可以通过自己实现DnsResolver方法来设置DNS解析超时时间。</p>

<p>以下代码为自定义 CustomDnsResolver 实现 DnsResolver。</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="kd">class</span> <span class="nc">CustomDnsResolver</span> <span class="kd">implements</span> <span class="nc">DnsResolver</span> <span class="o">{</span>

        <span class="kd">private</span> <span class="kd">final</span> <span class="nc">DnsResolver</span> <span class="n">systemDnsResolver</span><span class="o">;</span>
        <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Integer</span> <span class="n">connectTimeout</span><span class="o">;</span>

        <span class="kd">public</span> <span class="nf">CustomDnsResolver</span><span class="o">(</span><span class="nc">Integer</span> <span class="n">connectTimeout</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">this</span><span class="o">.</span><span class="na">systemDnsResolver</span> <span class="o">=</span> <span class="nc">SystemDefaultDnsResolver</span><span class="o">.</span><span class="na">INSTANCE</span><span class="o">;</span>
            <span class="k">this</span><span class="o">.</span><span class="na">connectTimeout</span> <span class="o">=</span> <span class="n">connectTimeout</span><span class="o">;</span>
        <span class="o">}</span>

        <span class="nd">@Override</span>
        <span class="kd">public</span> <span class="nc">InetAddress</span><span class="o">[]</span> <span class="nf">resolve</span><span class="o">(</span><span class="kd">final</span> <span class="nc">String</span> <span class="n">host</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="nc">CompletableFuture</span><span class="o">.</span><span class="na">supplyAsync</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="o">{</span>
                    <span class="k">try</span> <span class="o">{</span>
                        <span class="k">return</span> <span class="n">systemDnsResolver</span><span class="o">.</span><span class="na">resolve</span><span class="o">(</span><span class="n">host</span><span class="o">);</span>
                    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">UnknownHostException</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="na">get</span><span class="o">(</span><span class="n">connectTimeout</span><span class="o">,</span> <span class="nc">TimeUnit</span><span class="o">.</span><span class="na">SECONDS</span><span class="o">);</span>
            <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">InterruptedException</span> <span class="o">|</span> <span class="nc">ExecutionException</span> <span class="o">|</span> <span class="nc">TimeoutException</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="nd">@Override</span>
        <span class="kd">public</span> <span class="nc">String</span> <span class="nf">resolveCanonicalHostname</span><span class="o">(</span><span class="nc">String</span> <span class="n">host</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">UnknownHostException</span> <span class="o">{</span>
            <span class="k">return</span> <span class="n">systemDnsResolver</span><span class="o">.</span><span class="na">resolveCanonicalHostname</span><span class="o">(</span><span class="n">host</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>DNS解析的方法为resolve，我们resolve方法中通过CompletableFuture.supplyAsync调用系统的DNS解析方法，然后通过CompletableFuture.get方法进行超时控制。</p>

<h2 id="三配置apache-httpclient使用自定义的customdnsresolver">三、配置apache httpclient使用自定义的CustomDnsResolver</h2>

<p>PoolingHttpClientConnectionManager常用的构造方法只有一个Registry参数，该方法无法指定自定义 DnsResolver ，所以我们需要改用支持指定DnsResolver的构造方法。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nc">PoolingHttpClientConnectionManager</span> <span class="n">poolingConnectionManager</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">PoolingHttpClientConnectionManager</span><span class="o">(</span>
                <span class="n">registry</span><span class="o">,</span>
                <span class="nc">PoolConcurrencyPolicy</span><span class="o">.</span><span class="na">STRICT</span><span class="o">,</span>
                <span class="nc">PoolReusePolicy</span><span class="o">.</span><span class="na">LIFO</span><span class="o">,</span>
                <span class="nc">TimeValue</span><span class="o">.</span><span class="na">NEG_ONE_MILLISECOND</span><span class="o">,</span>
                <span class="kc">null</span><span class="o">,</span>
                <span class="k">new</span> <span class="nf">CustomDnsResolver</span><span class="o">(</span><span class="mi">2</span><span class="o">),</span>
                <span class="kc">null</span><span class="o">);</span>
</code></pre></div></div>

<p>构造方法的第6个参数就是DnsResolver，我们在初始化DnsResolver类时指定了超时时间为2，单位为秒。更灵活的方式应该是将该超时时间参数放到配置文件中。</p>

<p>通过自定义的CustomDnsResolver类，同时在PoolingHttpClientConnectionManager构造方法中传入CustomDnsResolver对象，就能够对DNS解析超时进行控制。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><summary type="html"><![CDATA[RestTemplate 常用的超时设置方法可以设置连接超时、接口请求超时、接口响应超时，但是对于DNS解析超时往往没有简单的方法可以设置。本文介绍设置DNS解析超时时间的方法，具体的代码参照 示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-rest-template-log]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/dns-timeout.jpg" /><media:content medium="image" url="https://springcamp.cn/assets/images/dns-timeout.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">spring动态控制定时任务</title><link href="https://springcamp.cn/spring-dynamic-scheduler/" rel="alternate" type="text/html" title="spring动态控制定时任务" /><published>2024-01-07T07:30:00+00:00</published><updated>2024-01-07T07:30:00+00:00</updated><id>https://springcamp.cn/spring-dynamic-scheduler</id><content type="html" xml:base="https://springcamp.cn/spring-dynamic-scheduler/"><![CDATA[<p>在spring框架中，对于简单的定时任务，可以使用 @Scheduled 注解实现，在实际项目中，经常需要动态的控制定时任务，比如通过接口增加、启动、停止、删除定时任务，动态的改变定时任务的执行时间等。</p>

<p>我们可以通过编码的方式动态控制定时任务，具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-dynamic-scheduler">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-dynamic-scheduler</a></p>

<h2 id="一概述">一、概述</h2>

<p>在spring框架可以通过 CronTask 和 TaskScheduler 动态控制定时任务，实现定时任务的动态更新，比如修改定时任务的执行时间，这个是 @Scheduled 无法实现的。采用编码控制动态任务的方式，我们还可以把动态任务执行信息保存到数据库中，通过数据库里的任务配置数据来动态控制定时任务，也可以通过接口来动态控制定时任务。</p>

<h2 id="二配置定时任务">二、配置定时任务</h2>

<p>首先，同 @Scheduled 注解的方式一样，动态控制定时任务也需要使用 @EnableScheduling 注解来开启定时任务功能：</p>

<p>然后通过实现 SchedulingConfigurer 接口来对动态任务进行配置：</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">MyScheduler</span> <span class="kd">implements</span> <span class="nc">SchedulingConfigurer</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="nc">ScheduledTaskRegistrar</span> <span class="n">taskRegistrar</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">ConcurrentHashMap</span><span class="o">&lt;</span><span class="nc">Long</span><span class="o">,</span> <span class="nc">ScheduledFuture</span><span class="o">&lt;?&gt;&gt;</span> <span class="n">scheduledFutures</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ConcurrentHashMap</span><span class="o">&lt;&gt;();</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">configureTasks</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">ScheduledTaskRegistrar</span> <span class="n">taskRegistrar</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">ThreadPoolTaskScheduler</span> <span class="n">threadPoolTaskScheduler</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ThreadPoolTaskScheduler</span><span class="o">();</span>
        <span class="n">threadPoolTaskScheduler</span><span class="o">.</span><span class="na">setPoolSize</span><span class="o">(</span><span class="mi">10</span><span class="o">);</span><span class="c1">// Set the pool of threads</span>
        <span class="n">threadPoolTaskScheduler</span><span class="o">.</span><span class="na">setThreadNamePrefix</span><span class="o">(</span><span class="s">"sys-scheduler"</span><span class="o">);</span>
        <span class="n">threadPoolTaskScheduler</span><span class="o">.</span><span class="na">initialize</span><span class="o">();</span>
        <span class="k">this</span><span class="o">.</span><span class="na">taskRegistrar</span> <span class="o">=</span> <span class="n">taskRegistrar</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">taskRegistrar</span><span class="o">.</span><span class="na">setTaskScheduler</span><span class="o">(</span><span class="n">threadPoolTaskScheduler</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@PreDestroy</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">destroy</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">taskRegistrar</span><span class="o">.</span><span class="na">destroy</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>通过上面的代码，我们就启用了动态任务的基本能力，为动态任务指定了执行线程池。</p>

<h2 id="三动态更新定时任务">三、动态更新定时任务</h2>

<p>更新定时任务通过 CronTask 和 TaskScheduler 来实现，我们新增一个注册定时任务的方法：</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">registerTask</span><span class="o">(</span><span class="nc">TaskData</span> <span class="n">taskData</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">//如果配置一致，则不需要重新创建定时任务</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">scheduledFutures</span><span class="o">.</span><span class="na">containsKey</span><span class="o">(</span><span class="n">taskData</span><span class="o">.</span><span class="na">getId</span><span class="o">())</span>
                <span class="o">&amp;&amp;</span> <span class="n">cronTasks</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">taskData</span><span class="o">.</span><span class="na">getId</span><span class="o">()).</span><span class="na">getExpression</span><span class="o">().</span><span class="na">equals</span><span class="o">(</span><span class="n">taskData</span><span class="o">.</span><span class="na">getExpression</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">//如果策略执行时间发生了变化，则取消当前策略的任务</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">scheduledFutures</span><span class="o">.</span><span class="na">containsKey</span><span class="o">(</span><span class="n">taskData</span><span class="o">.</span><span class="na">getId</span><span class="o">()))</span> <span class="o">{</span>
            <span class="n">scheduledFutures</span><span class="o">.</span><span class="na">remove</span><span class="o">(</span><span class="n">taskData</span><span class="o">.</span><span class="na">getId</span><span class="o">()).</span><span class="na">cancel</span><span class="o">(</span><span class="kc">false</span><span class="o">);</span>
            <span class="n">cronTasks</span><span class="o">.</span><span class="na">remove</span><span class="o">(</span><span class="n">taskData</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
        <span class="o">}</span>

        <span class="nc">CronTask</span> <span class="n">task</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">CronTask</span><span class="o">(</span><span class="n">taskData</span><span class="o">,</span> <span class="n">taskData</span><span class="o">.</span><span class="na">getExpression</span><span class="o">());</span>
        <span class="nc">TaskScheduler</span> <span class="n">scheduler</span> <span class="o">=</span> <span class="n">taskRegistrar</span><span class="o">.</span><span class="na">getScheduler</span><span class="o">();</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">scheduler</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">ScheduledFuture</span><span class="o">&lt;?&gt;</span> <span class="n">future</span> <span class="o">=</span> <span class="n">scheduler</span><span class="o">.</span><span class="na">schedule</span><span class="o">(</span><span class="n">task</span><span class="o">.</span><span class="na">getRunnable</span><span class="o">(),</span> <span class="n">task</span><span class="o">.</span><span class="na">getTrigger</span><span class="o">());</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">future</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">scheduledFutures</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="n">taskData</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="n">future</span><span class="o">);</span>
            <span class="o">}</span>
        <span class="o">}</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>我们新增了一个 registerTask 方法用于注册定时任务，入参中 TaskData 是定时任务的配置数据，为了简单，我们把配置数据和执行代码放到了一起：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Slf4j</span>
<span class="nd">@Data</span>
<span class="nd">@Entity</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">TaskData</span> <span class="kd">implements</span> <span class="nc">Runnable</span> <span class="o">{</span>
    <span class="nd">@Id</span>
    <span class="nd">@GeneratedValue</span><span class="o">(</span><span class="n">strategy</span> <span class="o">=</span> <span class="nc">GenerationType</span><span class="o">.</span><span class="na">IDENTITY</span><span class="o">)</span>
    <span class="kd">private</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">name</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">expression</span><span class="o">;</span>

    <span class="nd">@Transient</span>
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">run</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"{} is running with expression {}"</span><span class="o">,</span> <span class="k">this</span><span class="o">.</span><span class="na">getName</span><span class="o">(),</span> <span class="k">this</span><span class="o">.</span><span class="na">getExpression</span><span class="o">());</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>核心代码是创建一个 CronTask 对象，该对象包含两个参数：Runnable 方法和 cron 表达式。
CronTask 对象创建好后，通过 ScheduledTaskRegistrar 对定时任务进行注册，注册完成后，定时任务就会在cron表达式指定的时间点开始执行了。
执行的代码就是 Runnable 参数指定的方法。</p>

<h2 id="四动态停止定时任务">四、动态停止定时任务</h2>

<p>为了能够动态停止定时任务，我们在注册定时任务时，把注册结果放到了一个Map中：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">final</span> <span class="nc">ConcurrentHashMap</span><span class="o">&lt;</span><span class="nc">Long</span><span class="o">,</span> <span class="nc">ScheduledFuture</span><span class="o">&lt;?&gt;&gt;</span> <span class="n">scheduledFutures</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ConcurrentHashMap</span><span class="o">&lt;&gt;();</span>

<span class="nc">ScheduledFuture</span><span class="o">&lt;?&gt;</span> <span class="n">future</span> <span class="o">=</span> <span class="n">scheduler</span><span class="o">.</span><span class="na">schedule</span><span class="o">(</span><span class="n">task</span><span class="o">.</span><span class="na">getRunnable</span><span class="o">(),</span> <span class="n">task</span><span class="o">.</span><span class="na">getTrigger</span><span class="o">());</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">future</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">scheduledFutures</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="n">taskData</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="n">future</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="kt">void</span> <span class="nf">stop</span><span class="o">(</span><span class="nc">Long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">scheduledFutures</span><span class="o">.</span><span class="na">containsKey</span><span class="o">(</span><span class="n">id</span><span class="o">))</span> <span class="o">{</span>
            <span class="n">scheduledFutures</span><span class="o">.</span><span class="na">remove</span><span class="o">(</span><span class="n">id</span><span class="o">).</span><span class="na">cancel</span><span class="o">(</span><span class="kc">false</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>该方法需要传入定时任务的id，由于我们把定时任务信息保存到了 scheduledFutures 这个Map中，所以可以根据任务id参数查找到对应的定时任务信息，然后调用对应的 <code class="language-plaintext highlighter-rouge">cancel</code> 方法来停止定时任务。</p>

<h2 id="五通过接口控制定时任务">五、通过接口控制定时任务</h2>

<p>通过上面的步骤我们已经具备了动态控制定时任务的基本能力，下面增加接口来控制定时任务：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@EnableScheduling</span>
<span class="nd">@SpringBootApplication</span>
<span class="nd">@RestController</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">DemoApplication</span> <span class="o">{</span>
    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">MyScheduler</span> <span class="n">myScheduler</span><span class="o">;</span>
    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">TaskDataRepository</span> <span class="n">taskDataRepository</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="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">SpringApplication</span><span class="o">.</span><span class="na">run</span><span class="o">(</span><span class="nc">DemoApplication</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="nd">@RequestMapping</span><span class="o">(</span><span class="s">"/register"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">TaskData</span> <span class="nf">register</span><span class="o">(</span>
            <span class="nc">String</span> <span class="n">name</span><span class="o">,</span>
            <span class="nd">@RequestParam</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"expression"</span><span class="o">,</span> <span class="n">required</span> <span class="o">=</span> <span class="kc">false</span><span class="o">,</span> <span class="n">defaultValue</span> <span class="o">=</span> <span class="s">"0/1 * * * * ?"</span><span class="o">)</span> <span class="nc">String</span> <span class="n">expression</span>
    <span class="o">)</span> <span class="o">{</span>
        <span class="nc">TaskData</span> <span class="n">taskData</span> <span class="o">=</span> <span class="n">taskDataRepository</span><span class="o">.</span><span class="na">findOneByName</span><span class="o">(</span><span class="n">name</span><span class="o">).</span><span class="na">orElse</span><span class="o">(</span><span class="k">new</span> <span class="nc">TaskData</span><span class="o">());</span>
        <span class="n">taskData</span><span class="o">.</span><span class="na">setName</span><span class="o">(</span><span class="n">name</span><span class="o">);</span>
        <span class="n">taskData</span><span class="o">.</span><span class="na">setExpression</span><span class="o">(</span><span class="n">expression</span><span class="o">);</span>
        <span class="n">taskData</span> <span class="o">=</span> <span class="n">taskDataRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">taskData</span><span class="o">);</span>
        <span class="n">myScheduler</span><span class="o">.</span><span class="na">registerTask</span><span class="o">(</span><span class="n">taskData</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">taskData</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">"/stop"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">stop</span><span class="o">(</span><span class="nc">Long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">taskDataRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">id</span><span class="o">).</span><span class="na">ifPresent</span><span class="o">(</span><span class="n">taskData</span> <span class="o">-&gt;</span> <span class="o">{</span>
            <span class="n">myScheduler</span><span class="o">.</span><span class="na">stop</span><span class="o">(</span><span class="n">id</span><span class="o">);</span>
        <span class="o">});</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>我们提供了 register 和 stop 两个接口，这两个接口会在改变动态任务执行数据时，先将数据保存到数据库中，对定时任务进行持久化，避免程序重启后定时任务都丢失。</p>

<p>程序启动后，我们首先调用 register 接口新增一个定时任务：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://localhost:8080/register?name<span class="o">=</span><span class="nb">test</span>
</code></pre></div></div>

<p>接口调用后，在日志中可以看到定时任务开始执行了，register 接口也可以通过 expression 参数更新定时任务的执行时间：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2024-01-07T18:02:09.003+08:00  INFO 23012 <span class="nt">---</span> <span class="o">[</span> sys-scheduler5] c.s.springdynamicscheduler.TaskData      : <span class="nb">test </span>is running with expression 0/1 <span class="k">*</span> <span class="k">*</span> <span class="k">*</span> <span class="k">*</span> ?
2024-01-07T18:02:10.005+08:00  INFO 23012 <span class="nt">---</span> <span class="o">[</span> sys-scheduler3] c.s.springdynamicscheduler.TaskData      : <span class="nb">test </span>is running with expression 0/1 <span class="k">*</span> <span class="k">*</span> <span class="k">*</span> <span class="k">*</span> ?
2024-01-07T18:02:11.012+08:00  INFO 23012 <span class="nt">---</span> <span class="o">[</span> sys-scheduler3] c.s.springdynamicscheduler.TaskData      : <span class="nb">test </span>is running with expression 0/1 <span class="k">*</span> <span class="k">*</span> <span class="k">*</span> <span class="k">*</span> ?
</code></pre></div></div>

<p>再调用 stop 接口，通过日志可以发现定时任务停止了执行：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://localhost:8080/stop?id<span class="o">=</span>1
</code></pre></div></div>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><summary type="html"><![CDATA[在spring框架中，对于简单的定时任务，可以使用 @Scheduled 注解实现，在实际项目中，经常需要动态的控制定时任务，比如通过接口增加、启动、停止、删除定时任务，动态的改变定时任务的执行时间等。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/scheduler.jpg" /><media:content medium="image" url="https://springcamp.cn/assets/images/scheduler.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Spring Data 灵活查询的三种方式</title><link href="https://springcamp.cn/spring-data-flex-query/" rel="alternate" type="text/html" title="Spring Data 灵活查询的三种方式" /><published>2023-12-27T08:20:00+00:00</published><updated>2023-12-27T08:20:00+00:00</updated><id>https://springcamp.cn/spring-data-flex-query</id><content type="html" xml:base="https://springcamp.cn/spring-data-flex-query/"><![CDATA[<p>在页面中展示列表数据时，通常需要根据用户输入的不同的查询条件返回不同的查询结果，传统的方式往往采用手动编写原始sql拼接where条件的方式，这种方式并不安全，容易存在sql注入漏洞。</p>

<p>本文介绍用SpringDataJpa实现灵活查询的方式，具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-data-flex-query">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-data-flex-query</a></p>

<h2 id="一概述">一、概述</h2>

<p>SpringDataJpa提供了三种灵活查询的方式，分别是：1、通过@Query注解编写查询语句；2、Example查询；3、Specification查询。下面分别介绍这三种方式的使用方法。</p>

<h2 id="二通过query注解编写查询语句">二、通过@Query注解编写查询语句</h2>

<p>这种方式使用比较简单，在 Repository 方法中编写查询语句。</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">MyDataRepository</span> <span class="kd">extends</span> <span class="n">JpaRepository</span><span class="o">&lt;</span><span class="n">MyData</span><span class="o">,</span> <span class="n">Long</span><span class="o">&gt;,</span> <span class="n">JpaSpecificationExecutor</span><span class="o">&lt;</span><span class="n">MyData</span><span class="o">&gt;</span> <span class="o">{</span>
    <span class="nd">@Query</span><span class="o">(</span><span class="s2">"select U from MyData U where (?1 is null or U.id=?1) and (?2 is null or U.name=?2)"</span><span class="o">)</span>
    <span class="n">List</span><span class="o">&lt;</span><span class="n">MyData</span><span class="o">&gt;</span> <span class="n">findByQuery</span><span class="o">(</span><span class="n">Long</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>
</code></pre></div></div>

<p>为了能够按照不同的查询条件进行查询，需要在查询语句中对查询参数进行判空，将所有的查询参数组合成and查询条件。当某个查询参数为空时，查询路径会被is null覆盖，该查询参数不会对数据进行过滤。</p>

<h2 id="三example查询">三、Example查询</h2>

<p>Example查询是通过一个数据对象按照约定的规则进行查询，只有对象中的非空字段或加入过滤条件，为空的字段不会进行过滤。</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">MyService</span> <span class="o">{</span>
    <span class="nc">MyData</span> <span class="n">example</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MyData</span><span class="o">();</span>
        <span class="n">example</span><span class="o">.</span><span class="na">setName</span><span class="o">(</span><span class="s">"two"</span><span class="o">);</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"find by example: {}"</span><span class="o">,</span> <span class="n">myDataRepository</span><span class="o">.</span><span class="na">findAll</span><span class="o">(</span><span class="nc">Example</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">example</span><span class="o">)));</span>
<span class="o">}</span>
</code></pre></div></div>

<p>在上述代码中，由于我们只对example对象的name字段赋值，因此只会按照name条件进行过滤。</p>

<p>更加详细的使用方式，可以参照[官方文档(https://docs.spring.io/spring-data/jpa/reference/repositories/query-by-example.html)]</p>

<h2 id="四specification查询">四、Specification查询</h2>

<p>相比于前两种方式，Specification查询要灵活的多，可以任意组合查询条件，实现我们想要的查询结果。</p>

<p>首先 Repository 要扩展 JpaSpecificationExecutor ：</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">MyDataRepository</span> <span class="kd">extends</span> <span class="nc">JpaRepository</span><span class="o">&lt;</span><span class="nc">MyData</span><span class="o">,</span> <span class="nc">Long</span><span class="o">&gt;,</span> <span class="nc">JpaSpecificationExecutor</span><span class="o">&lt;</span><span class="nc">MyData</span><span class="o">&gt;</span> <span class="o">{</span>
<span class="o">}</span>
</code></pre></div></div>

<p>用Specification编写一个查询方法：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">MyData</span><span class="o">&gt;</span> <span class="nf">findBySpec</span><span class="o">(</span><span class="nc">Long</span> <span class="n">id</span><span class="o">,</span> <span class="nc">String</span> <span class="n">name</span><span class="o">,</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Long</span><span class="o">&gt;</span> <span class="n">ids</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">myDataRepository</span><span class="o">.</span><span class="na">findAll</span><span class="o">((</span><span class="n">root</span><span class="o">,</span> <span class="n">query</span><span class="o">,</span> <span class="n">builder</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="o">{</span>
            <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Predicate</span><span class="o">&gt;</span> <span class="n">predicates</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;();</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">id</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">predicates</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">builder</span><span class="o">.</span><span class="na">equal</span><span class="o">(</span><span class="n">root</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="s">"id"</span><span class="o">),</span> <span class="n">id</span><span class="o">));</span>
            <span class="o">}</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">name</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">predicates</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">builder</span><span class="o">.</span><span class="na">like</span><span class="o">(</span><span class="n">root</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="s">"name"</span><span class="o">),</span> <span class="n">name</span><span class="o">));</span>
            <span class="o">}</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">ids</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">predicates</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">root</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="s">"id"</span><span class="o">).</span><span class="na">in</span><span class="o">(</span><span class="n">ids</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">and</span><span class="o">(</span><span class="n">predicates</span><span class="o">.</span><span class="na">toArray</span><span class="o">(</span><span class="k">new</span> <span class="nc">Predicate</span><span class="o">[</span><span class="mi">0</span><span class="o">]));</span>
        <span class="o">},</span> <span class="nc">Sort</span><span class="o">.</span><span class="na">by</span><span class="o">(</span><span class="s">"id"</span><span class="o">).</span><span class="na">descending</span><span class="o">());</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>在查询方法中，我们分别通过 builder.equal、builder.like、 root.get(“id”).in 实现了 <code class="language-plaintext highlighter-rouge">=</code> <code class="language-plaintext highlighter-rouge">like</code> <code class="language-plaintext highlighter-rouge">in</code> 三个sql子句，除此之外，还有 <code class="language-plaintext highlighter-rouge">greaterThan</code> <code class="language-plaintext highlighter-rouge">lessThan</code> 等其它很多方法可以使用。</p>

<p>最后一个参数还可以指定分页和排序条件。</p>

<p>使用查询方法进行灵活查询：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"find by spec with id: {}"</span><span class="o">,</span> <span class="n">findBySpec</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="kc">null</span><span class="o">));</span>
<span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"find by spec with name: {}"</span><span class="o">,</span> <span class="n">findBySpec</span><span class="o">(</span><span class="kc">null</span><span class="o">,</span> <span class="s">"%wo%"</span><span class="o">,</span> <span class="kc">null</span><span class="o">));</span>
<span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"find by spec with id list: {}"</span><span class="o">,</span> <span class="n">findBySpec</span><span class="o">(</span><span class="kc">null</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="mi">2L</span><span class="o">)));</span>
</code></pre></div></div>

<p>执行程序后，我们可以在日志中看到，不同的查询条件返回了不同的查询结果。</p>

<p>完整的实例代码：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Slf4j</span>
<span class="nd">@SpringBootApplication</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Application</span> <span class="kd">implements</span> <span class="nc">CommandLineRunner</span> <span class="o">{</span>

    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">MyDataRepository</span> <span class="n">myDataRepository</span><span class="o">;</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">run</span><span class="o">(</span><span class="nc">String</span><span class="o">...</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">MyData</span> <span class="n">myData1</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MyData</span><span class="o">();</span>
        <span class="n">myData1</span><span class="o">.</span><span class="na">setName</span><span class="o">(</span><span class="s">"one"</span><span class="o">);</span>
        <span class="n">myDataRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">myData1</span><span class="o">);</span>
        <span class="nc">MyData</span> <span class="n">myData2</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MyData</span><span class="o">();</span>
        <span class="n">myData2</span><span class="o">.</span><span class="na">setName</span><span class="o">(</span><span class="s">"two"</span><span class="o">);</span>
        <span class="n">myDataRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">myData2</span><span class="o">);</span>

        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"find by id with query: {}"</span><span class="o">,</span> <span class="n">myDataRepository</span><span class="o">.</span><span class="na">findByQuery</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="kc">null</span><span class="o">));</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"find by id and name with query: {}"</span><span class="o">,</span> <span class="n">myDataRepository</span><span class="o">.</span><span class="na">findByQuery</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="s">"one"</span><span class="o">));</span>

        <span class="nc">MyData</span> <span class="n">example</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MyData</span><span class="o">();</span>
        <span class="n">example</span><span class="o">.</span><span class="na">setName</span><span class="o">(</span><span class="s">"two"</span><span class="o">);</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"find by example: {}"</span><span class="o">,</span> <span class="n">myDataRepository</span><span class="o">.</span><span class="na">findAll</span><span class="o">(</span><span class="nc">Example</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">example</span><span class="o">)));</span>

        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"find by spec with id: {}"</span><span class="o">,</span> <span class="n">findBySpec</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="kc">null</span><span class="o">));</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"find by spec with name: {}"</span><span class="o">,</span> <span class="n">findBySpec</span><span class="o">(</span><span class="kc">null</span><span class="o">,</span> <span class="s">"%wo%"</span><span class="o">,</span> <span class="kc">null</span><span class="o">));</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"find by spec with id list: {}"</span><span class="o">,</span> <span class="n">findBySpec</span><span class="o">(</span><span class="kc">null</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="mi">2L</span><span class="o">)));</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">MyData</span><span class="o">&gt;</span> <span class="nf">findBySpec</span><span class="o">(</span><span class="nc">Long</span> <span class="n">id</span><span class="o">,</span> <span class="nc">String</span> <span class="n">name</span><span class="o">,</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Long</span><span class="o">&gt;</span> <span class="n">ids</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">myDataRepository</span><span class="o">.</span><span class="na">findAll</span><span class="o">((</span><span class="n">root</span><span class="o">,</span> <span class="n">query</span><span class="o">,</span> <span class="n">builder</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="o">{</span>
            <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Predicate</span><span class="o">&gt;</span> <span class="n">predicates</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;();</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">id</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">predicates</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">builder</span><span class="o">.</span><span class="na">equal</span><span class="o">(</span><span class="n">root</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="s">"id"</span><span class="o">),</span> <span class="n">id</span><span class="o">));</span>
            <span class="o">}</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">name</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">predicates</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">builder</span><span class="o">.</span><span class="na">like</span><span class="o">(</span><span class="n">root</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="s">"name"</span><span class="o">),</span> <span class="n">name</span><span class="o">));</span>
            <span class="o">}</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">ids</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">predicates</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">root</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="s">"id"</span><span class="o">).</span><span class="na">in</span><span class="o">(</span><span class="n">ids</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">and</span><span class="o">(</span><span class="n">predicates</span><span class="o">.</span><span class="na">toArray</span><span class="o">(</span><span class="k">new</span> <span class="nc">Predicate</span><span class="o">[</span><span class="mi">0</span><span class="o">]));</span>
        <span class="o">},</span> <span class="nc">Sort</span><span class="o">.</span><span class="na">by</span><span class="o">(</span><span class="s">"id"</span><span class="o">).</span><span class="na">descending</span><span class="o">());</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="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">SpringApplication</span><span class="o">.</span><span class="na">run</span><span class="o">(</span><span class="nc">Application</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>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><summary type="html"><![CDATA[在页面中展示列表数据时，通常需要根据用户输入的不同的查询条件返回不同的查询结果，传统的方式往往采用手动编写原始sql拼接where条件的方式，这种方式并不安全，容易存在sql注入漏洞。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/data-flex-query.jpg" /><media:content medium="image" url="https://springcamp.cn/assets/images/data-flex-query.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Spring Boot 3.2 新特性之 HTTP Interface</title><link href="https://springcamp.cn/spring-boot-http-interface/" rel="alternate" type="text/html" title="Spring Boot 3.2 新特性之 HTTP Interface" /><published>2023-12-24T08:20:00+00:00</published><updated>2023-12-24T08:20:00+00:00</updated><id>https://springcamp.cn/spring-boot-http-interface</id><content type="html" xml:base="https://springcamp.cn/spring-boot-http-interface/"><![CDATA[<p>SpringBoot 3.2引入了新的 HTTP interface 用于http接口调用，采用了类似 openfeign 的风格。</p>

<p>具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-http-interface">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-http-interface</a></p>

<h2 id="一概述">一、概述</h2>

<p>HTTP Interface 是一个类似于 openfeign 的同步接口调用方法，采用 Java interfaces 声明远程接口调用的方法，理念上类似于SpringDataRepository，可以很大程度精简代码。</p>

<p>要使远程调用的接口可以执行，还需要通过 HttpServiceProxyFactory 指定底层的http接口调用库，支持 RestTemplate、WebClient、RestClient三种。</p>

<h2 id="二引入-http-interface">二、引入 HTTP interface</h2>

<p>首先引入 spring-boot-starter-web 依赖。</p>

<p>在 build.gradle 中增加一行代码:</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">implementation</span> <span class="s1">'org.springframework.boot:spring-boot-starter-web'</span>
</code></pre></div></div>

<h2 id="三声明接口调用-interface">三、声明接口调用 Interface</h2>

<p>通过声明 Interface 的方式实现远程接口调用方法：</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">MyService</span> <span class="o">{</span>
    <span class="nd">@GetExchange</span><span class="o">(</span><span class="s">"/anything"</span><span class="o">)</span>
    <span class="nc">String</span> <span class="nf">getData</span><span class="o">(</span><span class="nd">@RequestHeader</span><span class="o">(</span><span class="s">"MY-HEADER"</span><span class="o">)</span> <span class="nc">String</span> <span class="n">headerName</span><span class="o">);</span>

    <span class="nd">@GetExchange</span><span class="o">(</span><span class="s">"/anything/{id}"</span><span class="o">)</span>
    <span class="nc">String</span> <span class="nf">getData</span><span class="o">(</span><span class="nd">@PathVariable</span> <span class="kt">long</span> <span class="n">id</span><span class="o">);</span>

    <span class="nd">@PostExchange</span><span class="o">(</span><span class="s">"/anything"</span><span class="o">)</span>
    <span class="nc">String</span> <span class="nf">saveData</span><span class="o">(</span><span class="nd">@RequestBody</span> <span class="nc">MyData</span> <span class="n">data</span><span class="o">);</span>

    <span class="nd">@DeleteExchange</span><span class="o">(</span><span class="s">"/anything/{id}"</span><span class="o">)</span>
    <span class="nc">ResponseEntity</span><span class="o">&lt;</span><span class="nc">Void</span><span class="o">&gt;</span> <span class="nf">deleteData</span><span class="o">(</span><span class="nd">@PathVariable</span> <span class="kt">long</span> <span class="n">id</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>在上述代码中，我们分别声明了包括 GET/POST/DELETE 操作的四个方法，其中第一个方法演示了如何在远程接口调用时指定header参数，只需要简单的使用 RequestHeader 注解即可。</p>

<h2 id="四使用声明的方法">四、使用声明的方法</h2>

<p>类似于SpringDataRepository，使用 HTTP interface 也非常简单，只需要注入对应的 Bean 即可：</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">MyController</span> <span class="o">{</span>
    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">MyService</span> <span class="n">myService</span><span class="o">;</span>

    <span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/foo"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getData</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">myService</span><span class="o">.</span><span class="na">getData</span><span class="o">(</span><span class="s">"myHeader"</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/foo/{id}"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getDataById</span><span class="o">(</span><span class="nd">@PathVariable</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">myService</span><span class="o">.</span><span class="na">getData</span><span class="o">(</span><span class="n">id</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@PostMapping</span><span class="o">(</span><span class="s">"/foo"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">saveData</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">myService</span><span class="o">.</span><span class="na">saveData</span><span class="o">(</span><span class="k">new</span> <span class="nc">MyData</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="s">"demo"</span><span class="o">));</span>
    <span class="o">}</span>

    <span class="nd">@DeleteMapping</span><span class="o">(</span><span class="s">"/foo"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">ResponseEntity</span><span class="o">&lt;</span><span class="nc">Void</span><span class="o">&gt;</span> <span class="nf">deleteData</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">ResponseEntity</span><span class="o">&lt;</span><span class="nc">Void</span><span class="o">&gt;</span> <span class="n">resp</span> <span class="o">=</span> <span class="n">myService</span><span class="o">.</span><span class="na">deleteData</span><span class="o">(</span><span class="mi">1L</span><span class="o">);</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"delete {}"</span><span class="o">,</span> <span class="n">resp</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">resp</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>便于演示方便，我们编写了自己的Controller。</p>

<p>在Controller中，我们注入声明好的 HTTP interface：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">MyService</span> <span class="n">myService</span><span class="o">;</span>
</code></pre></div></div>

<p>当我们自己的接口被调用时，接口内部会通过注入的 MyService 声明的方法调用其它系统的接口。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RestClient</span> <span class="n">restClient</span> <span class="o">=</span> <span class="nc">RestClient</span><span class="o">.</span><span class="na">builder</span><span class="o">(</span><span class="n">restTemplate</span><span class="o">).</span><span class="na">baseUrl</span><span class="o">(</span><span class="s">"https://httpbin.org"</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>
<span class="nc">RestClientAdapter</span> <span class="n">adapter</span> <span class="o">=</span> <span class="nc">RestClientAdapter</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="n">restClient</span><span class="o">);</span>
<span class="nc">HttpServiceProxyFactory</span> <span class="n">factory</span> <span class="o">=</span> <span class="nc">HttpServiceProxyFactory</span><span class="o">.</span><span class="na">builderFor</span><span class="o">(</span><span class="n">adapter</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>
</code></pre></div></div>

<h2 id="五实现-http-interface">五、实现 HTTP interface</h2>

<p>Spring framework 通过 HttpServiceProxyFactory 来实现 HTTP interface 方法：</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">MyClientConfig</span> <span class="o">{</span>
    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">RestTemplate</span> <span class="nf">restTemplate</span><span class="o">(</span><span class="nc">RestTemplateBuilder</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">build</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">MyService</span> <span class="nf">myService</span><span class="o">(</span><span class="nc">RestTemplate</span> <span class="n">restTemplate</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">restTemplate</span><span class="o">.</span><span class="na">setUriTemplateHandler</span><span class="o">(</span><span class="k">new</span> <span class="nc">DefaultUriBuilderFactory</span><span class="o">(</span><span class="s">"https://httpbin.org"</span><span class="o">));</span>
        <span class="nc">RestTemplateAdapter</span> <span class="n">adapter</span> <span class="o">=</span> <span class="nc">RestTemplateAdapter</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="n">restTemplate</span><span class="o">);</span>
        <span class="nc">HttpServiceProxyFactory</span> <span class="n">factory</span> <span class="o">=</span> <span class="nc">HttpServiceProxyFactory</span><span class="o">.</span><span class="na">builderFor</span><span class="o">(</span><span class="n">adapter</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>

        <span class="k">return</span> <span class="n">factory</span><span class="o">.</span><span class="na">createClient</span><span class="o">(</span><span class="nc">MyService</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>在上述配置中，我们可以看到 MyService 这个 HTTP interface 对应的 Bean 的初始化方法。</p>

<p>如果想使用 Spring Boot 3.2 新出的 RestClient，那初始化代码可以改为</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RestClient</span> <span class="n">restClient</span> <span class="o">=</span> <span class="nc">RestClient</span><span class="o">.</span><span class="na">builder</span><span class="o">(</span><span class="n">restTemplate</span><span class="o">).</span><span class="na">baseUrl</span><span class="o">(</span><span class="s">"https://httpbin.org"</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>
<span class="nc">RestClientAdapter</span> <span class="n">adapter</span> <span class="o">=</span> <span class="nc">RestClientAdapter</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="n">restClient</span><span class="o">);</span>
<span class="nc">HttpServiceProxyFactory</span> <span class="n">factory</span> <span class="o">=</span> <span class="nc">HttpServiceProxyFactory</span><span class="o">.</span><span class="na">builderFor</span><span class="o">(</span><span class="n">adapter</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>
</code></pre></div></div>

<h2 id="六单元测试">六、单元测试</h2>

<p>常用的单元测试方法对于 HTTP interface 仍然可用，对应的文章可以参照：<a href="https://springcamp.cn/spring-boot-unit-test/">springboot单元测试技术</a></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Slf4j</span>
<span class="nd">@RunWith</span><span class="o">(</span><span class="nc">SpringRunner</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="nd">@SpringBootTest</span><span class="o">(</span><span class="n">webEnvironment</span> <span class="o">=</span> <span class="nc">SpringBootTest</span><span class="o">.</span><span class="na">WebEnvironment</span><span class="o">.</span><span class="na">RANDOM_PORT</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">DemoApplicationTest</span> <span class="o">{</span>

    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">TestRestTemplate</span> <span class="n">testRestTemplate</span><span class="o">;</span>
    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">RestTemplate</span> <span class="n">restTemplate</span><span class="o">;</span>

    <span class="kd">private</span> <span class="nc">MockRestServiceServer</span> <span class="n">mockRestServiceServer</span><span class="o">;</span>

    <span class="nd">@Before</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">before</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">mockRestServiceServer</span> <span class="o">=</span> <span class="nc">MockRestServiceServer</span><span class="o">.</span><span class="na">bindTo</span><span class="o">(</span><span class="n">restTemplate</span><span class="o">).</span><span class="na">ignoreExpectOrder</span><span class="o">(</span><span class="kc">true</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>
        <span class="k">this</span><span class="o">.</span><span class="na">mockRestServiceServer</span><span class="o">.</span><span class="na">expect</span><span class="o">(</span><span class="nc">ExpectedCount</span><span class="o">.</span><span class="na">manyTimes</span><span class="o">(),</span> <span class="nc">MockRestRequestMatchers</span><span class="o">.</span><span class="na">requestTo</span><span class="o">(</span><span class="nc">Matchers</span><span class="o">.</span><span class="na">startsWithIgnoringCase</span><span class="o">(</span><span class="s">"https://httpbin.org"</span><span class="o">)))</span>
                <span class="o">.</span><span class="na">andExpect</span><span class="o">(</span><span class="n">method</span><span class="o">(</span><span class="nc">HttpMethod</span><span class="o">.</span><span class="na">GET</span><span class="o">))</span>
                <span class="o">.</span><span class="na">andRespond</span><span class="o">(</span><span class="nc">MockRestResponseCreators</span><span class="o">.</span><span class="na">withSuccess</span><span class="o">(</span><span class="s">"{\"get\": 200}"</span><span class="o">,</span> <span class="nc">MediaType</span><span class="o">.</span><span class="na">APPLICATION_JSON</span><span class="o">));</span>
        <span class="k">this</span><span class="o">.</span><span class="na">mockRestServiceServer</span><span class="o">.</span><span class="na">expect</span><span class="o">(</span><span class="nc">ExpectedCount</span><span class="o">.</span><span class="na">manyTimes</span><span class="o">(),</span> <span class="nc">MockRestRequestMatchers</span><span class="o">.</span><span class="na">requestTo</span><span class="o">(</span><span class="nc">Matchers</span><span class="o">.</span><span class="na">startsWithIgnoringCase</span><span class="o">(</span><span class="s">"https://httpbin.org"</span><span class="o">)))</span>
                <span class="o">.</span><span class="na">andExpect</span><span class="o">(</span><span class="n">method</span><span class="o">(</span><span class="nc">HttpMethod</span><span class="o">.</span><span class="na">POST</span><span class="o">))</span>
                <span class="o">.</span><span class="na">andRespond</span><span class="o">(</span><span class="nc">MockRestResponseCreators</span><span class="o">.</span><span class="na">withSuccess</span><span class="o">(</span><span class="s">"{\"post\": 200}"</span><span class="o">,</span> <span class="nc">MediaType</span><span class="o">.</span><span class="na">APPLICATION_JSON</span><span class="o">));</span>
        <span class="k">this</span><span class="o">.</span><span class="na">mockRestServiceServer</span><span class="o">.</span><span class="na">expect</span><span class="o">(</span><span class="nc">ExpectedCount</span><span class="o">.</span><span class="na">manyTimes</span><span class="o">(),</span> <span class="nc">MockRestRequestMatchers</span><span class="o">.</span><span class="na">requestTo</span><span class="o">(</span><span class="nc">Matchers</span><span class="o">.</span><span class="na">startsWithIgnoringCase</span><span class="o">(</span><span class="s">"https://httpbin.org"</span><span class="o">)))</span>
                <span class="o">.</span><span class="na">andExpect</span><span class="o">(</span><span class="n">method</span><span class="o">(</span><span class="nc">HttpMethod</span><span class="o">.</span><span class="na">DELETE</span><span class="o">))</span>
                <span class="o">.</span><span class="na">andRespond</span><span class="o">(</span><span class="nc">MockRestResponseCreators</span><span class="o">.</span><span class="na">withSuccess</span><span class="o">(</span><span class="s">"{\"delete\": 200}"</span><span class="o">,</span> <span class="nc">MediaType</span><span class="o">.</span><span class="na">APPLICATION_JSON</span><span class="o">));</span>
    <span class="o">}</span>

    <span class="nd">@Test</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">testRemoteCallRest</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"testRemoteCallRest get {}"</span><span class="o">,</span> <span class="n">testRestTemplate</span><span class="o">.</span><span class="na">getForObject</span><span class="o">(</span><span class="s">"/foo"</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">));</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"testRemoteCallRest getById {}"</span><span class="o">,</span> <span class="n">testRestTemplate</span><span class="o">.</span><span class="na">getForObject</span><span class="o">(</span><span class="s">"/foo/1"</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">));</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"testRemoteCallRest post {}"</span><span class="o">,</span> <span class="n">testRestTemplate</span><span class="o">.</span><span class="na">postForObject</span><span class="o">(</span><span class="s">"/foo"</span><span class="o">,</span> <span class="k">new</span> <span class="nc">MyData</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="s">"demo"</span><span class="o">),</span> <span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">));</span>
        <span class="n">testRestTemplate</span><span class="o">.</span><span class="na">exchange</span><span class="o">(</span><span class="s">"/foo"</span><span class="o">,</span> <span class="nc">HttpMethod</span><span class="o">.</span><span class="na">DELETE</span><span class="o">,</span> <span class="nc">HttpEntity</span><span class="o">.</span><span class="na">EMPTY</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><category term="featured" /><summary type="html"><![CDATA[SpringBoot 3.2引入了新的 HTTP interface 用于http接口调用，采用了类似 openfeign 的风格。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/http-interface.jpg" /><media:content medium="image" url="https://springcamp.cn/assets/images/http-interface.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Spring Boot 3.2 新特性之 RestClient</title><link href="https://springcamp.cn/spring-boot-rest-client/" rel="alternate" type="text/html" title="Spring Boot 3.2 新特性之 RestClient" /><published>2023-12-03T08:20:00+00:00</published><updated>2023-12-03T08:20:00+00:00</updated><id>https://springcamp.cn/spring-boot-rest-client</id><content type="html" xml:base="https://springcamp.cn/spring-boot-rest-client/"><![CDATA[<p>SpringBoot 3.2引入了新的 RestClient 用于http接口调用，采用了 fluent API 的风格，可以进行链式调用。</p>

<p>具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-data-jdbc-client">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-data-jdbc-client</a></p>

<h2 id="一概述">一、概述</h2>

<p>RestClient 是一个类似于 RestTemplate 的的同步接口调用工具。相比于 RestTemplate 采用的是 template 设计模式，RestClient 采用了 fluent API 风格，简单灵活，易于阅读和维护。</p>

<h2 id="二引入-restclient">二、引入 RestClient</h2>

<p>首先引入 spring-boot-starter-web 依赖。</p>

<p>在 build.gradle 中增加一行代码:</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">implementation</span> <span class="s1">'org.springframework.boot:spring-boot-starter-web'</span>
</code></pre></div></div>

<p>对 RestClient 进行配置：</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">RestClientConfig</span> <span class="o">{</span>
    <span class="kd">public</span> <span class="nc">CloseableHttpClient</span> <span class="nf">httpClient</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">Registry</span><span class="o">&lt;</span><span class="nc">ConnectionSocketFactory</span><span class="o">&gt;</span> <span class="n">registry</span> <span class="o">=</span>
                <span class="nc">RegistryBuilder</span><span class="o">.&lt;</span><span class="nc">ConnectionSocketFactory</span><span class="o">&gt;</span><span class="n">create</span><span class="o">()</span>
                        <span class="o">.</span><span class="na">register</span><span class="o">(</span><span class="s">"http"</span><span class="o">,</span> <span class="nc">PlainConnectionSocketFactory</span><span class="o">.</span><span class="na">getSocketFactory</span><span class="o">())</span>
                        <span class="o">.</span><span class="na">register</span><span class="o">(</span><span class="s">"https"</span><span class="o">,</span> <span class="nc">SSLConnectionSocketFactory</span><span class="o">.</span><span class="na">getSocketFactory</span><span class="o">())</span>
                        <span class="o">.</span><span class="na">build</span><span class="o">();</span>
        <span class="nc">PoolingHttpClientConnectionManager</span> <span class="n">poolingConnectionManager</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">PoolingHttpClientConnectionManager</span><span class="o">(</span><span class="n">registry</span><span class="o">);</span>

        <span class="n">poolingConnectionManager</span><span class="o">.</span><span class="na">setDefaultSocketConfig</span><span class="o">(</span><span class="nc">SocketConfig</span><span class="o">.</span><span class="na">custom</span><span class="o">().</span><span class="na">setSoTimeout</span><span class="o">(</span><span class="nc">Timeout</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">2</span><span class="o">)).</span><span class="na">build</span><span class="o">());</span>
        <span class="n">poolingConnectionManager</span><span class="o">.</span><span class="na">setDefaultConnectionConfig</span><span class="o">(</span><span class="nc">ConnectionConfig</span><span class="o">.</span><span class="na">custom</span><span class="o">().</span><span class="na">setConnectTimeout</span><span class="o">(</span><span class="nc">Timeout</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">2</span><span class="o">)).</span><span class="na">build</span><span class="o">());</span>

        <span class="c1">// set total amount of connections across all HTTP routes</span>
        <span class="n">poolingConnectionManager</span><span class="o">.</span><span class="na">setMaxTotal</span><span class="o">(</span><span class="mi">200</span><span class="o">);</span>
        <span class="c1">// set maximum amount of connections for each http route in pool</span>
        <span class="n">poolingConnectionManager</span><span class="o">.</span><span class="na">setDefaultMaxPerRoute</span><span class="o">(</span><span class="mi">200</span><span class="o">);</span>

        <span class="nc">RequestConfig</span> <span class="n">requestConfig</span> <span class="o">=</span> <span class="nc">RequestConfig</span><span class="o">.</span><span class="na">custom</span><span class="o">()</span>
                <span class="o">.</span><span class="na">setConnectionKeepAlive</span><span class="o">(</span><span class="nc">TimeValue</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">10</span><span class="o">))</span>
                <span class="o">.</span><span class="na">setConnectionRequestTimeout</span><span class="o">(</span><span class="nc">Timeout</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">2</span><span class="o">))</span>
                <span class="o">.</span><span class="na">setResponseTimeout</span><span class="o">(</span><span class="nc">Timeout</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">2</span><span class="o">))</span>
                <span class="o">.</span><span class="na">build</span><span class="o">();</span>

        <span class="k">return</span> <span class="nc">HttpClients</span><span class="o">.</span><span class="na">custom</span><span class="o">()</span>
                <span class="o">.</span><span class="na">setDefaultRequestConfig</span><span class="o">(</span><span class="n">requestConfig</span><span class="o">)</span>
                <span class="o">.</span><span class="na">setConnectionManager</span><span class="o">(</span><span class="n">poolingConnectionManager</span><span class="o">)</span>
                <span class="o">.</span><span class="na">setKeepAliveStrategy</span><span class="o">(</span><span class="k">new</span> <span class="nc">DefaultConnectionKeepAliveStrategy</span><span class="o">())</span>
                <span class="o">.</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Slf4j</span>
    <span class="kd">static</span> <span class="kd">class</span> <span class="nc">CustomClientHttpRequestInterceptor</span> <span class="kd">implements</span> <span class="nc">ClientHttpRequestInterceptor</span> <span class="o">{</span>
        <span class="nd">@Override</span>
        <span class="nd">@NonNull</span>
        <span class="kd">public</span> <span class="nc">ClientHttpResponse</span> <span class="nf">intercept</span><span class="o">(</span><span class="nc">HttpRequest</span> <span class="n">request</span><span class="o">,</span> <span class="nd">@NonNull</span> <span class="kt">byte</span><span class="o">[]</span> <span class="n">bytes</span><span class="o">,</span> <span class="nd">@NonNull</span> <span class="nc">ClientHttpRequestExecution</span> <span class="n">execution</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span>
            <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"HTTP Method: {}, URI: {}, Headers: {}"</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">request</span><span class="o">.</span><span class="na">getURI</span><span class="o">(),</span> <span class="n">request</span><span class="o">.</span><span class="na">getHeaders</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="k">if</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="na">equals</span><span class="o">(</span><span class="nc">HttpMethod</span><span class="o">.</span><span class="na">POST</span><span class="o">))</span> <span class="o">{</span>
                <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"HTTP body: {}"</span><span class="o">,</span> <span class="k">new</span> <span class="nc">String</span><span class="o">(</span><span class="n">bytes</span><span class="o">,</span> <span class="nc">StandardCharsets</span><span class="o">.</span><span class="na">UTF_8</span><span class="o">));</span>
            <span class="o">}</span>

            <span class="nc">ClientHttpResponse</span> <span class="n">response</span> <span class="o">=</span> <span class="n">execution</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">bytes</span><span class="o">);</span>
            <span class="nc">ClientHttpResponse</span> <span class="n">responseWrapper</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">BufferingClientHttpResponseWrapper</span><span class="o">(</span><span class="n">response</span><span class="o">);</span>

            <span class="nc">String</span> <span class="n">body</span> <span class="o">=</span> <span class="nc">StreamUtils</span><span class="o">.</span><span class="na">copyToString</span><span class="o">(</span><span class="n">responseWrapper</span><span class="o">.</span><span class="na">getBody</span><span class="o">(),</span> <span class="nc">StandardCharsets</span><span class="o">.</span><span class="na">UTF_8</span><span class="o">);</span>
            <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"RESPONSE body: {}"</span><span class="o">,</span> <span class="n">body</span><span class="o">);</span>

            <span class="k">return</span> <span class="n">responseWrapper</span><span class="o">;</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="kd">static</span> <span class="kd">class</span> <span class="nc">BufferingClientHttpResponseWrapper</span> <span class="kd">implements</span> <span class="nc">ClientHttpResponse</span> <span class="o">{</span>

        <span class="kd">private</span> <span class="kd">final</span> <span class="nc">ClientHttpResponse</span> <span class="n">response</span><span class="o">;</span>
        <span class="kd">private</span> <span class="kt">byte</span><span class="o">[]</span> <span class="n">body</span><span class="o">;</span>

        <span class="nc">BufferingClientHttpResponseWrapper</span><span class="o">(</span><span class="nc">ClientHttpResponse</span> <span class="n">response</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">this</span><span class="o">.</span><span class="na">response</span> <span class="o">=</span> <span class="n">response</span><span class="o">;</span>
        <span class="o">}</span>

        <span class="nd">@NonNull</span>
        <span class="kd">public</span> <span class="nc">HttpStatusCode</span> <span class="nf">getStatusCode</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span>
            <span class="k">return</span> <span class="k">this</span><span class="o">.</span><span class="na">response</span><span class="o">.</span><span class="na">getStatusCode</span><span class="o">();</span>
        <span class="o">}</span>

        <span class="nd">@NonNull</span>
        <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getStatusText</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span>
            <span class="k">return</span> <span class="k">this</span><span class="o">.</span><span class="na">response</span><span class="o">.</span><span class="na">getStatusText</span><span class="o">();</span>
        <span class="o">}</span>

        <span class="nd">@NonNull</span>
        <span class="kd">public</span> <span class="nc">HttpHeaders</span> <span class="nf">getHeaders</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">response</span><span class="o">.</span><span class="na">getHeaders</span><span class="o">();</span>
        <span class="o">}</span>

        <span class="nd">@NonNull</span>
        <span class="kd">public</span> <span class="nc">InputStream</span> <span class="nf">getBody</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">IOException</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">body</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                <span class="k">this</span><span class="o">.</span><span class="na">body</span> <span class="o">=</span> <span class="nc">StreamUtils</span><span class="o">.</span><span class="na">copyToByteArray</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">response</span><span class="o">.</span><span class="na">getBody</span><span class="o">());</span>
            <span class="o">}</span>
            <span class="k">return</span> <span class="k">new</span> <span class="nf">ByteArrayInputStream</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">body</span><span class="o">);</span>
        <span class="o">}</span>

        <span class="kd">public</span> <span class="kt">void</span> <span class="nf">close</span><span class="o">()</span> <span class="o">{</span>
            <span class="k">this</span><span class="o">.</span><span class="na">response</span><span class="o">.</span><span class="na">close</span><span class="o">();</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">RestTemplate</span> <span class="nf">restTemplate</span><span class="o">(</span><span class="nc">RestTemplateBuilder</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">requestFactory</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="k">new</span> <span class="nc">HttpComponentsClientHttpRequestFactory</span><span class="o">(</span><span class="n">httpClient</span><span class="o">()))</span>
                <span class="o">.</span><span class="na">interceptors</span><span class="o">(</span><span class="k">new</span> <span class="nc">CustomClientHttpRequestInterceptor</span><span class="o">())</span>
                <span class="o">.</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">RestClient</span> <span class="nf">restClient</span><span class="o">(</span><span class="nc">RestTemplate</span> <span class="n">restTemplate</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">RestClient</span><span class="o">.</span><span class="na">builder</span><span class="o">(</span><span class="n">restTemplate</span><span class="o">).</span><span class="na">requestFactory</span><span class="o">(</span><span class="k">new</span> <span class="nc">HttpComponentsClientHttpRequestFactory</span><span class="o">(</span><span class="n">httpClient</span><span class="o">())).</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>在配置中我们仍然定义了 RestTemplate ，并使用 RestTemplate 来初始化 RestClient 为的是继续使用 RestTemplate 的日志打印功能 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-rest-template-log">参照 https://github.com/qihaiyan/springcamp/tree/main/spring-rest-template-log</a></p>

<p>如果不想继续使用RestTemplate，那初始化代码可以改为</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RestClient</span><span class="o">.</span><span class="na">builder</span><span class="o">().</span><span class="na">requestFactory</span><span class="o">(</span><span class="k">new</span> <span class="nc">HttpComponentsClientHttpRequestFactory</span><span class="o">(</span><span class="n">httpClient</span><span class="o">())).</span><span class="na">build</span><span class="o">();</span>
</code></pre></div></div>

<p>同时我们给 RestClient 配置了 requestFactory ，可以使用长连接调用接口。</p>

<h2 id="三get接口调用">三、GET接口调用</h2>

<p>调用GET接口返回字符串：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">restClient</span><span class="o">.</span><span class="na">get</span><span class="o">()</span>
                <span class="o">.</span><span class="na">uri</span><span class="o">(</span><span class="s">"https://httpbin.org/get"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">retrieve</span><span class="o">()</span>
                <span class="o">.</span><span class="na">body</span><span class="o">(</span><span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
</code></pre></div></div>

<p>调用GET接口对象：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">restClient</span><span class="o">.</span><span class="na">get</span><span class="o">()</span>
                <span class="o">.</span><span class="na">uri</span><span class="o">(</span><span class="s">"https://httpbin.org/get"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">retrieve</span><span class="o">()</span>
                <span class="o">.</span><span class="na">body</span><span class="o">(</span><span class="nc">MyData</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
</code></pre></div></div>

<p>调用GET接口返回List：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">List</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">list</span> <span class="o">=</span> <span class="n">restClient</span><span class="o">.</span><span class="na">get</span><span class="o">()</span>
                <span class="o">.</span><span class="na">uri</span><span class="o">(</span><span class="s">"http://someservice/list"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">retrieve</span><span class="o">()</span>
                <span class="o">.</span><span class="na">body</span><span class="o">(</span><span class="k">new</span> <span class="nc">ParameterizedTypeReference</span><span class="o">&lt;&gt;()</span> <span class="o">{});</span>
</code></pre></div></div>

<h2 id="四post接口调用">四、POST接口调用</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">MyData</span> <span class="n">postBody</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MyData</span><span class="o">(</span><span class="s">"test"</span><span class="o">,</span> <span class="s">"test RestClient"</span><span class="o">);</span>
        <span class="nc">ResponseEntity</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">respObj</span> <span class="o">=</span> <span class="n">restClient</span><span class="o">.</span><span class="na">post</span><span class="o">()</span>
                <span class="o">.</span><span class="na">uri</span><span class="o">(</span><span class="s">"https://httpbin.org/post"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">contentType</span><span class="o">(</span><span class="nc">MediaType</span><span class="o">.</span><span class="na">APPLICATION_JSON</span><span class="o">)</span>
                <span class="o">.</span><span class="na">body</span><span class="o">(</span><span class="n">postBody</span><span class="o">)</span>
                <span class="o">.</span><span class="na">retrieve</span><span class="o">()</span>
                <span class="o">.</span><span class="na">toEntity</span><span class="o">(</span><span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
</code></pre></div></div>

<h2 id="五exchange接口调用">五、Exchange接口调用</h2>

<p>当需要对接口返回结果进行更加精确的控制时，可以采用 Exchange 方法。
例如当接口返回 4xx 时，让 restClient 返回空字符串，否则返回正常结果：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">restClient</span><span class="o">.</span><span class="na">get</span><span class="o">()</span>
                <span class="o">.</span><span class="na">uri</span><span class="o">(</span><span class="s">"https://httpbin.org/get"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">accept</span><span class="o">(</span><span class="nc">MediaType</span><span class="o">.</span><span class="na">APPLICATION_JSON</span><span class="o">)</span>
                <span class="o">.</span><span class="na">exchange</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="o">-&gt;</span> <span class="o">{</span>
                    <span class="k">if</span> <span class="o">(</span><span class="n">response</span><span class="o">.</span><span class="na">getStatusCode</span><span class="o">().</span><span class="na">is4xxClientError</span><span class="o">())</span> <span class="o">{</span>
                        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"status 4xx"</span><span class="o">);</span>
                        <span class="k">return</span> <span class="s">""</span><span class="o">;</span>
                    <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
                        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"response: {}"</span><span class="o">,</span> <span class="n">response</span><span class="o">);</span>
                        <span class="k">return</span> <span class="n">response</span><span class="o">;</span>
                    <span class="o">}</span>
                <span class="o">});</span>
</code></pre></div></div>

<h2 id="六错误处理">六、错误处理</h2>

<p>当接口返回错误时，可以在 onStatus 方法中进行判断并进行对应的操作：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">restClient</span><span class="o">.</span><span class="na">get</span><span class="o">()</span>
                <span class="o">.</span><span class="na">uri</span><span class="o">(</span><span class="s">"https://httpbin.org/status/404"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">retrieve</span><span class="o">()</span>
                <span class="o">.</span><span class="na">onStatus</span><span class="o">(</span><span class="n">status</span> <span class="o">-&gt;</span> <span class="n">status</span><span class="o">.</span><span class="na">value</span><span class="o">()</span> <span class="o">==</span> <span class="mi">404</span><span class="o">,</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="o">-&gt;</span> <span class="o">{</span>
                    <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"status 404"</span><span class="o">);</span>
                <span class="o">})</span>
                <span class="o">.</span><span class="na">toBodilessEntity</span><span class="o">();</span>
</code></pre></div></div>

<p>toBodilessEntity 方法是一种忽略接口返回结果的方法，当不需要读取接口返回结果时，可以使用 toBodilessEntity 方法。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><summary type="html"><![CDATA[SpringBoot 3.2引入了新的 RestClient 用于http接口调用，采用了 fluent API 的风格，可以进行链式调用。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/rest-client.jpg" /><media:content medium="image" url="https://springcamp.cn/assets/images/rest-client.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Spring Boot 3.2 新特性之 JdbcClient</title><link href="https://springcamp.cn/spring-boot-jdbc-client/" rel="alternate" type="text/html" title="Spring Boot 3.2 新特性之 JdbcClient" /><published>2023-11-26T08:20:00+00:00</published><updated>2023-11-26T08:20:00+00:00</updated><id>https://springcamp.cn/spring-boot-jdbc-client</id><content type="html" xml:base="https://springcamp.cn/spring-boot-jdbc-client/"><![CDATA[<p>SpringBoot 3.2引入了新的 JdbcClient 用于数据库操作，JdbcClient对JdbcTemplate进行了封装，采用了 fluent API 的风格，可以进行链式调用。</p>

<p>自此，spring自带的数据库操作有了4种方式：JdbcTemplate、JdbcClient、SpringDataJdbc、SpringDataJpa。</p>

<p>对于不适合使用复杂的ORM框架，或者需要编写复杂的SQL的场景，可以使用JdbcClient自己编写SQL来操作数据库。不过JdbcClient不支持数据的批量操作和存储过程调用，对于这种情况就需要使用JdbcTemplate。</p>

<p>具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-data-jdbc-client">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-data-jdbc-client</a></p>

<h2 id="一概述">一、概述</h2>

<p>JdbcClient是一个轻量的数据库操作框架，采用 fluent API 风格，简单灵活，易于阅读和维护，支持编写复杂的SQL。</p>

<h2 id="二引入-jdbcclient">二、引入 JdbcClient</h2>

<p>首先引入 spring-data-jdbc 依赖。</p>

<p>在 build.gradle 中增加一行代码:</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">implementation</span> <span class="s1">'org.springframework.boot:spring-boot-starter-data-jdbc'</span>
</code></pre></div></div>

<p>在Service中直接注入JdbcClient即可：</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">DbService</span> <span class="o">{</span>
    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">JdbcClient</span> <span class="n">jdbcClient</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="三查询操作">三、查询操作</h2>

<p>通过JdbcClient，可以按照主键查数据，也可以按照自定义查询条件查数据。</p>

<p>按照主键查数据：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">MyData</span> <span class="nf">findDataById</span><span class="o">(</span><span class="nc">Long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">jdbcClient</span><span class="o">.</span><span class="na">sql</span><span class="o">(</span><span class="s">"select * from my_data where id = ?"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">params</span><span class="o">(</span><span class="n">id</span><span class="o">)</span>
                <span class="o">.</span><span class="na">query</span><span class="o">(</span><span class="nc">MyData</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
                <span class="o">.</span><span class="na">single</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="nc">List</span><span class="o">&lt;</span><span class="nc">MyData</span><span class="o">&gt;</span> <span class="nf">findDataByName</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">jdbcClient</span><span class="o">.</span><span class="na">sql</span><span class="o">(</span><span class="s">"select * from my_data where name = ?"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">params</span><span class="o">(</span><span class="n">name</span><span class="o">)</span>
                <span class="o">.</span><span class="na">query</span><span class="o">(</span><span class="nc">MyData</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
                <span class="o">.</span><span class="na">list</span><span class="o">();</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>以上两种查询方式，查询条件中的变量使用的是占位符，JdbcClient也支持按照参数名进行查询：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">Integer</span> <span class="nf">insertDataWithNamedParam</span><span class="o">(</span><span class="nc">MyData</span> <span class="n">myData</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Integer</span> <span class="n">rowsAffected</span> <span class="o">=</span> <span class="n">jdbcClient</span><span class="o">.</span><span class="na">sql</span><span class="o">(</span><span class="s">"insert into my_data values(:id,:name) "</span><span class="o">)</span>
                <span class="o">.</span><span class="na">param</span><span class="o">(</span><span class="s">"id"</span><span class="o">,</span> <span class="n">myData</span><span class="o">.</span><span class="na">id</span><span class="o">())</span>
                <span class="o">.</span><span class="na">param</span><span class="o">(</span><span class="s">"name"</span><span class="o">,</span> <span class="n">myData</span><span class="o">.</span><span class="na">name</span><span class="o">())</span>
                <span class="o">.</span><span class="na">update</span><span class="o">();</span>
        <span class="k">return</span> <span class="n">rowsAffected</span><span class="o">;</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>当参数比较多时，可以将参数放到一个Map中，用Map进行查询：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">MyData</span><span class="o">&gt;</span> <span class="nf">findDataByParamMap</span><span class="o">(</span><span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="o">?&gt;</span> <span class="n">paramMap</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">jdbcClient</span><span class="o">.</span><span class="na">sql</span><span class="o">(</span><span class="s">"select * from my_data where name = :name"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">params</span><span class="o">(</span><span class="n">paramMap</span><span class="o">)</span>
                <span class="o">.</span><span class="na">query</span><span class="o">(</span><span class="nc">MyData</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
                <span class="o">.</span><span class="na">list</span><span class="o">();</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>当查询返回的结果不能简单的映射到一个类时，可以编写RowMapper，适用于SQL语句比较复杂的场景：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">MyData</span><span class="o">&gt;</span> <span class="nf">findDataWithRowMapper</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">jdbcClient</span><span class="o">.</span><span class="na">sql</span><span class="o">(</span><span class="s">"select * from my_data"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">query</span><span class="o">((</span><span class="n">rs</span><span class="o">,</span> <span class="n">rowNum</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="k">new</span> <span class="nc">MyData</span><span class="o">(</span><span class="n">rs</span><span class="o">.</span><span class="na">getLong</span><span class="o">(</span><span class="s">"id"</span><span class="o">),</span> <span class="n">rs</span><span class="o">.</span><span class="na">getString</span><span class="o">(</span><span class="s">"name"</span><span class="o">)))</span>
                <span class="o">.</span><span class="na">list</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="nc">Integer</span> <span class="nf">countByName</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">jdbcClient</span><span class="o">.</span><span class="na">sql</span><span class="o">(</span><span class="s">"select count(*) from my_data where name = ?"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">params</span><span class="o">(</span><span class="n">name</span><span class="o">)</span>
                <span class="o">.</span><span class="na">query</span><span class="o">(</span><span class="nc">Integer</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
                <span class="o">.</span><span class="na">single</span><span class="o">();</span>
    <span class="o">}</span>
</code></pre></div></div>

<h2 id="四插入数据">四、插入数据</h2>

<p>可以使用 JdbcClient 的 update 方法进行数据的插入和更新。</p>

<p>通过占位符参数插入数据:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">Integer</span> <span class="nf">insertDataWithParam</span><span class="o">(</span><span class="nc">MyData</span> <span class="n">myData</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Integer</span> <span class="n">rowsAffected</span> <span class="o">=</span> <span class="n">jdbcClient</span><span class="o">.</span><span class="na">sql</span><span class="o">(</span><span class="s">"insert into my_data values(?,?) "</span><span class="o">)</span>
                <span class="o">.</span><span class="na">param</span><span class="o">(</span><span class="n">myData</span><span class="o">.</span><span class="na">id</span><span class="o">())</span>
                <span class="o">.</span><span class="na">param</span><span class="o">(</span><span class="n">myData</span><span class="o">.</span><span class="na">name</span><span class="o">())</span>
                <span class="o">.</span><span class="na">update</span><span class="o">();</span>
        <span class="k">return</span> <span class="n">rowsAffected</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="nc">Integer</span> <span class="nf">insertDataWithNamedParam</span><span class="o">(</span><span class="nc">MyData</span> <span class="n">myData</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Integer</span> <span class="n">rowsAffected</span> <span class="o">=</span> <span class="n">jdbcClient</span><span class="o">.</span><span class="na">sql</span><span class="o">(</span><span class="s">"insert into my_data values(:id,:name) "</span><span class="o">)</span>
                <span class="o">.</span><span class="na">param</span><span class="o">(</span><span class="s">"id"</span><span class="o">,</span> <span class="n">myData</span><span class="o">.</span><span class="na">id</span><span class="o">())</span>
                <span class="o">.</span><span class="na">param</span><span class="o">(</span><span class="s">"name"</span><span class="o">,</span> <span class="n">myData</span><span class="o">.</span><span class="na">name</span><span class="o">())</span>
                <span class="o">.</span><span class="na">update</span><span class="o">();</span>
        <span class="k">return</span> <span class="n">rowsAffected</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="nc">Integer</span> <span class="nf">insertDataWithObject</span><span class="o">(</span><span class="nc">MyData</span> <span class="n">myData</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Integer</span> <span class="n">rowsAffected</span> <span class="o">=</span> <span class="n">jdbcClient</span><span class="o">.</span><span class="na">sql</span><span class="o">(</span><span class="s">"insert into my_data values(:id,:name) "</span><span class="o">)</span>
                <span class="o">.</span><span class="na">paramSource</span><span class="o">(</span><span class="n">myData</span><span class="o">)</span>
                <span class="o">.</span><span class="na">update</span><span class="o">();</span>
        <span class="k">return</span> <span class="n">rowsAffected</span><span class="o">;</span>
    <span class="o">}</span>
</code></pre></div></div>

<h2 id="五总结">五、总结</h2>

<p>通过上述的示例，可以看到基本的数据库操作都可以用 JdbcClient 实现，避免了复杂的ORM框架的使用，切操作要比ORM框架简单灵活的多。fluent API 的风格也更容易编写和阅读。</p>

<p>完整的数据库操作方法的调用演示：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Slf4j</span>
<span class="nd">@SpringBootApplication</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Application</span> <span class="kd">implements</span> <span class="nc">CommandLineRunner</span> <span class="o">{</span>

    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">DbService</span> <span class="n">dbService</span><span class="o">;</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">run</span><span class="o">(</span><span class="nc">String</span><span class="o">...</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">MyData</span> <span class="n">myData</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MyData</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="s">"test"</span><span class="o">);</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"insert rows: {}"</span><span class="o">,</span> <span class="n">dbService</span><span class="o">.</span><span class="na">insertDataWithObject</span><span class="o">(</span><span class="n">myData</span><span class="o">));</span>

        <span class="nc">MyData</span> <span class="n">myData2</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MyData</span><span class="o">(</span><span class="mi">2L</span><span class="o">,</span> <span class="s">"test"</span><span class="o">);</span>
        <span class="n">dbService</span><span class="o">.</span><span class="na">insertDataWithParam</span><span class="o">(</span><span class="n">myData2</span><span class="o">);</span>

        <span class="nc">MyData</span> <span class="n">myData3</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MyData</span><span class="o">(</span><span class="mi">3L</span><span class="o">,</span> <span class="s">"author"</span><span class="o">);</span>
        <span class="n">dbService</span><span class="o">.</span><span class="na">insertDataWithNamedParam</span><span class="o">(</span><span class="n">myData3</span><span class="o">);</span>

        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"findDataById: {}"</span><span class="o">,</span> <span class="n">dbService</span><span class="o">.</span><span class="na">findDataById</span><span class="o">(</span><span class="mi">1L</span><span class="o">));</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"findDataByName: {}"</span><span class="o">,</span> <span class="n">dbService</span><span class="o">.</span><span class="na">findDataByName</span><span class="o">(</span><span class="s">"test"</span><span class="o">));</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"findDataWithRowMapper: {}"</span><span class="o">,</span> <span class="n">dbService</span><span class="o">.</span><span class="na">findDataWithRowMapper</span><span class="o">());</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"findDataByParamMap: {}"</span><span class="o">,</span> <span class="n">dbService</span><span class="o">.</span><span class="na">findDataByParamMap</span><span class="o">(</span><span class="nc">Map</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"name"</span><span class="o">,</span> <span class="s">"author"</span><span class="o">)));</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"countByName: {}"</span><span class="o">,</span> <span class="n">dbService</span><span class="o">.</span><span class="na">countByName</span><span class="o">(</span><span class="s">"test"</span><span class="o">));</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="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">SpringApplication</span><span class="o">.</span><span class="na">run</span><span class="o">(</span><span class="nc">Application</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>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><summary type="html"><![CDATA[SpringBoot 3.2引入了新的 JdbcClient 用于数据库操作，JdbcClient对JdbcTemplate进行了封装，采用了 fluent API 的风格，可以进行链式调用。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/jdbc-client.jpg" /><media:content medium="image" url="https://springcamp.cn/assets/images/jdbc-client.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Spring Data Envers 支持有条件变动纪录的保存和查询</title><link href="https://springcamp.cn/spring-data-conditional-auditing/" rel="alternate" type="text/html" title="Spring Data Envers 支持有条件变动纪录的保存和查询" /><published>2023-10-15T08:20:00+00:00</published><updated>2023-10-15T08:20:00+00:00</updated><id>https://springcamp.cn/spring-data-conditional-auditing</id><content type="html" xml:base="https://springcamp.cn/spring-data-conditional-auditing/"><![CDATA[<p>数据审计是业务系统的一个基本能力，需要系统能够将关键数据的变动纪录都保存下来，并支持变动纪录的查询。</p>

<p>通过spring-data-envers可以很容易的实现数据变动纪录的保存和查询。</p>

<p>有些情况下，我们需要只保存满足特定条件的数据变动纪录，不满足条件的变动纪录不进行保存，例如只保存某个字段有值的变动纪录。</p>

<p>本文介绍支持有条件变动纪录的保存和查询的方法。</p>

<p>具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-data-envers-conditional">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-data-envers-conditional</a></p>

<h2 id="一概述">一、概述</h2>

<p>可以通过 spring-data-envers 很容易的实现变动纪录的保存和查询，只需要增加几个注解就可以。但是要实现有条件的变动纪录的保存和查询就需要进行一些复杂的处理。</p>

<h2 id="二使用-spring-data-envers">二、使用 spring-data-envers</h2>

<p>首先引入 spring-data-envers 依赖。</p>

<p>在 build.gradle 中增加一行代码:</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">implementation</span> <span class="s1">'org.springframework.data:spring-data-envers'</span>
</code></pre></div></div>

<p>在实体类上增加 Audited 注解：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Data</span>
<span class="nd">@Entity</span>
<span class="nd">@Audited</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">MyData</span> <span class="o">{</span>
    <span class="nd">@Id</span>
    <span class="nd">@GeneratedValue</span>
    <span class="kd">private</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">;</span>

    <span class="kd">private</span> <span class="nc">String</span> <span class="n">author</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Repository 扩展 RevisionRepository 方法：</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">MyDataRepository</span> <span class="kd">extends</span> <span class="nc">JpaRepository</span><span class="o">&lt;</span><span class="nc">MyData</span><span class="o">,</span> <span class="nc">Long</span><span class="o">&gt;,</span> <span class="nc">RevisionRepository</span><span class="o">&lt;</span><span class="nc">MyData</span><span class="o">,</span> <span class="nc">Long</span><span class="o">,</span> <span class="nc">Integer</span><span class="o">&gt;</span> <span class="o">{</span>
<span class="o">}</span>
</code></pre></div></div>

<p>通过以上3步操作，就添加好了变动纪录的保存功能，我们可以通过调用变动纪录查询方法确认变动纪录保存成功。</p>

<p>当 Repository 扩展 RevisionRepository 方法后，会有一个默认实现的 findRevisions 方法，我们可以直接调用：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">Revisions</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">,</span> <span class="nc">MyData</span><span class="o">&gt;</span> <span class="nf">findRevisions</span><span class="o">(</span><span class="nc">Long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">myDataRepository</span><span class="o">.</span><span class="na">findRevisions</span><span class="o">(</span><span class="n">id</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">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">run</span><span class="o">(</span><span class="nc">String</span><span class="o">...</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">MyData</span> <span class="n">myData</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MyData</span><span class="o">();</span>
        <span class="n">myData</span><span class="o">.</span><span class="na">setId</span><span class="o">(</span><span class="mi">1L</span><span class="o">);</span>
        <span class="n">myData</span><span class="o">.</span><span class="na">setAuthor</span><span class="o">(</span><span class="s">"test"</span><span class="o">);</span>
        <span class="n">dbService</span><span class="o">.</span><span class="na">saveData</span><span class="o">(</span><span class="n">myData</span><span class="o">);</span>
        <span class="n">dbService</span><span class="o">.</span><span class="na">findRevisions</span><span class="o">(</span><span class="n">myData</span><span class="o">.</span><span class="na">getId</span><span class="o">()).</span><span class="na">forEach</span><span class="o">(</span><span class="n">r</span> <span class="o">-&gt;</span> <span class="nc">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">"revision: "</span> <span class="o">+</span> <span class="n">r</span><span class="o">.</span><span class="na">toString</span><span class="o">()));</span>


        <span class="n">myData</span><span class="o">.</span><span class="na">setAuthor</span><span class="o">(</span><span class="s">"newAuthor"</span><span class="o">);</span>
        <span class="n">dbService</span><span class="o">.</span><span class="na">saveData</span><span class="o">(</span><span class="n">myData</span><span class="o">);</span>
        <span class="n">dbService</span><span class="o">.</span><span class="na">findRevisions</span><span class="o">(</span><span class="n">myData</span><span class="o">.</span><span class="na">getId</span><span class="o">()).</span><span class="na">forEach</span><span class="o">(</span><span class="n">r</span> <span class="o">-&gt;</span> <span class="nc">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">"revision: "</span> <span class="o">+</span> <span class="n">r</span><span class="o">.</span><span class="na">toString</span><span class="o">()));</span>
<span class="o">}</span>
</code></pre></div></div>

<p>执行完程序后，可以看到两次保存数据的操作都可以查询到对应的变动纪录，并且变动纪录还通过 revisionType 显示了是插入还是更新操作：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>revision: Revision 1 of entity MyData(id=1, author=test) - Revision metadata DefaultRevisionMetadata{entity=DefaultRevisionEntity(id = 1, revisionDate = Oct 15, 2023, 11:41:15 AM), revisionType=INSERT}
revision: Revision 2 of entity MyData(id=1, author=newAuthor) - Revision metadata DefaultRevisionMetadata{entity=DefaultRevisionEntity(id = 2, revisionDate = Oct 15, 2023, 11:41:16 AM), revisionType=UPDATE}
</code></pre></div></div>

<h2 id="三通过自定义-event-listener-实现有条件的变动纪录的保存">三、通过自定义 Event Listener 实现有条件的变动纪录的保存</h2>

<p>在进行数据变动时， Envers 通过监听事件来进行对应的处理，总共有以下几个监听事件：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">EventType</span><span class="o">.</span><span class="na">POST_INSERT</span>
<span class="nc">EventType</span><span class="o">.</span><span class="na">PRE_UPDATE</span>
<span class="nc">EventType</span><span class="o">.</span><span class="na">POST_UPDATE</span>
<span class="nc">EventType</span><span class="o">.</span><span class="na">POST_DELETE</span>
<span class="nc">EventType</span><span class="o">.</span><span class="na">POST_COLLECTION_RECREATE</span>
<span class="nc">EventType</span><span class="o">.</span><span class="na">PRE_COLLECTION_REMOVE</span>
<span class="nc">EventType</span><span class="o">.</span><span class="na">PRE_COLLECTION_UPDATE</span>
</code></pre></div></div>

<p>每个监听事件都对应着特定的 Listener ，在本文实例中，我们期望当 author 的值被更新为空时，不保存变动纪录，我们可以通过自定义 PRE_UPDATE 和 POST_UPDATE 的Listener来实现。</p>

<p>因为框架提供了默认的Listener，因此自定义 Listener 只需要扩展默认的Listener，并加入我们自己的特有逻辑就可以。</p>

<p>MyEnversPostUpdateEventListenerImpl ：</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">MyEnversPreUpdateEventListenerImpl</span> <span class="kd">extends</span> <span class="nc">EnversPreUpdateEventListenerImpl</span> <span class="o">{</span>

    <span class="kd">public</span> <span class="nf">MyEnversPreUpdateEventListenerImpl</span><span class="o">(</span><span class="nc">EnversService</span> <span class="n">enversService</span><span class="o">)</span> <span class="o">{</span>
        <span class="kd">super</span><span class="o">(</span><span class="n">enversService</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">onPreUpdate</span><span class="o">(</span><span class="nc">PreUpdateEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">event</span><span class="o">.</span><span class="na">getEntity</span><span class="o">()</span> <span class="k">instanceof</span> <span class="nc">MyData</span>
                <span class="o">&amp;&amp;</span> <span class="o">((</span><span class="nc">MyData</span><span class="o">)</span> <span class="n">event</span><span class="o">.</span><span class="na">getEntity</span><span class="o">()).</span><span class="na">getAuthor</span><span class="o">()</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="kc">false</span><span class="o">;</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="kd">super</span><span class="o">.</span><span class="na">onPreUpdate</span><span class="o">(</span><span class="n">event</span><span class="o">);</span>
    <span class="o">}</span>

<span class="o">}</span>
</code></pre></div></div>

<p>MyEnversPostUpdateEventListenerImpl:</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">MyEnversPostUpdateEventListenerImpl</span> <span class="kd">extends</span> <span class="nc">EnversPostUpdateEventListenerImpl</span> <span class="o">{</span>

    <span class="kd">public</span> <span class="nf">MyEnversPostUpdateEventListenerImpl</span><span class="o">(</span><span class="nc">EnversService</span> <span class="n">enversService</span><span class="o">)</span> <span class="o">{</span>
        <span class="kd">super</span><span class="o">(</span><span class="n">enversService</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">onPostUpdate</span><span class="o">(</span><span class="nc">PostUpdateEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">event</span><span class="o">.</span><span class="na">getEntity</span><span class="o">()</span> <span class="k">instanceof</span> <span class="nc">MyData</span> <span class="o">&amp;&amp;</span> <span class="o">((</span><span class="nc">MyData</span><span class="o">)</span> <span class="n">event</span><span class="o">.</span><span class="na">getEntity</span><span class="o">()).</span><span class="na">getAuthor</span><span class="o">()</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="kd">super</span><span class="o">.</span><span class="na">onPostUpdate</span><span class="o">(</span><span class="n">event</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>在自定义 Listener 中，我们增加了 对于 author 字段是否为空的判断逻辑。</p>

<h2 id="四自定义-event-listener-注册到系统中">四、自定义 Event Listener 注册到系统中</h2>

<p>自定义 Event Listener 完成后，我们还需要让框架执行我们自定义的 Listener， 而不是用默认的 Listener。</p>

<p>框架通过 EnversIntegrator 类注册的 Listener, 我们要做的是重新实现 EnversIntegrator , 在本实例中重新实现的类为 MyEnversIntegrator :</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">MyEnversIntegrator</span> <span class="kd">implements</span> <span class="nc">Integrator</span> <span class="o">{</span>
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">integrate</span><span class="o">(</span><span class="nc">Metadata</span> <span class="n">metadata</span><span class="o">,</span>
                          <span class="nc">BootstrapContext</span> <span class="n">bootstrapContext</span><span class="o">,</span>
                          <span class="nc">SessionFactoryImplementor</span> <span class="n">sessionFactory</span><span class="o">)</span> <span class="o">{</span>

        <span class="kd">final</span> <span class="nc">ServiceRegistry</span> <span class="n">serviceRegistry</span> <span class="o">=</span> <span class="n">sessionFactory</span><span class="o">.</span><span class="na">getServiceRegistry</span><span class="o">();</span>
        <span class="kd">final</span> <span class="nc">EnversService</span> <span class="n">enversService</span> <span class="o">=</span> <span class="n">serviceRegistry</span><span class="o">.</span><span class="na">getService</span><span class="o">(</span><span class="nc">EnversService</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>

        <span class="kd">final</span> <span class="nc">EventListenerRegistry</span> <span class="n">listenerRegistry</span> <span class="o">=</span> <span class="n">serviceRegistry</span><span class="o">.</span><span class="na">getService</span><span class="o">(</span><span class="nc">EventListenerRegistry</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
        <span class="n">listenerRegistry</span><span class="o">.</span><span class="na">addDuplicationStrategy</span><span class="o">(</span><span class="nc">EnversListenerDuplicationStrategy</span><span class="o">.</span><span class="na">INSTANCE</span><span class="o">);</span>

        <span class="k">if</span> <span class="o">(</span><span class="n">enversService</span><span class="o">.</span><span class="na">getEntitiesConfigurations</span><span class="o">().</span><span class="na">hasAuditedEntities</span><span class="o">())</span> <span class="o">{</span>
            <span class="n">listenerRegistry</span><span class="o">.</span><span class="na">appendListeners</span><span class="o">(</span>
                    <span class="nc">EventType</span><span class="o">.</span><span class="na">POST_DELETE</span><span class="o">,</span>
                    <span class="k">new</span> <span class="nf">EnversPostDeleteEventListenerImpl</span><span class="o">(</span><span class="n">enversService</span><span class="o">)</span>
            <span class="o">);</span>
            <span class="n">listenerRegistry</span><span class="o">.</span><span class="na">appendListeners</span><span class="o">(</span>
                    <span class="nc">EventType</span><span class="o">.</span><span class="na">POST_INSERT</span><span class="o">,</span>
                    <span class="k">new</span> <span class="nf">EnversPostInsertEventListenerImpl</span><span class="o">(</span><span class="n">enversService</span><span class="o">)</span>
            <span class="o">);</span>
            <span class="n">listenerRegistry</span><span class="o">.</span><span class="na">appendListeners</span><span class="o">(</span>
                    <span class="nc">EventType</span><span class="o">.</span><span class="na">PRE_UPDATE</span><span class="o">,</span>
                    <span class="k">new</span> <span class="nf">MyEnversPreUpdateEventListenerImpl</span><span class="o">(</span><span class="n">enversService</span><span class="o">)</span>
            <span class="o">);</span>
            <span class="n">listenerRegistry</span><span class="o">.</span><span class="na">appendListeners</span><span class="o">(</span>
                    <span class="nc">EventType</span><span class="o">.</span><span class="na">POST_UPDATE</span><span class="o">,</span>
                    <span class="k">new</span> <span class="nf">MyEnversPostUpdateEventListenerImpl</span><span class="o">(</span><span class="n">enversService</span><span class="o">)</span>
            <span class="o">);</span>
            <span class="n">listenerRegistry</span><span class="o">.</span><span class="na">appendListeners</span><span class="o">(</span>
                    <span class="nc">EventType</span><span class="o">.</span><span class="na">POST_COLLECTION_RECREATE</span><span class="o">,</span>
                    <span class="k">new</span> <span class="nf">EnversPostCollectionRecreateEventListenerImpl</span><span class="o">(</span><span class="n">enversService</span><span class="o">)</span>
            <span class="o">);</span>
            <span class="n">listenerRegistry</span><span class="o">.</span><span class="na">appendListeners</span><span class="o">(</span>
                    <span class="nc">EventType</span><span class="o">.</span><span class="na">PRE_COLLECTION_REMOVE</span><span class="o">,</span>
                    <span class="k">new</span> <span class="nf">EnversPreCollectionRemoveEventListenerImpl</span><span class="o">(</span><span class="n">enversService</span><span class="o">)</span>
            <span class="o">);</span>
            <span class="n">listenerRegistry</span><span class="o">.</span><span class="na">appendListeners</span><span class="o">(</span>
                    <span class="nc">EventType</span><span class="o">.</span><span class="na">PRE_COLLECTION_UPDATE</span><span class="o">,</span>
                    <span class="k">new</span> <span class="nf">EnversPreCollectionUpdateEventListenerImpl</span><span class="o">(</span><span class="n">enversService</span><span class="o">)</span>
            <span class="o">);</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">disintegrate</span><span class="o">(</span><span class="nc">SessionFactoryImplementor</span> <span class="n">sessionFactory</span><span class="o">,</span> <span class="nc">SessionFactoryServiceRegistry</span> <span class="n">serviceRegistry</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// nothing to do</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>通过代码可以发现，我们只是修改了 PRE_UPDATE 和 POST_UPDATE 注册的 Listener , 其它事件的 Listener 仍然用框架默认的。</p>

<p>最后我们需要把我们实现的 MyEnversIntegrator 放到 META-INF/services/org.hibernate.integrator.spi.Integrator 这个配置文件中。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cn.springcamp.springdata.envers.MyEnversIntegrator
</code></pre></div></div>

<h2 id="五确认有条件变动纪录的保存是否生效">五、确认有条件变动纪录的保存是否生效</h2>

<p>最后我们修改控制台打印程序，将 author 字段更新为 null 并保存，查看变动纪录里是否有这个更新操作的纪录。</p>

<p>增加保存代码 :</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// won't generate audit record when author is null</span>
<span class="n">myData</span><span class="o">.</span><span class="na">setAuthor</span><span class="o">(</span><span class="kc">null</span><span class="o">);</span>
<span class="n">dbService</span><span class="o">.</span><span class="na">saveData</span><span class="o">(</span><span class="n">myData</span><span class="o">);</span>
<span class="n">dbService</span><span class="o">.</span><span class="na">findRevisions</span><span class="o">(</span><span class="n">myData</span><span class="o">.</span><span class="na">getId</span><span class="o">()).</span><span class="na">forEach</span><span class="o">(</span><span class="n">r</span> <span class="o">-&gt;</span> <span class="nc">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">"revision: "</span> <span class="o">+</span> <span class="n">r</span><span class="o">.</span><span class="na">toString</span><span class="o">()));</span>
</code></pre></div></div>

<p>执行程序并观察控制台打印内容：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>revision: Revision 1 of entity MyData(id=1, author=test) - Revision metadata DefaultRevisionMetadata{entity=DefaultRevisionEntity(id = 1, revisionDate = Oct 15, 2023, 11:41:15 AM), revisionType=INSERT}
revision: Revision 2 of entity MyData(id=1, author=newAuthor) - Revision metadata DefaultRevisionMetadata{entity=DefaultRevisionEntity(id = 2, revisionDate = Oct 15, 2023, 11:41:16 AM), revisionType=UPDATE}
</code></pre></div></div>

<p>通过打印内容可以确认，author 字段更新为 null 的变动纪录没有被纪录，说明我们的处理是生效的。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><summary type="html"><![CDATA[数据审计是业务系统的一个基本能力，需要系统能够将关键数据的变动纪录都保存下来，并支持变动纪录的查询。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/data-audit.jpg" /><media:content medium="image" url="https://springcamp.cn/assets/images/data-audit.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Spring RestTempate 打印请求和响应内容日志</title><link href="https://springcamp.cn/spring-rest-template-log/" rel="alternate" type="text/html" title="Spring RestTempate 打印请求和响应内容日志" /><published>2023-05-03T08:20:00+00:00</published><updated>2023-05-03T08:20:00+00:00</updated><id>https://springcamp.cn/spring-rest-template-log</id><content type="html" xml:base="https://springcamp.cn/spring-rest-template-log/"><![CDATA[<p>系统中经常需要调用第三方接口实现业务功能，为了方便调试和定位问题，我们通常需要将接口调用参数和返回结果打印到日志文件中。在Spring项目中一般会用RestTemplate来调用第三方接口。
通过在RestTemplate调用过程中统一打印日志，可以保持代码的整洁，也可以统一日志格式，比在业务逻辑中到处打印接口调用日志要方便的多。</p>

<p>具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-rest-template-log">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-rest-template-log</a></p>

<h2 id="一概述">一、概述</h2>

<p>RestTemplate使用前需要先定义bean，在定义bean时可以通过指定<code class="language-plaintext highlighter-rouge">interceptors</code>来打印日志。</p>

<h2 id="二定义resttemplate的bean并指定interceptors">二、定义RestTemplate的bean，并指定interceptors</h2>

<p>RestTemplate的bean的定义在RestTemplateConfig类中实现。</p>

<p>RestTemplateConfig.java:</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="nc">RestTemplate</span> <span class="nf">restTemplate</span><span class="o">(</span><span class="nc">RestTemplateBuilder</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">requestFactory</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="k">new</span> <span class="nc">HttpComponentsClientHttpRequestFactory</span><span class="o">(</span><span class="n">httpClient</span><span class="o">()))</span>
                <span class="o">.</span><span class="na">interceptors</span><span class="o">(</span><span class="k">new</span> <span class="nc">CustomClientHttpRequestInterceptor</span><span class="o">())</span>
                <span class="o">.</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>其中 <code class="language-plaintext highlighter-rouge">interceptors</code> 方法用来指定我们自己实现的日志打印 interceptors 。</p>

<h2 id="三实现日志打印-interceptors">三、实现日志打印 interceptors</h2>

<p>自定义的interceptors需要实现 ClientHttpRequestInterceptor 这个 interface。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">static</span> <span class="kd">class</span> <span class="nc">CustomClientHttpRequestInterceptor</span> <span class="kd">implements</span> <span class="nc">ClientHttpRequestInterceptor</span> <span class="o">{</span>
    <span class="nd">@Override</span>
    <span class="nd">@NonNull</span>
    <span class="kd">public</span> <span class="nc">ClientHttpResponse</span> <span class="nf">intercept</span><span class="o">(</span><span class="nc">HttpRequest</span> <span class="n">request</span><span class="o">,</span> <span class="nd">@NonNull</span> <span class="kt">byte</span><span class="o">[]</span> <span class="n">bytes</span><span class="o">,</span> <span class="nd">@NonNull</span> <span class="nc">ClientHttpRequestExecution</span> <span class="n">execution</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"HTTP Method: {}, URI: {}, Headers: {}"</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">request</span><span class="o">.</span><span class="na">getURI</span><span class="o">(),</span> <span class="n">request</span><span class="o">.</span><span class="na">getHeaders</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="k">if</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="na">equals</span><span class="o">(</span><span class="nc">HttpMethod</span><span class="o">.</span><span class="na">POST</span><span class="o">))</span> <span class="o">{</span>
            <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"HTTP body: {}"</span><span class="o">,</span> <span class="k">new</span> <span class="nc">String</span><span class="o">(</span><span class="n">bytes</span><span class="o">,</span> <span class="nc">StandardCharsets</span><span class="o">.</span><span class="na">UTF_8</span><span class="o">));</span>
        <span class="o">}</span>

        <span class="nc">ClientHttpResponse</span> <span class="n">response</span> <span class="o">=</span> <span class="n">execution</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">bytes</span><span class="o">);</span>
        <span class="nc">ClientHttpResponse</span> <span class="n">responseWrapper</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">BufferingClientHttpResponseWrapper</span><span class="o">(</span><span class="n">response</span><span class="o">);</span>

        <span class="nc">String</span> <span class="n">body</span> <span class="o">=</span> <span class="nc">StreamUtils</span><span class="o">.</span><span class="na">copyToString</span><span class="o">(</span><span class="n">responseWrapper</span><span class="o">.</span><span class="na">getBody</span><span class="o">(),</span> <span class="nc">StandardCharsets</span><span class="o">.</span><span class="na">UTF_8</span><span class="o">);</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"RESPONSE body: {}"</span><span class="o">,</span> <span class="n">body</span><span class="o">);</span>

        <span class="k">return</span> <span class="n">responseWrapper</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>接口请求地址和请求参数的日志打印比较简单，将intercept方法中的参与打印到日志中即可。</p>

<p>返回结果的body的日志打印需要做一些特殊处理。</p>

<p><code class="language-plaintext highlighter-rouge">ClientHttpResponse response = execution.execute(request, bytes);</code> 这行代码我们拿到了返回结果，但是不能直接读取返回的数据。</p>

<p>因为返回结果中的<code class="language-plaintext highlighter-rouge">getBody()</code>方法返回的是 InputStream ，直接读取后，会导致后续的处理拿不到结果。</p>

<p>因此我们需要对返回execution.execute方法返回的结果进行包装，将返回结果放到自定义的BufferingClientHttpResponseWrapper类中。</p>

<p><code class="language-plaintext highlighter-rouge">ClientHttpResponse responseWrapper = new BufferingClientHttpResponseWrapper(response);</code></p>

<p>BufferingClientHttpResponseWrapper类会将body的数据复制到一个本地变量中，用于支持多次读取。</p>

<p>BufferingClientHttpResponseWrapper类的实现如下：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">static</span> <span class="kd">class</span> <span class="nc">BufferingClientHttpResponseWrapper</span> <span class="kd">implements</span> <span class="nc">ClientHttpResponse</span> <span class="o">{</span>

        <span class="kd">private</span> <span class="kd">final</span> <span class="nc">ClientHttpResponse</span> <span class="n">response</span><span class="o">;</span>
        <span class="kd">private</span> <span class="kt">byte</span><span class="o">[]</span> <span class="n">body</span><span class="o">;</span>

        <span class="nc">BufferingClientHttpResponseWrapper</span><span class="o">(</span><span class="nc">ClientHttpResponse</span> <span class="n">response</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">this</span><span class="o">.</span><span class="na">response</span> <span class="o">=</span> <span class="n">response</span><span class="o">;</span>
        <span class="o">}</span>

        <span class="nd">@NonNull</span>
        <span class="kd">public</span> <span class="nc">HttpStatusCode</span> <span class="nf">getStatusCode</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span>
            <span class="k">return</span> <span class="k">this</span><span class="o">.</span><span class="na">response</span><span class="o">.</span><span class="na">getStatusCode</span><span class="o">();</span>
        <span class="o">}</span>

        <span class="nd">@NonNull</span>
        <span class="kd">public</span> <span class="kt">int</span> <span class="nf">getRawStatusCode</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span>
            <span class="k">return</span> <span class="k">this</span><span class="o">.</span><span class="na">response</span><span class="o">.</span><span class="na">getRawStatusCode</span><span class="o">();</span>
        <span class="o">}</span>

        <span class="nd">@NonNull</span>
        <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getStatusText</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span>
            <span class="k">return</span> <span class="k">this</span><span class="o">.</span><span class="na">response</span><span class="o">.</span><span class="na">getStatusText</span><span class="o">();</span>
        <span class="o">}</span>

        <span class="nd">@NonNull</span>
        <span class="kd">public</span> <span class="nc">HttpHeaders</span> <span class="nf">getHeaders</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">response</span><span class="o">.</span><span class="na">getHeaders</span><span class="o">();</span>
        <span class="o">}</span>

        <span class="nd">@NonNull</span>
        <span class="kd">public</span> <span class="nc">InputStream</span> <span class="nf">getBody</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">IOException</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">body</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                <span class="k">this</span><span class="o">.</span><span class="na">body</span> <span class="o">=</span> <span class="nc">StreamUtils</span><span class="o">.</span><span class="na">copyToByteArray</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">response</span><span class="o">.</span><span class="na">getBody</span><span class="o">());</span>
            <span class="o">}</span>
            <span class="k">return</span> <span class="k">new</span> <span class="nf">ByteArrayInputStream</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">body</span><span class="o">);</span>
        <span class="o">}</span>

        <span class="kd">public</span> <span class="kt">void</span> <span class="nf">close</span><span class="o">()</span> <span class="o">{</span>
            <span class="k">this</span><span class="o">.</span><span class="na">response</span><span class="o">.</span><span class="na">close</span><span class="o">();</span>
        <span class="o">}</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>这个类主要是对 <code class="language-plaintext highlighter-rouge">getBody()</code> 进行了特殊处理，在方法调用时，通过 <code class="language-plaintext highlighter-rouge">treamUtils.copyToByteArray</code> 将body数据复制到本地变量中。</p>

<p>每次读取body时，都会从本地变量中读取，避免了第一次读取body后，后续再读取body会读不到数据的问题。</p>

<h2 id="四调用接口查看日志内容">四、调用接口查看日志内容</h2>

<p>我们在代码中模拟调用一个第三方接口 <code class="language-plaintext highlighter-rouge">http://someservice/foo</code> ，接口调用在 DemoController 类中实现：</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">"/demo/get"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">Object</span> <span class="nf">demoGet</span><span class="o">(</span><span class="nc">String</span> <span class="n">arg</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">restTemplate</span><span class="o">.</span><span class="na">postForObject</span><span class="o">(</span><span class="s">"http://someservice/foo"</span><span class="o">,</span> <span class="k">new</span> <span class="nc">BodyRequest</span><span class="o">(</span><span class="s">"test"</span><span class="o">),</span> <span class="nc">BodyRequest</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>在单元测试代码 <code class="language-plaintext highlighter-rouge">DemoApplicationTest</code> 中调用这个接口。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">String</span> <span class="n">resp</span> <span class="o">=</span> <span class="n">testRestTemplate</span><span class="o">.</span><span class="na">getForObject</span><span class="o">(</span><span class="s">"/demo/get?arg=test"</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
</code></pre></div></div>

<p>执行单元测试代码后，可以看到日志中打印的接口调用参数和返回结果：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HTTP Method: POST, URI: http://someservice/foo, Headers: [Accept:"application/json, application/*+json", Content-Type:"application/json", Content-Length:"15"]
HTTP body: {"arg1":"test"}
RESPONSE body: {"code": 200}
</code></pre></div></div>

<p>日志中打印的返回结果 <code class="language-plaintext highlighter-rouge">{"code": 200}</code> 是我们在单元测试中对 <code class="language-plaintext highlighter-rouge">http://someservice/foo</code> 接口mock的数据。</p>

<p>具体如何对第三方接口进行mock，可以参照 <a href="https://springcamp.cn/spring-boot-unit-test/">springboot单元测试技术</a> 这篇文章。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><summary type="html"><![CDATA[系统中经常需要调用第三方接口实现业务功能，为了方便调试和定位问题，我们通常需要将接口调用参数和返回结果打印到日志文件中。在Spring项目中一般会用RestTemplate来调用第三方接口。 通过在RestTemplate调用过程中统一打印日志，可以保持代码的整洁，也可以统一日志格式，比在业务逻辑中到处打印接口调用日志要方便的多。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/rest-log.png" /><media:content medium="image" url="https://springcamp.cn/assets/images/rest-log.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">简易规则引擎 easy-rules</title><link href="https://springcamp.cn/easy-rules/" rel="alternate" type="text/html" title="简易规则引擎 easy-rules" /><published>2023-03-26T09:50:00+00:00</published><updated>2023-03-26T09:50:00+00:00</updated><id>https://springcamp.cn/easy-rules</id><content type="html" xml:base="https://springcamp.cn/easy-rules/"><![CDATA[<p>合理的使用规则引擎可以极大的减少代码复杂度，提升代码可维护性。业界知名的开源规则引擎有Drools，功能丰富，但也比较庞大。在一些简单的场景中，我们只需要简易的规则引擎就能满足要求。
本文介绍一个小巧的规则引擎 <a href="https://github.com/j-easy/easy-rules">easy-rules</a>，作为一个lib库提供，支持spring的SPEL表达式，可以很好的集成在spring项目中。</p>

<p>具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-easy-rule">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-easy-rule</a></p>

<h2 id="一概述">一、概述</h2>

<p>通过将业务规则配置的配置文件中，可以精简代码，同时已于维护，当规则修改时，只需要修改配置文件即可。easy-rules是一个小巧的规则引擎，支持spring的SPEL表达式，同时还支持 Apache JEXL 表达式和 MVL 表达式。</p>

<h2 id="二项目中加入依赖">二、项目中加入依赖</h2>

<p>在项目的gradle中增加依赖关系。</p>

<p>build.gradle:</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">plugins</span> <span class="o">{</span>
    <span class="n">id</span> <span class="s1">'org.springframework.boot'</span> <span class="n">version</span> <span class="s1">'3.0.5'</span>
    <span class="n">id</span> <span class="s1">'io.spring.dependency-management'</span> <span class="n">version</span> <span class="s1">'1.1.0'</span>
    <span class="n">id</span> <span class="s1">'java'</span>
<span class="o">}</span>

<span class="n">group</span> <span class="o">=</span> <span class="s1">'cn.springcamp'</span>
<span class="n">version</span> <span class="o">=</span> <span class="s1">'0.0.1-SNAPSHOT'</span>
<span class="n">sourceCompatibility</span> <span class="o">=</span> <span class="s1">'17'</span>

<span class="n">configurations</span> <span class="o">{</span>
    <span class="n">compileOnly</span> <span class="o">{</span>
        <span class="n">extendsFrom</span> <span class="n">annotationProcessor</span>
    <span class="o">}</span>
    <span class="n">testCompileOnly</span> <span class="o">{</span>
        <span class="n">extendsFrom</span> <span class="n">testAnnotationProcessor</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="n">repositories</span> <span class="o">{</span>
    <span class="n">mavenCentral</span><span class="o">()</span>
<span class="o">}</span>

<span class="n">dependencies</span> <span class="o">{</span>
    <span class="n">implementation</span> <span class="s2">"org.springframework.boot:spring-boot-starter-json"</span>
    <span class="n">implementation</span> <span class="s1">'org.springframework.boot:spring-boot-starter-validation'</span>
    <span class="n">implementation</span> <span class="s1">'org.jeasy:easy-rules-core:4.1.0'</span>
    <span class="n">implementation</span> <span class="s1">'org.jeasy:easy-rules-spel:4.1.0'</span>
    <span class="n">implementation</span> <span class="s1">'org.jeasy:easy-rules-support:4.1.0'</span>
    <span class="n">annotationProcessor</span> <span class="s1">'org.projectlombok:lombok'</span>
    <span class="n">testAnnotationProcessor</span> <span class="s1">'org.projectlombok:lombok'</span>
    <span class="n">testImplementation</span> <span class="s2">"org.springframework.boot:spring-boot-starter-test"</span>
    <span class="n">testImplementation</span> <span class="s1">'org.junit.vintage:junit-vintage-engine'</span>
    <span class="n">testImplementation</span> <span class="s1">'org.junit.vintage:junit-vintage-engine'</span>
<span class="o">}</span>

<span class="n">dependencyManagement</span> <span class="o">{</span>
    <span class="n">imports</span> <span class="o">{</span>
        <span class="n">mavenBom</span> <span class="s2">"org.springframework.cloud:spring-cloud-dependencies:2022.0.1"</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="n">test</span> <span class="o">{</span>
    <span class="n">useJUnitPlatform</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="三配置文件">三、配置文件</h2>

<p>示例程序将业务规则放到配置文件中，业务规则配置文件(demo-rule.yml)代码:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">age</span><span class="nv"> </span><span class="s">rule"</span>
<span class="na">description</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>
<span class="na">priority</span><span class="pi">:</span> <span class="m">1</span>
<span class="na">condition</span><span class="pi">:</span> <span class="s2">"</span><span class="s">#person.getAdult()</span><span class="nv"> </span><span class="s">==</span><span class="nv"> </span><span class="s">false"</span>
<span class="na">actions</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">T(java.lang.System).out.println(</span><span class="se">\"</span><span class="s">Shop:</span><span class="nv"> </span><span class="s">Sorry,</span><span class="nv"> </span><span class="s">you</span><span class="nv"> </span><span class="s">are</span><span class="nv"> </span><span class="s">not</span><span class="nv"> </span><span class="s">allowed</span><span class="se">\"</span><span class="s">)"</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">#person.setAdult(true)"</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">#person.setAge(18)"</span>
<span class="nn">---</span>
<span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">alcohol</span><span class="nv"> </span><span class="s">rule"</span>
<span class="na">description</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>
<span class="na">priority</span><span class="pi">:</span> <span class="m">1</span>
<span class="na">condition</span><span class="pi">:</span> <span class="s2">"</span><span class="s">#person.getAdult()</span><span class="nv"> </span><span class="s">==</span><span class="nv"> </span><span class="s">true"</span>
<span class="na">actions</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">T(java.lang.System).out.println(</span><span class="se">\"</span><span class="s">Shop:</span><span class="nv"> </span><span class="s">you</span><span class="nv"> </span><span class="s">are</span><span class="nv"> </span><span class="s">now</span><span class="nv"> </span><span class="s">allowed</span><span class="se">\"</span><span class="s">)"</span>
</code></pre></div></div>

<p>配置文件中的规则通过 condition 进行配置，当满足规则时，会调用 actions 中配置的动作。
示例项目使用了spring的SPEL表达式进行规则配置，配置文件中配置了2个规则，第一个规则通过 <code class="language-plaintext highlighter-rouge">person</code> 这个spring bean中的getAdult()判断是否满足规则，满足规则时调用三个方法。</p>

<p>在spring-boot本身的配置文件中 application.yml 配置规则文件：</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">rule</span><span class="pi">:</span>
  <span class="na">skip-on-first-failed-rule</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">skip-on-first-applied-rule</span><span class="pi">:</span> <span class="no">false</span>
  <span class="na">skip-on-first-non-triggered-rule</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">rules</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">rule-id</span><span class="pi">:</span> <span class="s2">"</span><span class="s">demo"</span>
      <span class="na">rule-file-location</span><span class="pi">:</span> <span class="s2">"</span><span class="s">classpath:demo-rule.yml"</span>
</code></pre></div></div>

<h2 id="四代码中对规则引擎进行配置">四、代码中对规则引擎进行配置</h2>

<p>通过 <code class="language-plaintext highlighter-rouge">RuleEngineConfig</code> 这个spring的配置类对规则引擎进行配置：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Slf4j</span>
<span class="nd">@EnableConfigurationProperties</span><span class="o">(</span><span class="nc">RuleEngineConfigProperties</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="nd">@Configuration</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">RuleEngineConfig</span> <span class="kd">implements</span> <span class="nc">BeanFactoryAware</span> <span class="o">{</span>
    <span class="nd">@Autowired</span><span class="o">(</span><span class="n">required</span> <span class="o">=</span> <span class="kc">false</span><span class="o">)</span>
    <span class="kd">private</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">RuleListener</span><span class="o">&gt;</span> <span class="n">ruleListeners</span><span class="o">;</span>

    <span class="nd">@Autowired</span><span class="o">(</span><span class="n">required</span> <span class="o">=</span> <span class="kc">false</span><span class="o">)</span>
    <span class="kd">private</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">RulesEngineListener</span><span class="o">&gt;</span> <span class="n">rulesEngineListeners</span><span class="o">;</span>

    <span class="kd">private</span> <span class="nc">BeanFactory</span> <span class="n">beanFactory</span><span class="o">;</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">RulesEngineParameters</span> <span class="nf">rulesEngineParameters</span><span class="o">(</span><span class="nc">RuleEngineConfigProperties</span> <span class="n">properties</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">RulesEngineParameters</span> <span class="n">parameters</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">RulesEngineParameters</span><span class="o">();</span>
        <span class="n">parameters</span><span class="o">.</span><span class="na">setSkipOnFirstAppliedRule</span><span class="o">(</span><span class="n">properties</span><span class="o">.</span><span class="na">isSkipOnFirstAppliedRule</span><span class="o">());</span>
        <span class="n">parameters</span><span class="o">.</span><span class="na">setSkipOnFirstFailedRule</span><span class="o">(</span><span class="n">properties</span><span class="o">.</span><span class="na">isSkipOnFirstFailedRule</span><span class="o">());</span>
        <span class="n">parameters</span><span class="o">.</span><span class="na">setSkipOnFirstNonTriggeredRule</span><span class="o">(</span><span class="n">properties</span><span class="o">.</span><span class="na">isSkipOnFirstNonTriggeredRule</span><span class="o">());</span>
        <span class="k">return</span> <span class="n">parameters</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">RulesEngine</span> <span class="nf">rulesEngine</span><span class="o">(</span><span class="nc">RulesEngineParameters</span> <span class="n">rulesEngineParameters</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">DefaultRulesEngine</span> <span class="n">rulesEngine</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DefaultRulesEngine</span><span class="o">(</span><span class="n">rulesEngineParameters</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(!</span><span class="nc">CollectionUtils</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">(</span><span class="n">ruleListeners</span><span class="o">))</span> <span class="o">{</span>
            <span class="n">rulesEngine</span><span class="o">.</span><span class="na">registerRuleListeners</span><span class="o">(</span><span class="n">ruleListeners</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="k">if</span> <span class="o">(!</span><span class="nc">CollectionUtils</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">(</span><span class="n">rulesEngineListeners</span><span class="o">))</span> <span class="o">{</span>
            <span class="n">rulesEngine</span><span class="o">.</span><span class="na">registerRulesEngineListeners</span><span class="o">(</span><span class="n">rulesEngineListeners</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="n">rulesEngine</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">BeanResolver</span> <span class="nf">beanResolver</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">BeanFactoryResolver</span><span class="o">(</span><span class="n">beanFactory</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">RuleEngineTemplate</span> <span class="nf">ruleEngineTemplate</span><span class="o">(</span><span class="nc">RuleEngineConfigProperties</span> <span class="n">properties</span><span class="o">,</span> <span class="nc">RulesEngine</span> <span class="n">rulesEngine</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">RuleEngineTemplate</span> <span class="n">ruleEngineTemplate</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">RuleEngineTemplate</span><span class="o">();</span>
        <span class="n">ruleEngineTemplate</span><span class="o">.</span><span class="na">setBeanResolver</span><span class="o">(</span><span class="n">beanResolver</span><span class="o">());</span>
        <span class="n">ruleEngineTemplate</span><span class="o">.</span><span class="na">setProperties</span><span class="o">(</span><span class="n">properties</span><span class="o">);</span>
        <span class="n">ruleEngineTemplate</span><span class="o">.</span><span class="na">setRulesEngine</span><span class="o">(</span><span class="n">rulesEngine</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">ruleEngineTemplate</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">RuleListener</span> <span class="nf">defaultRuleListener</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">RuleListener</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">beforeEvaluate</span><span class="o">(</span><span class="nc">Rule</span> <span class="n">rule</span><span class="o">,</span> <span class="nc">Facts</span> <span class="n">facts</span><span class="o">)</span> <span class="o">{</span>
                <span class="k">return</span> <span class="kc">true</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">afterEvaluate</span><span class="o">(</span><span class="nc">Rule</span> <span class="n">rule</span><span class="o">,</span> <span class="nc">Facts</span> <span class="n">facts</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">b</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"-----------------afterEvaluate-----------------"</span><span class="o">);</span>
                <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="n">rule</span><span class="o">.</span><span class="na">getName</span><span class="o">()</span> <span class="o">+</span> <span class="n">rule</span><span class="o">.</span><span class="na">getDescription</span><span class="o">()</span> <span class="o">+</span> <span class="n">facts</span><span class="o">.</span><span class="na">toString</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">beforeExecute</span><span class="o">(</span><span class="nc">Rule</span> <span class="n">rule</span><span class="o">,</span> <span class="nc">Facts</span> <span class="n">facts</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"-----------------beforeExecute-----------------"</span><span class="o">);</span>
                <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="n">rule</span><span class="o">.</span><span class="na">getName</span><span class="o">()</span> <span class="o">+</span> <span class="n">rule</span><span class="o">.</span><span class="na">getDescription</span><span class="o">()</span> <span class="o">+</span> <span class="n">facts</span><span class="o">.</span><span class="na">toString</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">onSuccess</span><span class="o">(</span><span class="nc">Rule</span> <span class="n">rule</span><span class="o">,</span> <span class="nc">Facts</span> <span class="n">facts</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"-----------------onSuccess-----------------"</span><span class="o">);</span>
                <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="n">rule</span><span class="o">.</span><span class="na">getName</span><span class="o">()</span> <span class="o">+</span> <span class="n">rule</span><span class="o">.</span><span class="na">getDescription</span><span class="o">()</span> <span class="o">+</span> <span class="n">facts</span><span class="o">.</span><span class="na">toString</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">onFailure</span><span class="o">(</span><span class="nc">Rule</span> <span class="n">rule</span><span class="o">,</span> <span class="nc">Facts</span> <span class="n">facts</span><span class="o">,</span> <span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"-----------------onFailure-----------------"</span><span class="o">);</span>
                <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="n">rule</span><span class="o">.</span><span class="na">getName</span><span class="o">()</span> <span class="o">+</span> <span class="s">"----------"</span> <span class="o">+</span> <span class="n">rule</span><span class="o">.</span><span class="na">getDescription</span><span class="o">()</span> <span class="o">+</span> <span class="n">facts</span><span class="o">.</span><span class="na">toString</span><span class="o">()</span> <span class="o">+</span> <span class="n">e</span><span class="o">.</span><span class="na">toString</span><span class="o">());</span>
            <span class="o">}</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">setBeanFactory</span><span class="o">(</span><span class="nc">BeanFactory</span> <span class="n">beanFactory</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">BeansException</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">beanFactory</span> <span class="o">=</span> <span class="n">beanFactory</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>配置文件中配置了 <code class="language-plaintext highlighter-rouge">ruleEngineTemplate</code> 这个spring bean，通过ruleEngineTemplate触发规则引擎的执行。</p>

<h2 id="五执行规则引擎">五、执行规则引擎</h2>

<p><code class="language-plaintext highlighter-rouge">ruleEngineTemplate</code> 配置好后，我们可以在业务代码中执行规则引擎，处理配置文件中配置的业务规则：</p>

<p>最为演示，我们将规则引擎的执行代码放到了 Application 的 run 方法中，程序启动后立即执行规则引擎：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@SpringBootApplication</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Application</span> <span class="kd">implements</span> <span class="nc">CommandLineRunner</span> <span class="o">{</span>

    <span class="nd">@Autowired</span>
    <span class="nc">RuleEngineTemplate</span> <span class="n">ruleEngineTemplate</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="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">SpringApplication</span><span class="o">.</span><span class="na">run</span><span class="o">(</span><span class="nc">Application</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="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">run</span><span class="o">(</span><span class="nc">String</span><span class="o">...</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Person</span> <span class="n">person</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Person</span><span class="o">();</span>
        <span class="nc">Facts</span> <span class="n">facts</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Facts</span><span class="o">();</span>
        <span class="n">facts</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"person"</span><span class="o">,</span> <span class="n">person</span><span class="o">);</span>
        <span class="n">ruleEngineTemplate</span><span class="o">.</span><span class="na">fire</span><span class="o">(</span><span class="s">"demo"</span><span class="o">,</span> <span class="n">facts</span><span class="o">);</span>

    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>程序执行后可以看到控制台里打印了 <code class="language-plaintext highlighter-rouge">Shop: Sorry, you are not allowed</code>，这个内容对应的是我们在规则文件中的actions中配置的 <code class="language-plaintext highlighter-rouge">"T(java.lang.System).out.println(\"Shop: Sorry, you are not allowed\")"</code> ，说明规则成功执行了。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><summary type="html"><![CDATA[合理的使用规则引擎可以极大的减少代码复杂度，提升代码可维护性。业界知名的开源规则引擎有Drools，功能丰富，但也比较庞大。在一些简单的场景中，我们只需要简易的规则引擎就能满足要求。 本文介绍一个小巧的规则引擎 easy-rules，作为一个lib库提供，支持spring的SPEL表达式，可以很好的集成在spring项目中。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/rule_engine.png" /><media:content medium="image" url="https://springcamp.cn/assets/images/rule_engine.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">SpringCloudGateway 动态转发后端服务</title><link href="https://springcamp.cn/spring-cloud-gateway/" rel="alternate" type="text/html" title="SpringCloudGateway 动态转发后端服务" /><published>2023-02-19T09:50:00+00:00</published><updated>2023-02-19T09:50:00+00:00</updated><id>https://springcamp.cn/spring-cloud-gateway</id><content type="html" xml:base="https://springcamp.cn/spring-cloud-gateway/"><![CDATA[<p>API网关的核心功能是统一流量入口，实现路由转发，SpringCloudGateway是API网关开发的技术之一，此外比较流行的还有Kong和ApiSix，这2个都是基于OpenResty技术栈。
简单的路由转发可以通过SpringCloudGateway的配置文件实现，在一些业务场景种，会需要动态替换路由配置中的后端服务地址，单纯靠配置文件无法满足这种需求。
本文介绍一种将路由配置保存到数据库中，可以根据接口请求的特定条件，从数据库中动态读取后端服务地址，实现灵活转发。</p>

<p>具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-cloud-gateway">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-cloud-gateway</a></p>

<h2 id="一概述">一、概述</h2>

<p>通过把SpringCloudGateway的相关路由配置规则保存到数据库中，可以动态的灵活调整路由。在本文的实现中，我们通过请求header中的特定值，动态选择对应的后端服务地址。</p>

<h2 id="二项目中加入依赖">二、项目中加入依赖</h2>

<p>在项目的gradle中增加依赖关系。</p>

<p>build.gradle:</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">plugins</span> <span class="o">{</span>
    <span class="n">id</span> <span class="s1">'org.springframework.boot'</span> <span class="n">version</span> <span class="s1">'3.0.2'</span>
    <span class="n">id</span> <span class="s1">'io.spring.dependency-management'</span> <span class="n">version</span> <span class="s1">'1.1.0'</span>
    <span class="n">id</span> <span class="s1">'java'</span>
<span class="o">}</span>

<span class="n">group</span> <span class="o">=</span> <span class="s1">'cn.springcamp'</span>
<span class="n">version</span> <span class="o">=</span> <span class="s1">'0.0.1-SNAPSHOT'</span>
<span class="n">sourceCompatibility</span> <span class="o">=</span> <span class="s1">'17'</span>

<span class="n">configurations</span> <span class="o">{</span>
    <span class="n">compileOnly</span> <span class="o">{</span>
        <span class="n">extendsFrom</span> <span class="n">annotationProcessor</span>
    <span class="o">}</span>
    <span class="n">testCompileOnly</span> <span class="o">{</span>
        <span class="n">extendsFrom</span> <span class="n">testAnnotationProcessor</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="n">repositories</span> <span class="o">{</span>
    <span class="n">mavenCentral</span><span class="o">()</span>
<span class="o">}</span>

<span class="n">dependencies</span> <span class="o">{</span>
    <span class="n">implementation</span> <span class="s2">"org.springframework.boot:spring-boot-starter-json"</span>
    <span class="n">implementation</span> <span class="s1">'org.springframework.boot:spring-boot-starter-validation'</span>
    <span class="n">implementation</span> <span class="s1">'org.springframework.boot:spring-boot-starter-data-r2dbc'</span>
    <span class="n">implementation</span> <span class="s1">'org.springframework.cloud:spring-cloud-starter-gateway'</span>
    <span class="n">runtimeOnly</span> <span class="s1">'com.h2database:h2'</span>
    <span class="n">runtimeOnly</span> <span class="s1">'io.r2dbc:r2dbc-h2'</span>
    <span class="n">annotationProcessor</span> <span class="s1">'org.projectlombok:lombok'</span>
    <span class="n">testAnnotationProcessor</span> <span class="s1">'org.projectlombok:lombok'</span>
    <span class="n">testImplementation</span> <span class="s2">"org.springframework.boot:spring-boot-starter-test"</span>
    <span class="n">testImplementation</span> <span class="s1">'org.junit.vintage:junit-vintage-engine'</span>
    <span class="n">testImplementation</span> <span class="s1">'io.projectreactor:reactor-test'</span>
    <span class="n">testImplementation</span> <span class="s1">'com.h2database:h2'</span>
    <span class="n">testImplementation</span> <span class="s1">'io.r2dbc:r2dbc-h2'</span>
    <span class="n">testImplementation</span> <span class="s1">'org.junit.vintage:junit-vintage-engine'</span>
<span class="o">}</span>

<span class="n">dependencyManagement</span> <span class="o">{</span>
    <span class="n">imports</span> <span class="o">{</span>
        <span class="n">mavenBom</span> <span class="s2">"org.springframework.cloud:spring-cloud-dependencies:2022.0.1"</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="n">test</span> <span class="o">{</span>
    <span class="n">useJUnitPlatform</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>

<p>由于SpringCloudGateway基于SpringWebFlux技术构建，所以依赖中的数据库配置需要使用r2dbc 。</p>

<h2 id="三配置文件">三、配置文件</h2>

<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">r2dbc</span><span class="pi">:</span>
    <span class="na">url</span><span class="pi">:</span> <span class="s">r2dbc:h2:mem:///testdb?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</span>
    <span class="na">username</span><span class="pi">:</span> <span class="s">sa</span>
    <span class="na">password</span><span class="pi">:</span>
  <span class="na">cloud</span><span class="pi">:</span>
    <span class="na">gateway</span><span class="pi">:</span>
      <span class="na">routes</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">id</span><span class="pi">:</span> <span class="s">routeOne</span>
          <span class="na">predicates</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s">Path=/route1/**</span>
          <span class="na">uri</span><span class="pi">:</span> <span class="s">no://op</span>
          <span class="na">filters</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s">UriHostPlaceholderFilter=10001</span>
        <span class="pi">-</span> <span class="na">id</span><span class="pi">:</span> <span class="s">routeTwo</span>
          <span class="na">predicates</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s">Path=/route2/**</span>
          <span class="na">uri</span><span class="pi">:</span> <span class="s">no://op</span>
          <span class="na">filters</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s">UriHostPlaceholderFilter=10001</span>
</code></pre></div></div>

<p>配置文件中配置了2个路由，对应的接口地址路径分别是 <code class="language-plaintext highlighter-rouge">/route1/**</code> 和 <code class="language-plaintext highlighter-rouge">Path=/route2/**</code> ，路径中的 <code class="language-plaintext highlighter-rouge">***</code> 表示模糊匹配，只要是以 <code class="language-plaintext highlighter-rouge">/route1/</code> 为前缀的路径都可以被访问到。</p>

<p>后端服务地址配置了一个无意的地址: <code class="language-plaintext highlighter-rouge">uri: no://op</code> ，因为我们的处理逻辑会通过从数据库中读取配置来动态替换后端服务地址。</p>

<h2 id="四动态路由数据存储格式">四、动态路由数据存储格式</h2>

<p>我们通过 <code class="language-plaintext highlighter-rouge">ROUTE_FILTER_ENTITY</code> 这个数据库表来存储接口后端服务配置数据。表结构为：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="nv">"ROUTE_FILTER_ENTITY"</span>
<span class="p">(</span>
   <span class="n">id</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">)</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span>
   <span class="n">route_id</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">),</span>  <span class="c1">-- 路由ID，对应配置文件中的 ```id``` 配置项</span>
   <span class="n">code</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">),</span> <span class="c1">-- 接口请求header中的code参数的值</span>
   <span class="n">url</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">)</span> <span class="c1">-- 后端服务地址</span>
<span class="p">);</span>
</code></pre></div></div>

<p>当客户端访问 <code class="language-plaintext highlighter-rouge">/route1/test</code> 接口时，根据配置文件的路由配置，SpringCloudGateway 会命中 <code class="language-plaintext highlighter-rouge">id: routeOne</code> 这个路由规则，这个规则对应的后端服务地址是 <code class="language-plaintext highlighter-rouge">uri: no://op</code> ，并不是我们期望的真实后端服务地址。</p>

<p>因此，我们需要读取到真实的后端服务地址，并将请求转发到这个地址。跟据 routeId 和 接口请求header中的code参数的值，就可以从 ROUTE_FILTER_ENTITY 表中查到对应的后端服务地址 <code class="language-plaintext highlighter-rouge">url</code> 这个字段的值。</p>

<p>我们已经读取到了后端服务地址，还需要将请求转发到这个地址，下面介绍转发的方法。</p>

<h2 id="五后端服务动态转发">五、后端服务动态转发</h2>

<p>动态转发通过自定义 filter 的方式实现，自定义 filter 代码如下：</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">UriHostPlaceholderFilter</span> <span class="kd">extends</span> <span class="nc">AbstractGatewayFilterFactory</span><span class="o">&lt;</span><span class="nc">UriHostPlaceholderFilter</span><span class="o">.</span><span class="na">Config</span><span class="o">&gt;</span> <span class="o">{</span>
    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">RouteFilterRepository</span> <span class="n">routeFilterRepository</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">UriHostPlaceholderFilter</span><span class="o">()</span> <span class="o">{</span>
        <span class="kd">super</span><span class="o">(</span><span class="nc">Config</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="nf">shortcutFieldOrder</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">Collections</span><span class="o">.</span><span class="na">singletonList</span><span class="o">(</span><span class="s">"order"</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">GatewayFilter</span> <span class="nf">apply</span><span class="o">(</span><span class="nc">Config</span> <span class="n">config</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">OrderedGatewayFilter</span><span class="o">((</span><span class="n">exchange</span><span class="o">,</span> <span class="n">chain</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="o">{</span>
            <span class="nc">String</span> <span class="n">code</span> <span class="o">=</span> <span class="n">exchange</span><span class="o">.</span><span class="na">getRequest</span><span class="o">().</span><span class="na">getHeaders</span><span class="o">().</span><span class="na">getOrDefault</span><span class="o">(</span><span class="s">"code"</span><span class="o">,</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;()).</span><span class="na">stream</span><span class="o">().</span><span class="na">findFirst</span><span class="o">().</span><span class="na">orElse</span><span class="o">(</span><span class="s">""</span><span class="o">);</span>
            <span class="nc">String</span> <span class="n">routeId</span> <span class="o">=</span> <span class="n">exchange</span><span class="o">.</span><span class="na">getAttribute</span><span class="o">(</span><span class="no">GATEWAY_PREDICATE_MATCHED_PATH_ROUTE_ID_ATTR</span><span class="o">);</span>
            <span class="k">if</span> <span class="o">(</span><span class="nc">StringUtils</span><span class="o">.</span><span class="na">hasText</span><span class="o">(</span><span class="n">code</span><span class="o">))</span> <span class="o">{</span>
                <span class="nc">String</span> <span class="n">newurl</span><span class="o">;</span>
                <span class="k">try</span> <span class="o">{</span>
                    <span class="n">newurl</span> <span class="o">=</span> <span class="n">routeFilterRepository</span><span class="o">.</span><span class="na">findByRouteIdAndCode</span><span class="o">(</span><span class="n">routeId</span><span class="o">,</span> <span class="n">code</span><span class="o">).</span><span class="na">toFuture</span><span class="o">().</span><span class="na">get</span><span class="o">().</span><span class="na">getUrl</span><span class="o">();</span>
                <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">InterruptedException</span> <span class="o">|</span> <span class="nc">ExecutionException</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="k">if</span> <span class="o">(</span><span class="nc">StringUtils</span><span class="o">.</span><span class="na">hasText</span><span class="o">(</span><span class="n">exchange</span><span class="o">.</span><span class="na">getRequest</span><span class="o">().</span><span class="na">getURI</span><span class="o">().</span><span class="na">getQuery</span><span class="o">()))</span> <span class="o">{</span>
                    <span class="n">newurl</span> <span class="o">=</span> <span class="n">newurl</span> <span class="o">+</span> <span class="s">"?"</span> <span class="o">+</span> <span class="n">exchange</span><span class="o">.</span><span class="na">getRequest</span><span class="o">().</span><span class="na">getURI</span><span class="o">().</span><span class="na">getQuery</span><span class="o">();</span>
                <span class="o">}</span>
                <span class="no">URI</span> <span class="n">newUri</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">newUri</span> <span class="o">=</span> <span class="k">new</span> <span class="no">URI</span><span class="o">(</span><span class="n">newurl</span><span class="o">);</span>
                <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">URISyntaxException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
                    <span class="n">log</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="s">"uri error"</span><span class="o">,</span> <span class="n">e</span><span class="o">);</span>
                <span class="o">}</span>

                <span class="n">exchange</span><span class="o">.</span><span class="na">getAttributes</span><span class="o">().</span><span class="na">put</span><span class="o">(</span><span class="no">GATEWAY_REQUEST_URL_ATTR</span><span class="o">,</span> <span class="n">newUri</span><span class="o">);</span>
            <span class="o">}</span>
            <span class="k">return</span> <span class="n">chain</span><span class="o">.</span><span class="na">filter</span><span class="o">(</span><span class="n">exchange</span><span class="o">);</span>
        <span class="o">},</span> <span class="n">config</span><span class="o">.</span><span class="na">getOrder</span><span class="o">());</span>
    <span class="o">}</span>

    <span class="nd">@Data</span>
    <span class="nd">@NoArgsConstructor</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">Config</span> <span class="o">{</span>
        <span class="kd">private</span> <span class="kt">int</span> <span class="n">order</span><span class="o">;</span>

        <span class="kd">public</span> <span class="nf">Config</span><span class="o">(</span><span class="kt">int</span> <span class="n">order</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">this</span><span class="o">.</span><span class="na">order</span> <span class="o">=</span> <span class="n">order</span><span class="o">;</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>通过扩展 AbstractGatewayFilterFactory 类，我们自定义了 UriHostPlaceholderFilter 这个 filter 。</p>

<p>代码的核心逻辑在 apply 方法中。</p>

<p>首先通过 <code class="language-plaintext highlighter-rouge">String code = exchange.getRequest().getHeaders().getOrDefault("code", new ArrayList&lt;&gt;()).stream().findFirst().orElse("")</code> 可以获取到接口请求 header 中 code 这个参数的值。</p>

<p>再通过 <code class="language-plaintext highlighter-rouge">String routeId = exchange.getAttribute(GATEWAY_PREDICATE_MATCHED_PATH_ROUTE_ID_ATTR)</code> 可以获取到 routeId 。</p>

<p>最后通过 <code class="language-plaintext highlighter-rouge">newurl = routeFilterRepository.findByRouteIdAndCode(routeId, code).toFuture().get().getUrl()</code> 就可以从数据库中读取到配置好的后端服务地址。</p>

<p>拿到后端服务地址后， 通过调用 <code class="language-plaintext highlighter-rouge">exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, newUri);</code> 将请求转发到对应的地址。</p>

<h2 id="六单元测试">六、单元测试</h2>

<p>在单元测试代码中，我们预置了一条后端服务动态配置数据：</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">insert</span> <span class="k">into</span> <span class="n">ROUTE_FILTER_ENTITY</span> <span class="k">values</span><span class="p">(</span><span class="s1">'1'</span><span class="p">,</span><span class="s1">'routeOne'</span><span class="p">,</span><span class="s1">'alpha'</span><span class="p">,</span><span class="s1">'http://httpbin.org/anything'</span><span class="p">)</span>
</code></pre></div></div>

<p>然后模拟请求 <code class="language-plaintext highlighter-rouge">/route1/test?a=test</code> 这个接口，根据我们的配置，请求会被转发到 <code class="language-plaintext highlighter-rouge">http://httpbin.org/anything</code> 。</p>

<p>执行单元测试后，可以从日志中发现，接口返回的数据是 http://httpbin.org/anything 这个后端服务返回的数据。</p>

<p>当我们希望调整后端服务地址时，只需要把 ROUTE_FILTER_ENTITY 表中的这条配置数据中的 url 字段改成其它的任何服务地址即可，大大增加了程序的灵活度。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="SpringCloudGateway" /><category term="featured" /><summary type="html"><![CDATA[API网关的核心功能是统一流量入口，实现路由转发，SpringCloudGateway是API网关开发的技术之一，此外比较流行的还有Kong和ApiSix，这2个都是基于OpenResty技术栈。 简单的路由转发可以通过SpringCloudGateway的配置文件实现，在一些业务场景种，会需要动态替换路由配置中的后端服务地址，单纯靠配置文件无法满足这种需求。 本文介绍一种将路由配置保存到数据库中，可以根据接口请求的特定条件，从数据库中动态读取后端服务地址，实现灵活转发。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/springcloudgateway.jpg" /><media:content medium="image" url="https://springcamp.cn/assets/images/springcloudgateway.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Spring Boot 3的AOT（GraalVM Native Image）应用开发</title><link href="https://springcamp.cn/spring-native/" rel="alternate" type="text/html" title="Spring Boot 3的AOT（GraalVM Native Image）应用开发" /><published>2022-11-26T09:50:00+00:00</published><updated>2022-11-26T09:50:00+00:00</updated><id>https://springcamp.cn/spring-native</id><content type="html" xml:base="https://springcamp.cn/spring-native/"><![CDATA[<p>GraalVM Native Images是一个利用AOT(Ahead-of-Time)技术把java程序直接编译成可执行程序的编译工具，编译出来的程序在运行时不再依赖JRE，同时启动速度快，资源消耗低，这对传统java程序来说都是极大的优势。同时云原生应用来说，GraalVM Native Images编译生成的程序体积很小，非常适合云原生环境，目前由于传统java程序生成的镜像中需要包含一个体积很大的JRE或JDK而经常被人诟病。
Spring Boot从3.0版本开始支持AOT技术。
具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-native">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-native</a></p>

<h2 id="一概述">一、概述</h2>

<p>Spring Boot 3.0 仍然支持传统的开发方式，既编译生成jar包，通过JRE来执行，在此基础上，通过调整编译方式，可以编译生成直接运行的可执行程序，Spring AOT与传统应用的区别包括：</p>

<ol>
  <li>程序运行时动态调整的资源无法直接使用，例如反射、动态代理等，需要在代码中通过Hint为编译器指定</li>
  <li>应用的classpath在编译后就固定了，不能动态调整</li>
  <li>类不会延迟加载（lazy loading），应用启动时一次性加载完成</li>
  <li>部分java切面（AOP）技术不支持</li>
</ol>

<h2 id="二项目中加入依赖">二、项目中加入依赖</h2>

<p>在项目的gradle中增加依赖关系。</p>

<p>build.gradle:</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">plugins</span> <span class="o">{</span>
    <span class="n">id</span> <span class="s1">'org.springframework.boot'</span> <span class="n">version</span> <span class="s1">'3.0.0'</span>
    <span class="n">id</span> <span class="s1">'io.spring.dependency-management'</span> <span class="n">version</span> <span class="s1">'1.1.0'</span>
    <span class="n">id</span> <span class="s1">'org.graalvm.buildtools.native'</span> <span class="n">version</span> <span class="s1">'0.9.22'</span>
    <span class="n">id</span> <span class="s1">'java'</span>
<span class="o">}</span>

<span class="n">group</span> <span class="o">=</span> <span class="s1">'cn.springcamp'</span>
<span class="n">version</span> <span class="o">=</span> <span class="s1">'0.0.1-SNAPSHOT'</span>
<span class="n">sourceCompatibility</span> <span class="o">=</span> <span class="s1">'17'</span>

<span class="n">configurations</span> <span class="o">{</span>
    <span class="n">compileOnly</span> <span class="o">{</span>
        <span class="n">extendsFrom</span> <span class="n">annotationProcessor</span>
    <span class="o">}</span>
    <span class="n">testCompileOnly</span> <span class="o">{</span>
        <span class="n">extendsFrom</span> <span class="n">testAnnotationProcessor</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="n">repositories</span> <span class="o">{</span>
    <span class="n">mavenCentral</span><span class="o">()</span>
<span class="o">}</span>

<span class="n">dependencies</span> <span class="o">{</span>
    <span class="n">implementation</span> <span class="s1">'org.springframework.boot:spring-boot-starter-web'</span>
    <span class="n">implementation</span> <span class="s1">'org.springframework.boot:spring-boot-starter-data-jpa'</span>
    <span class="n">implementation</span> <span class="s1">'com.h2database:h2'</span>
    <span class="n">annotationProcessor</span> <span class="s1">'org.projectlombok:lombok'</span>
    <span class="n">testAnnotationProcessor</span> <span class="s1">'org.projectlombok:lombok'</span>
    <span class="n">testImplementation</span> <span class="s1">'org.springframework.boot:spring-boot-starter-test'</span>
<span class="o">}</span>

<span class="n">test</span> <span class="o">{</span>
    <span class="n">useJUnitPlatform</span><span class="o">()</span>
<span class="o">}</span>
</code></pre></div></div>

<p>与传统Spring Boot应用相比，gradle文件中增加了 org.graalvm.buildtools.native 这个plugin，其它的没有区别。</p>

<p>由于 org.graalvm.buildtools.native 这个plugin没有发布到 Gradle Plugin Portal 中，<a href="https://graalvm.github.io/native-build-tools/latest/gradle-plugin.html">参照 https://graalvm.github.io/native-build-tools/latest/gradle-plugin.html</a>，因此需要在 settings.gradle 指定仓库地址：</p>

<p>settings.gradle</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">pluginManagement</span> <span class="o">{</span>
    <span class="n">repositories</span> <span class="o">{</span>
        <span class="n">mavenCentral</span><span class="o">()</span>
        <span class="n">gradlePluginPortal</span><span class="o">()</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="三主要程序代码">三、主要程序代码</h2>

<p>示例程序提供了一个rest接口，该接口从数据库中读取数据。为了便于演示，使用H2数据库。</p>

<p>Application程序代码:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@RestController</span>
<span class="nd">@SpringBootApplication</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Application</span> <span class="o">{</span>
    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">DbService</span> <span class="n">dbService</span><span class="o">;</span>

    <span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">"/hello"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">DemoData</span> <span class="nf">hello</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">dbService</span><span class="o">.</span><span class="na">hello</span><span class="o">();</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="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">SpringApplication</span><span class="o">.</span><span class="na">run</span><span class="o">(</span><span class="nc">Application</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>DbService代码:</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">DbService</span> <span class="o">{</span>
    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">TestDataRepository</span> <span class="n">testDataRepository</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nc">DemoData</span> <span class="nf">hello</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">DemoData</span> <span class="n">demoData</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DemoData</span><span class="o">();</span>
        <span class="n">demoData</span> <span class="o">=</span> <span class="n">testDataRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">demoData</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">demoData</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>由于程序中没有使用反射，所以代码跟传统程序没有什么区别。</p>

<h2 id="四编译-native-image">四、编译 Native Image</h2>

<p>Spring Boot 编译 Native Image 支持2种方式，一种通过Docker进行编译，需要本地安装Docker。另外一种是用本地的编译环境进行编译，需要安装Visual Studio。
由于第一种方式比较简单，除了要安装Docker外没有很复杂的操作，本文只介绍第二种方式。</p>

<h3 id="41-安装编译环境">4.1 安装编译环境</h3>

<p>需要安装 GraalVM 和 Visual Studio 两个编译工具。
GraalVM可以直接下载安装，<a href="https://www.graalvm.org/downloads/">下载地址</a> ，也可以通过 <a href="https://scoop.sh/">Scoop</a> 进行安装。
Visual Studio 需要下载安装，由于Visual Studio体积比较大，也可以只安装 <a href="https://aka.ms/vs/17/release/vs_BuildTools.exe">Visual Studio Build Tools</a></p>

<h3 id="42-执行编译命令">4.2 执行编译命令</h3>

<p>由于windows命令行工具有命令长度限制，因此编译命令不能在windows命令行工具中直接执行（包括powershell和cmd），需要在安装好的Visual Studio命令行工具（x64 Native Tools Command Prompt for VS 2022）中执行。</p>

<p>执行命令</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gradle nativeCompile
</code></pre></div></div>

<p>编译生成的可执行程序在当前工程目录的 <code class="language-plaintext highlighter-rouge">build\native\nativeCompile</code> 目录中，可以看到一个与工程名相同的以exe后缀结尾的文件。
直接运行该文件，就能体验到java程序的启动速度竟然能如此之快。</p>

<p>传统应用启动速度：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cn.springcamp.springnative.Application   : Started Application in 2.927 seconds (process running for 3.642)
</code></pre></div></div>

<p>native应用启动速度：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cn.springcamp.springnative.Application   : Started Application in 0.134 seconds (process running for 0.141)
</code></pre></div></div>

<p>启动速度从 3.642 提升到了 0.141 秒。</p>

<p>启动速度虽然快了，但是编译耗时也多了不少，这是一个缺点。</p>

<h2 id="五单元测试">五、单元测试</h2>

<p>传统的Spring Boot单元测试技术仍然可以使用。 <a href="https://springcamp.cn/spring-boot-unit-test/">Spring Boot单元测试技术</a>在这篇文章中有专门介绍。
需要注意的是 Spring Native不支持JUnit4，需要使用JUnit5。</p>

<p>单元测试代码：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Slf4j</span>
<span class="nd">@ExtendWith</span><span class="o">(</span><span class="nc">SpringExtension</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="nd">@SpringBootTest</span><span class="o">(</span><span class="n">webEnvironment</span> <span class="o">=</span> <span class="nc">SpringBootTest</span><span class="o">.</span><span class="na">WebEnvironment</span><span class="o">.</span><span class="na">RANDOM_PORT</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ApplicationTest</span> <span class="o">{</span>
    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">TestRestTemplate</span> <span class="n">testRestTemplate</span><span class="o">;</span>

    <span class="nd">@Test</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">testHello</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">resp</span> <span class="o">=</span> <span class="n">testRestTemplate</span><span class="o">.</span><span class="na">getForObject</span><span class="o">(</span><span class="s">"/hello"</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"hello result : {}"</span> <span class="o">+</span> <span class="n">resp</span><span class="o">);</span>
        <span class="n">assertThat</span><span class="o">(</span><span class="n">resp</span><span class="o">,</span> <span class="n">is</span><span class="o">(</span><span class="s">"{\"id\":1}"</span><span class="o">));</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>通过传统单元测试技术可以验证代码业务逻辑是正常的，对于编译成 Native Image 后程序是否还能正常运行，传统单元测试技术保证不了，需要进一步使用 Native Image 单元测试。</p>

<p>Native Image 单元测试通过以下命令执行：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gradle nativeTest
</code></pre></div></div>

<p>该命令会首先把应用编译成 Native Image 可执行程序，再跑单元测试用例。由于  Native Image 编译相比传统应用耗时要长很多，所以先通过传统的Spring Boot单元测试技术保证代码业务逻辑正常后，再使用 Native Image 单元测试命令，减少整个开发流程的耗时。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="GraalVM Native" /><summary type="html"><![CDATA[GraalVM Native Images是一个利用AOT(Ahead-of-Time)技术把java程序直接编译成可执行程序的编译工具，编译出来的程序在运行时不再依赖JRE，同时启动速度快，资源消耗低，这对传统java程序来说都是极大的优势。同时云原生应用来说，GraalVM Native Images编译生成的程序体积很小，非常适合云原生环境，目前由于传统java程序生成的镜像中需要包含一个体积很大的JRE或JDK而经常被人诟病。 Spring Boot从3.0版本开始支持AOT技术。 具体的代码参照 示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-native]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/spring-native.jpg" /><media:content medium="image" url="https://springcamp.cn/assets/images/spring-native.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">ElasticSearch新版JavaClient使用简介</title><link href="https://springcamp.cn/elasticsearch-javaclient/" rel="alternate" type="text/html" title="ElasticSearch新版JavaClient使用简介" /><published>2022-04-17T09:50:00+00:00</published><updated>2022-04-17T09:50:00+00:00</updated><id>https://springcamp.cn/elasticsearch-javaclient</id><content type="html" xml:base="https://springcamp.cn/elasticsearch-javaclient/"><![CDATA[<p>ElasticSearch在7.17版本之前使用的java客户端是Java REST Client，但是从7.17版本开始，官方将Java REST Client标记为弃用（deprecated），推荐使用新版Java Client。
本文介绍新版ElasticSearch Java Client的基本用法。
具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/elasticsearch-javaclient">示例项目 https://github.com/qihaiyan/springcamp/tree/main/elasticsearch-javaclient</a></p>

<h2 id="一概述">一、概述</h2>

<p>Elasticsearch 7.17 版本新增的Java API Client具有以下优点：</p>

<ol>
  <li>强类型</li>
  <li>同步和异步调用</li>
  <li>流式和函数式调用</li>
  <li>与Jackson无缝集成</li>
  <li>封装了连接池、重试、json序列化等通用能力</li>
</ol>

<h2 id="二项目中加入依赖">二、项目中加入依赖</h2>

<p>在项目的gradle或maven中增加依赖关系。</p>

<p>Gradle:</p>

<div class="language-gradle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">dependencies</span> <span class="o">{</span>
    <span class="n">implementation</span> <span class="s1">'co.elastic.clients:elasticsearch-java:8.1.2'</span>
    <span class="n">implementation</span> <span class="s1">'com.fasterxml.jackson.core:jackson-databind:2.12.3'</span>

    <span class="c1">// Needed only if you use the spring-dependency-management</span>
    <span class="c1">// and spring-boot Gradle plugins</span>
    <span class="n">implementation</span> <span class="s1">'jakarta.json:jakarta.json-api:2.0.1'</span> 
<span class="o">}</span>
</code></pre></div></div>

<p>Maven:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;project&gt;</span>
  <span class="nt">&lt;dependencies&gt;</span>

    <span class="nt">&lt;dependency&gt;</span>
      <span class="nt">&lt;groupId&gt;</span>co.elastic.clients<span class="nt">&lt;/groupId&gt;</span>
      <span class="nt">&lt;artifactId&gt;</span>elasticsearch-java<span class="nt">&lt;/artifactId&gt;</span>
      <span class="nt">&lt;version&gt;</span>8.1.2<span class="nt">&lt;/version&gt;</span>
    <span class="nt">&lt;/dependency&gt;</span>

    <span class="nt">&lt;dependency&gt;</span>
      <span class="nt">&lt;groupId&gt;</span>com.fasterxml.jackson.core<span class="nt">&lt;/groupId&gt;</span>
      <span class="nt">&lt;artifactId&gt;</span>jackson-databind<span class="nt">&lt;/artifactId&gt;</span>
      <span class="nt">&lt;version&gt;</span>2.12.3<span class="nt">&lt;/version&gt;</span>
    <span class="nt">&lt;/dependency&gt;</span>

    <span class="c">&lt;!-- Needed only if you use the spring-boot Maven plugin --&gt;</span>
    <span class="nt">&lt;dependency&gt;</span> 
      <span class="nt">&lt;groupId&gt;</span>jakarta.json<span class="nt">&lt;/groupId&gt;</span>
      <span class="nt">&lt;artifactId&gt;</span>jakarta.json-api<span class="nt">&lt;/artifactId&gt;</span>
      <span class="nt">&lt;version&gt;</span>2.0.1<span class="nt">&lt;/version&gt;</span>
    <span class="nt">&lt;/dependency&gt;</span>

  <span class="nt">&lt;/dependencies&gt;</span>
<span class="nt">&lt;/project&gt;</span>
</code></pre></div></div>

<p>jakarta.json这个包的引入是为了解决springboot项目的兼容性问题，详见<a href="https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/installation.html#spring-jakarta-json">官方文档</a></p>

<h2 id="三javaclient的初始化">三、JavaClient的初始化</h2>

<p>Java API Client包含三部分：</p>

<ol>
  <li>java客户端对应的类是ElasticsearchClient</li>
  <li>一个JSON object mapper用于数据的序列化和反序列化</li>
  <li>底层Transport通信</li>
</ol>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nc">RestClient</span> <span class="n">restClient</span> <span class="o">=</span> <span class="nc">RestClient</span><span class="o">.</span><span class="na">builder</span><span class="o">(</span><span class="k">new</span> <span class="nc">HttpHost</span><span class="o">(</span><span class="s">"localhost"</span><span class="o">,</span> <span class="mi">9200</span><span class="o">)).</span><span class="na">build</span><span class="o">();</span>
    <span class="nc">ElasticsearchTransport</span> <span class="n">transport</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">RestClientTransport</span><span class="o">(</span><span class="n">restClient</span><span class="o">,</span> <span class="k">new</span> <span class="nc">JacksonJsonpMapper</span><span class="o">());</span>
    <span class="n">elasticsearchClient</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ElasticsearchClient</span><span class="o">(</span><span class="n">transport</span><span class="o">);</span>
</code></pre></div></div>

<h2 id="三elasticsearch的基本操作">三、ElasticSearch的基本操作</h2>

<p>1.保存数据，通过Java Client的自动序列化能力，我们可以直接把对象传递给Java Client，无需再手动处理ElasticSearch中json数据的序列化。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nc">DemoDomain</span> <span class="n">record</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DemoDomain</span><span class="o">();</span>
    <span class="n">record</span><span class="o">.</span><span class="na">setId</span><span class="o">(</span><span class="s">"1"</span><span class="o">);</span>
    <span class="n">record</span><span class="o">.</span><span class="na">setName</span><span class="o">(</span><span class="s">"test"</span><span class="o">);</span>

    <span class="nc">IndexRequest</span><span class="o">&lt;</span><span class="nc">DemoDomain</span><span class="o">&gt;</span> <span class="n">indexRequest</span> <span class="o">=</span> <span class="nc">IndexRequest</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">b</span> <span class="o">-&gt;</span> <span class="n">b</span>
            <span class="o">.</span><span class="na">index</span><span class="o">(</span><span class="no">MY_INDEX</span><span class="o">)</span>
            <span class="o">.</span><span class="na">id</span><span class="o">(</span><span class="n">record</span><span class="o">.</span><span class="na">getId</span><span class="o">())</span>
            <span class="o">.</span><span class="na">document</span><span class="o">(</span><span class="n">record</span><span class="o">)</span>
            <span class="o">.</span><span class="na">refresh</span><span class="o">(</span><span class="nc">Refresh</span><span class="o">.</span><span class="na">True</span><span class="o">));</span>  <span class="c1">// Make it visible for search</span>

    <span class="n">elasticsearchClient</span><span class="o">.</span><span class="na">index</span><span class="o">(</span><span class="n">indexRequest</span><span class="o">);</span>
</code></pre></div></div>

<p>2.查询所有数据，同样json数据的反序列化也有Java Client自动处理，同时函数式编程使得代码很简洁。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nc">SearchRequest</span> <span class="n">searchRequest</span> <span class="o">=</span> <span class="nc">SearchRequest</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">s</span> <span class="o">-&gt;</span> <span class="n">s</span>
                <span class="o">.</span><span class="na">index</span><span class="o">(</span><span class="no">MY_INDEX</span><span class="o">)</span>
                <span class="o">.</span><span class="na">query</span><span class="o">(</span><span class="n">q</span> <span class="o">-&gt;</span> <span class="n">q</span>
                        <span class="o">.</span><span class="na">bool</span><span class="o">(</span><span class="n">b</span> <span class="o">-&gt;</span> <span class="n">b</span>
                                <span class="o">.</span><span class="na">must</span><span class="o">(</span><span class="n">m</span> <span class="o">-&gt;</span> <span class="n">m</span><span class="o">.</span><span class="na">term</span><span class="o">(</span><span class="n">t</span> <span class="o">-&gt;</span> <span class="n">t</span><span class="o">.</span><span class="na">field</span><span class="o">(</span><span class="s">"name"</span><span class="o">).</span><span class="na">value</span><span class="o">(</span><span class="nc">FieldValue</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"test"</span><span class="o">))))</span>
                        <span class="o">)</span>
                <span class="o">));</span>
    <span class="nc">SearchResponse</span><span class="o">&lt;</span><span class="nc">DemoDomain</span><span class="o">&gt;</span> <span class="n">search</span> <span class="o">=</span> <span class="n">elasticsearchClient</span><span class="o">.</span><span class="na">search</span><span class="o">(</span><span class="n">searchRequest</span><span class="o">,</span> <span class="nc">DemoDomain</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
    <span class="k">return</span> <span class="n">search</span><span class="o">.</span><span class="na">hits</span><span class="o">().</span><span class="na">hits</span><span class="o">().</span><span class="na">stream</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="nl">Hit:</span><span class="o">:</span><span class="n">source</span><span class="o">).</span><span class="na">toList</span><span class="o">();</span>
</code></pre></div></div>

<p>3.查询单个数据，在查询条件中指定id条件可以精确查询单条记录。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nc">SearchRequest</span> <span class="n">searchRequest</span> <span class="o">=</span> <span class="nc">SearchRequest</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">s</span> <span class="o">-&gt;</span> <span class="n">s</span>
                <span class="o">.</span><span class="na">index</span><span class="o">(</span><span class="no">MY_INDEX</span><span class="o">)</span>
                <span class="o">.</span><span class="na">query</span><span class="o">(</span><span class="n">q</span> <span class="o">-&gt;</span> <span class="n">q</span>
                        <span class="o">.</span><span class="na">bool</span><span class="o">(</span><span class="n">b</span> <span class="o">-&gt;</span> <span class="n">b</span>
                                <span class="o">.</span><span class="na">must</span><span class="o">(</span><span class="n">m</span> <span class="o">-&gt;</span> <span class="n">m</span><span class="o">.</span><span class="na">term</span><span class="o">(</span>
                                        <span class="n">t</span> <span class="o">-&gt;</span> <span class="n">t</span><span class="o">.</span><span class="na">field</span><span class="o">(</span><span class="s">"id"</span><span class="o">).</span><span class="na">value</span><span class="o">(</span><span class="nc">FieldValue</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"1"</span><span class="o">))))</span>
                                <span class="o">.</span><span class="na">must</span><span class="o">(</span><span class="n">m</span> <span class="o">-&gt;</span> <span class="n">m</span><span class="o">.</span><span class="na">term</span><span class="o">(</span>
                                        <span class="n">t</span> <span class="o">-&gt;</span> <span class="n">t</span><span class="o">.</span><span class="na">field</span><span class="o">(</span><span class="s">"name"</span><span class="o">).</span><span class="na">value</span><span class="o">(</span><span class="nc">FieldValue</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"test"</span><span class="o">))))</span>
                        <span class="o">)</span>
                <span class="o">));</span>
    <span class="nc">SearchResponse</span><span class="o">&lt;</span><span class="nc">DemoDomain</span><span class="o">&gt;</span> <span class="n">search</span> <span class="o">=</span> <span class="n">elasticsearchClient</span><span class="o">.</span><span class="na">search</span><span class="o">(</span><span class="n">searchRequest</span><span class="o">,</span> <span class="nc">DemoDomain</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
    <span class="k">return</span> <span class="n">search</span><span class="o">.</span><span class="na">hits</span><span class="o">().</span><span class="na">hits</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="mi">0</span><span class="o">).</span><span class="na">source</span><span class="o">();</span>
</code></pre></div></div>

<p>4.删除单个数据，通过DeleteRequest可以删除指定的id的记录。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nc">DeleteRequest</span> <span class="n">deleteRequest</span> <span class="o">=</span> <span class="nc">DeleteRequest</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">s</span> <span class="o">-&gt;</span> <span class="n">s</span>
                <span class="o">.</span><span class="na">index</span><span class="o">(</span><span class="no">MY_INDEX</span><span class="o">)</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="n">elasticsearchClient</span><span class="o">.</span><span class="na">delete</span><span class="o">(</span><span class="n">deleteRequest</span><span class="o">);</span>
</code></pre></div></div>

<p>5.删除查找到的数据，将查询操作和删除操作合并到一起。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nc">SearchRequest</span> <span class="n">searchRequest</span> <span class="o">=</span> <span class="nc">SearchRequest</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">s</span> <span class="o">-&gt;</span> <span class="n">s</span>
                <span class="o">.</span><span class="na">index</span><span class="o">(</span><span class="no">MY_INDEX</span><span class="o">)</span>
                <span class="o">.</span><span class="na">query</span><span class="o">(</span><span class="n">q</span> <span class="o">-&gt;</span> <span class="n">q</span>
                        <span class="o">.</span><span class="na">bool</span><span class="o">(</span><span class="n">b</span> <span class="o">-&gt;</span> <span class="n">b</span>
                                <span class="o">.</span><span class="na">must</span><span class="o">(</span><span class="n">m</span> <span class="o">-&gt;</span> <span class="n">m</span><span class="o">.</span><span class="na">term</span><span class="o">(</span>
                                        <span class="n">t</span> <span class="o">-&gt;</span> <span class="n">t</span><span class="o">.</span><span class="na">field</span><span class="o">(</span><span class="s">"name"</span><span class="o">).</span><span class="na">value</span><span class="o">(</span><span class="nc">FieldValue</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"test"</span><span class="o">))))</span>
                        <span class="o">)</span>
                <span class="o">));</span>
    <span class="nc">SearchResponse</span><span class="o">&lt;</span><span class="nc">DemoDomain</span><span class="o">&gt;</span> <span class="n">search</span> <span class="o">=</span> <span class="n">elasticsearchClient</span><span class="o">.</span><span class="na">search</span><span class="o">(</span><span class="n">searchRequest</span><span class="o">,</span> <span class="nc">DemoDomain</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
    <span class="n">elasticsearchClient</span><span class="o">.</span><span class="na">search</span><span class="o">(</span><span class="n">searchRequest</span><span class="o">,</span> <span class="nc">DemoDomain</span><span class="o">.</span><span class="na">class</span><span class="o">).</span><span class="na">hits</span><span class="o">().</span><span class="na">hits</span><span class="o">().</span><span class="na">forEach</span><span class="o">(</span><span class="n">record</span> <span class="o">-&gt;</span> <span class="o">{</span>
            <span class="nc">DeleteRequest</span> <span class="n">deleteRequest</span> <span class="o">=</span> <span class="nc">DeleteRequest</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">s</span> <span class="o">-&gt;</span> <span class="n">s</span>
                    <span class="o">.</span><span class="na">index</span><span class="o">(</span><span class="no">MY_INDEX</span><span class="o">)</span>
                    <span class="o">.</span><span class="na">id</span><span class="o">(</span><span class="n">record</span><span class="o">.</span><span class="na">source</span><span class="o">().</span><span class="na">getId</span><span class="o">()));</span>
            <span class="k">try</span> <span class="o">{</span>
                <span class="n">elasticsearchClient</span><span class="o">.</span><span class="na">delete</span><span class="o">(</span><span class="n">deleteRequest</span><span class="o">);</span>
            <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">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">RuntimeException</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>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="ElasticSearch" /><summary type="html"><![CDATA[ElasticSearch在7.17版本之前使用的java客户端是Java REST Client，但是从7.17版本开始，官方将Java REST Client标记为弃用（deprecated），推荐使用新版Java Client。 本文介绍新版ElasticSearch Java Client的基本用法。 具体的代码参照 示例项目 https://github.com/qihaiyan/springcamp/tree/main/elasticsearch-javaclient]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/elasticsearch-intro.png" /><media:content medium="image" url="https://springcamp.cn/assets/images/elasticsearch-intro.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">spring-rest接口LocalDateTime转时间戳</title><link href="https://springcamp.cn/spring-localdatetime-epoch/" rel="alternate" type="text/html" title="spring-rest接口LocalDateTime转时间戳" /><published>2022-04-04T09:50:00+00:00</published><updated>2022-04-04T09:50:00+00:00</updated><id>https://springcamp.cn/spring-localdatetime-epoch</id><content type="html" xml:base="https://springcamp.cn/spring-localdatetime-epoch/"><![CDATA[<p>本文介绍spring-rest接口中的LocalDateTime日期类型转时间戳的方法。
具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-redis-spring-localdatetime-epoch">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-localdatetime-epoch</a></p>

<h2 id="一概述">一、概述</h2>

<p>java程序中一般将日期类型定义为LocalDateTime，数据库中保存的时间是0时区的时间（UTC时间）。对于接口来说，为了支持全球化多时区，接口中的日期类型通常会返回UTC时间戳，简称Epoch，数据类型为long，前端程序会根据本地时区，将时间戳转换为日期格式的字符串，如YYYY-mm-dd HH:mm:ss。</p>

<p>如果在每个时间型字段在接口返回时都进行转换处理，会比较繁琐。应该在一个统一的地方处理这种转换，业务逻辑处理过程中不感知这种转换。</p>

<h2 id="二通过jackson2objectmapperbuildercustomizer进行全局类型转换">二、通过Jackson2ObjectMapperBuilderCustomizer进行全局类型转换</h2>

<p>spring提供了<code class="language-plaintext highlighter-rouge">Jackson2ObjectMapperBuilderCustomizer</code>可以用于自定义json与对象之间相互转换的处理。</p>

<p>通过自定义<code class="language-plaintext highlighter-rouge">Jackson2ObjectMapperBuilderCustomizer</code>，我们可以在json与对象的相互转换转换阶段完成LocalDateTime和Epoch之间的转换，包括接口的入参和出参。</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">LocalDateTimeToEpochSerdeConfig</span> <span class="o">{</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">Jackson2ObjectMapperBuilderCustomizer</span> <span class="nf">jackson2ObjectMapperBuilderCustomizer</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">builder</span> <span class="o">-&gt;</span> <span class="n">builder</span><span class="o">.</span><span class="na">serializerByType</span><span class="o">(</span><span class="nc">LocalDateTime</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="k">new</span> <span class="nc">LocalDateTimeToEpochSerializer</span><span class="o">())</span>
                <span class="o">.</span><span class="na">deserializerByType</span><span class="o">(</span><span class="nc">LocalDateTime</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="k">new</span> <span class="nc">LocalDateTimeFromEpochDeserializer</span><span class="o">());</span>
    <span class="o">}</span>

    <span class="cm">/**
     * 序列化
     */</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">LocalDateTimeToEpochSerializer</span> <span class="kd">extends</span> <span class="nc">JsonSerializer</span><span class="o">&lt;</span><span class="nc">LocalDateTime</span><span class="o">&gt;</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="nc">LocalDateTime</span> <span class="n">value</span><span class="o">,</span> <span class="nc">JsonGenerator</span> <span class="n">gen</span><span class="o">,</span> <span class="nc">SerializerProvider</span> <span class="n">serializers</span><span class="o">)</span>
                <span class="kd">throws</span> <span class="nc">IOException</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="kc">null</span><span class="o">)</span> <span class="o">{</span>
                <span class="kt">long</span> <span class="n">timestamp</span> <span class="o">=</span> <span class="n">value</span><span class="o">.</span><span class="na">atZone</span><span class="o">(</span><span class="nc">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="na">getEpochSecond</span><span class="o">();</span>
                <span class="n">gen</span><span class="o">.</span><span class="na">writeNumber</span><span class="o">(</span><span class="n">timestamp</span><span class="o">);</span>
            <span class="o">}</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="cm">/**
     * 反序列化
     */</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">LocalDateTimeFromEpochDeserializer</span> <span class="kd">extends</span> <span class="nc">JsonDeserializer</span><span class="o">&lt;</span><span class="nc">LocalDateTime</span><span class="o">&gt;</span> <span class="o">{</span>
        <span class="nd">@Override</span>
        <span class="kd">public</span> <span class="nc">LocalDateTime</span> <span class="nf">deserialize</span><span class="o">(</span><span class="nc">JsonParser</span> <span class="n">p</span><span class="o">,</span> <span class="nc">DeserializationContext</span> <span class="n">ctxt</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span>
            <span class="nc">NumberDeserializers</span><span class="o">.</span><span class="na">LongDeserializer</span> <span class="n">longDeserializer</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">NumberDeserializers</span><span class="o">.</span><span class="na">LongDeserializer</span><span class="o">(</span><span class="nc">Long</span><span class="o">.</span><span class="na">TYPE</span><span class="o">,</span> <span class="mi">0L</span><span class="o">);</span>
            <span class="nc">Long</span> <span class="n">epoch</span> <span class="o">=</span> <span class="n">longDeserializer</span><span class="o">.</span><span class="na">deserialize</span><span class="o">(</span><span class="n">p</span><span class="o">,</span> <span class="n">ctxt</span><span class="o">);</span>
            <span class="k">return</span> <span class="nc">LocalDateTime</span><span class="o">.</span><span class="na">ofInstant</span><span class="o">(</span><span class="nc">Instant</span><span class="o">.</span><span class="na">ofEpochSecond</span><span class="o">(</span><span class="n">epoch</span><span class="o">),</span> <span class="nc">ZoneId</span><span class="o">.</span><span class="na">systemDefault</span><span class="o">());</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>以上代码中分别包含了json的序列化和反序列化操作，在序列化操作中，把LocalDateTime转换为Epoch。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   <span class="cm">/**
     * 序列化
     */</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">LocalDateTimeToEpochSerializer</span> <span class="kd">extends</span> <span class="nc">JsonSerializer</span><span class="o">&lt;</span><span class="nc">LocalDateTime</span><span class="o">&gt;</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="nc">LocalDateTime</span> <span class="n">value</span><span class="o">,</span> <span class="nc">JsonGenerator</span> <span class="n">gen</span><span class="o">,</span> <span class="nc">SerializerProvider</span> <span class="n">serializers</span><span class="o">)</span>
                <span class="kd">throws</span> <span class="nc">IOException</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="kc">null</span><span class="o">)</span> <span class="o">{</span>
                <span class="kt">long</span> <span class="n">timestamp</span> <span class="o">=</span> <span class="n">value</span><span class="o">.</span><span class="na">atZone</span><span class="o">(</span><span class="nc">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="na">getEpochSecond</span><span class="o">();</span>
                <span class="n">gen</span><span class="o">.</span><span class="na">writeNumber</span><span class="o">(</span><span class="n">timestamp</span><span class="o">);</span>
            <span class="o">}</span>
        <span class="o">}</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>在反序列化操作中，把Epoch转换为LocalDateTime。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="cm">/**
     * 反序列化
     */</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">LocalDateTimeFromEpochDeserializer</span> <span class="kd">extends</span> <span class="nc">JsonDeserializer</span><span class="o">&lt;</span><span class="nc">LocalDateTime</span><span class="o">&gt;</span> <span class="o">{</span>
        <span class="nd">@Override</span>
        <span class="kd">public</span> <span class="nc">LocalDateTime</span> <span class="nf">deserialize</span><span class="o">(</span><span class="nc">JsonParser</span> <span class="n">p</span><span class="o">,</span> <span class="nc">DeserializationContext</span> <span class="n">ctxt</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span>
            <span class="nc">NumberDeserializers</span><span class="o">.</span><span class="na">LongDeserializer</span> <span class="n">longDeserializer</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">NumberDeserializers</span><span class="o">.</span><span class="na">LongDeserializer</span><span class="o">(</span><span class="nc">Long</span><span class="o">.</span><span class="na">TYPE</span><span class="o">,</span> <span class="mi">0L</span><span class="o">);</span>
            <span class="nc">Long</span> <span class="n">epoch</span> <span class="o">=</span> <span class="n">longDeserializer</span><span class="o">.</span><span class="na">deserialize</span><span class="o">(</span><span class="n">p</span><span class="o">,</span> <span class="n">ctxt</span><span class="o">);</span>
            <span class="k">return</span> <span class="nc">LocalDateTime</span><span class="o">.</span><span class="na">ofInstant</span><span class="o">(</span><span class="nc">Instant</span><span class="o">.</span><span class="na">ofEpochSecond</span><span class="o">(</span><span class="n">epoch</span><span class="o">),</span> <span class="nc">ZoneId</span><span class="o">.</span><span class="na">systemDefault</span><span class="o">());</span>
        <span class="o">}</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>通过以上配置，我们可以在实体类中使用LocalDateTime类型。客户端请求接口时，对于返回结果，自动转换为Epoch数据，对于请求参数，自动从Epoch转换为LocalDateTime。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><summary type="html"><![CDATA[本文介绍spring-rest接口中的LocalDateTime日期类型转时间戳的方法。 具体的代码参照 示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-localdatetime-epoch]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/epoch.jpg" /><media:content medium="image" url="https://springcamp.cn/assets/images/epoch.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">spring缓存自定义resolver</title><link href="https://springcamp.cn/spring-redis-resolver/" rel="alternate" type="text/html" title="spring缓存自定义resolver" /><published>2022-03-13T14:20:00+00:00</published><updated>2022-03-13T14:20:00+00:00</updated><id>https://springcamp.cn/spring-redis-resolver</id><content type="html" xml:base="https://springcamp.cn/spring-redis-resolver/"><![CDATA[<p>本文介绍spring中自定义缓存resolver，通过自定义resolver，可以在spring的cache注解中增加附加处理。
具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-redis-resolver">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-redis-resolver</a></p>

<h2 id="一概述">一、概述</h2>

<p>cache-aside模式是常用的缓存使用模式。使用流程如下图：</p>

<p><img src="../assets/images/cache-aside.png" alt="cache-aside" /></p>

<p>当更新数据库中的数据后，对缓存做失效处理，后续就能读取到数据库中最新的数据，使得缓存数据与数据库数据保持一致。</p>

<p>在spring中通过cache注解进行缓存的处理，一般会把缓存处理封装到dao层，这样业务层就不需要感知缓存操作的细节，可以专注于业务逻辑的处理。</p>

<h2 id="二缓存的读取和失效">二、缓存的读取和失效</h2>

<p>dao层的操作通常使用springdatajpa，数据库方法都是一个interface，通过在interface上增加对应的cache注解实现缓存处理。</p>

<p>读取数据：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Cacheable</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"testCache"</span><span class="o">,</span> <span class="n">key</span> <span class="o">=</span> <span class="s">"#p0"</span><span class="o">,</span> <span class="n">unless</span> <span class="o">=</span> <span class="s">"#result == null"</span><span class="o">)</span>
<span class="nc">Optional</span><span class="o">&lt;</span><span class="nc">DemoEntity</span><span class="o">&gt;</span> <span class="nf">findById</span><span class="o">(</span><span class="nc">Long</span> <span class="n">id</span><span class="o">);</span>
</code></pre></div></div>

<p>通过Cacheable注解，从数据库中读取到数据后，会同步写到缓存中。</p>

<p>保存数据：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@CacheEvict</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"testCache"</span><span class="o">,</span> <span class="n">key</span> <span class="o">=</span> <span class="s">"#p0.id"</span><span class="o">)</span>
<span class="nc">DemoEntity</span> <span class="nf">save</span><span class="o">(</span><span class="nc">DemoEntity</span> <span class="n">entity</span><span class="o">);</span>
</code></pre></div></div>

<p>通过CacheEvict注解，在将数据写入到数据库后，对缓存进行失效。
如果我们想在缓存失效后，进行其它的操作，例如将失效缓存的key写入kafka，用于其它系统同步删除缓存，这时该怎样处理？</p>

<h2 id="三自定义缓存resolver">三、自定义缓存resolver</h2>

<p>spring提供了自定义缓存resolver的方式，通过自定义resolver，可以在缓存处理中增加附加操作。</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">RedisCacheConfig</span> <span class="kd">extends</span> <span class="nc">CachingConfigurerSupport</span> <span class="o">{</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">RedisCacheManager</span> <span class="nf">redisCacheManager</span><span class="o">(</span><span class="nc">RedisConnectionFactory</span> <span class="n">redisConnectionFactory</span><span class="o">)</span> <span class="o">{</span>

        <span class="nc">RedisCacheConfiguration</span> <span class="n">cacheConfiguration</span> <span class="o">=</span> <span class="nc">RedisCacheConfiguration</span><span class="o">.</span><span class="na">defaultCacheConfig</span><span class="o">()</span>
                <span class="o">.</span><span class="na">computePrefixWith</span><span class="o">(</span><span class="n">cacheName</span> <span class="o">-&gt;</span> <span class="n">cacheName</span><span class="o">.</span><span class="na">concat</span><span class="o">(</span><span class="s">":"</span><span class="o">));</span>

        <span class="k">return</span> <span class="nc">RedisCacheManager</span><span class="o">.</span><span class="na">builder</span><span class="o">(</span><span class="n">redisConnectionFactory</span><span class="o">)</span>
                <span class="o">.</span><span class="na">cacheDefaults</span><span class="o">(</span><span class="n">cacheConfiguration</span><span class="o">)</span>
                <span class="o">.</span><span class="na">build</span><span class="o">();</span>

    <span class="o">}</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">CacheResolver</span> <span class="nf">customCacheResolver</span><span class="o">(</span><span class="nc">RedisConnectionFactory</span> <span class="n">redisConnectionFactory</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">CustomCacheResolver</span><span class="o">(</span><span class="n">redisCacheManager</span><span class="o">(</span><span class="n">redisConnectionFactory</span><span class="o">));</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>以上代码是redis缓存的配置，其中 <code class="language-plaintext highlighter-rouge">RedisCacheManager</code> 部分是常规的cacheManager的配置， 而  <code class="language-plaintext highlighter-rouge">customCacheResolver</code> 部分是自定义resolver的配置，通过定义customCacheResolver这个bean，可以在cache注解中引用到这个自定义的resolver。</p>

<p>定义好customCacheResolver的bean后，我们就可以在cache注解中引用，上面提到的数据保存方法改造后的代码：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@CacheEvict</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"testCache"</span><span class="o">,</span> <span class="n">cacheResolver</span> <span class="o">=</span> <span class="s">"customCacheResolver"</span><span class="o">,</span> <span class="n">key</span> <span class="o">=</span> <span class="s">"#p0.id"</span><span class="o">)</span>
<span class="nc">DemoEntity</span> <span class="nf">save</span><span class="o">(</span><span class="nc">DemoEntity</span> <span class="n">entity</span><span class="o">);</span>
</code></pre></div></div>

<p>相比于之前的实现，对CacheEvict增加指定cacheResolver。</p>

<h2 id="四自定义resolver的实现">四、自定义resolver的实现</h2>

<p>上面我们介绍了如果配置和引用cacheResolver，下面介绍自定义cacheResolver的实现。</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">CustomCacheResolver</span> <span class="kd">extends</span> <span class="nc">SimpleCacheResolver</span> <span class="o">{</span>

    <span class="kd">public</span> <span class="nf">CustomCacheResolver</span><span class="o">(</span><span class="nc">CacheManager</span> <span class="n">cacheManager</span><span class="o">)</span> <span class="o">{</span>
        <span class="kd">super</span><span class="o">(</span><span class="n">cacheManager</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="nd">@NonNull</span>
    <span class="kd">public</span> <span class="nc">Collection</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">Cache</span><span class="o">&gt;</span> <span class="nf">resolveCaches</span><span class="o">(</span><span class="nc">CacheOperationInvocationContext</span><span class="o">&lt;?&gt;</span> <span class="n">context</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">ParameterNameDiscoverer</span> <span class="n">paramNameDiscoverer</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DefaultParameterNameDiscoverer</span><span class="o">();</span>
        <span class="nc">EvaluationContext</span> <span class="n">evaluationContext</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MethodBasedEvaluationContext</span><span class="o">(</span><span class="n">context</span><span class="o">.</span><span class="na">getOperation</span><span class="o">(),</span> <span class="n">context</span><span class="o">.</span><span class="na">getMethod</span><span class="o">(),</span> <span class="n">context</span><span class="o">.</span><span class="na">getArgs</span><span class="o">(),</span> <span class="n">paramNameDiscoverer</span><span class="o">);</span>
        <span class="nc">Expression</span> <span class="n">exp</span> <span class="o">=</span> <span class="o">(</span><span class="k">new</span> <span class="nc">SpelExpressionParser</span><span class="o">()).</span><span class="na">parseExpression</span><span class="o">(((</span><span class="nc">CacheEvictOperation</span><span class="o">)</span> <span class="n">context</span><span class="o">.</span><span class="na">getOperation</span><span class="o">()).</span><span class="na">getKey</span><span class="o">());</span>
        <span class="nc">Collection</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">Cache</span><span class="o">&gt;</span> <span class="n">caches</span> <span class="o">=</span> <span class="kd">super</span><span class="o">.</span><span class="na">resolveCaches</span><span class="o">(</span><span class="n">context</span><span class="o">);</span>
        <span class="n">context</span><span class="o">.</span><span class="na">getOperation</span><span class="o">().</span><span class="na">getCacheNames</span><span class="o">().</span><span class="na">forEach</span><span class="o">(</span><span class="n">cacheName</span> <span class="o">-&gt;</span> <span class="o">{</span>
            <span class="nc">String</span> <span class="n">key</span> <span class="o">=</span> <span class="n">cacheName</span> <span class="o">+</span> <span class="sc">':'</span> <span class="o">+</span> <span class="n">exp</span><span class="o">.</span><span class="na">getValue</span><span class="o">(</span><span class="n">evaluationContext</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
            <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"cache key={}"</span><span class="o">,</span> <span class="n">key</span><span class="o">);</span>
        <span class="o">});</span>
        <span class="k">return</span> <span class="n">caches</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>上面的代码定义了CustomCacheResolver这个自定义resolver类，继承SimpleCacheResolver。SimpleCacheResolver类是spring在cache注解中默认使用的resolver。
我们通过扩展SimpleCacheResolver这个类，来增加附加操作。其中<code class="language-plaintext highlighter-rouge">resolveCaches</code>就是解析缓存操作的部分。
在这部分代码中，我们需要的是获取到 <code class="language-plaintext highlighter-rouge">@CacheEvict(value = "testCache", cacheResolver = "customCacheResolver", key = "#p0.id")</code> 注解中失效的缓存的key的值。
通过 <code class="language-plaintext highlighter-rouge">context.getOperation()).getKey()</code> 从参数context中可以读取到key的定义，即 <code class="language-plaintext highlighter-rouge">#p0.id</code> ，这个定义是一个spel表达式，与普通的spel表达式不同， p0这个变量是jpa方法中的一个特有变量，表示方法中的第一个参数，同样p1表示方法中的第二个参数。通过普通的spel处理无法解析这个spel表达式。
spring提供了 <code class="language-plaintext highlighter-rouge">MethodBasedEvaluationContext</code> 类用于解析这种特殊的spel表达式。</p>

<p>通过一下四行代码，我们就能够获取到具体的key的值：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">ParameterNameDiscoverer</span> <span class="n">paramNameDiscoverer</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DefaultParameterNameDiscoverer</span><span class="o">();</span>
<span class="nc">EvaluationContext</span> <span class="n">evaluationContext</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MethodBasedEvaluationContext</span><span class="o">(</span><span class="n">context</span><span class="o">.</span><span class="na">getOperation</span><span class="o">(),</span> <span class="n">context</span><span class="o">.</span><span class="na">getMethod</span><span class="o">(),</span> <span class="n">context</span><span class="o">.</span><span class="na">getArgs</span><span class="o">(),</span> <span class="n">paramNameDiscoverer</span><span class="o">);</span>
<span class="nc">Expression</span> <span class="n">exp</span> <span class="o">=</span> <span class="o">(</span><span class="k">new</span> <span class="nc">SpelExpressionParser</span><span class="o">()).</span><span class="na">parseExpression</span><span class="o">(((</span><span class="nc">CacheEvictOperation</span><span class="o">)</span> <span class="n">context</span><span class="o">.</span><span class="na">getOperation</span><span class="o">()).</span><span class="na">getKey</span><span class="o">());</span>
<span class="nc">String</span> <span class="n">key</span> <span class="o">=</span> <span class="n">cacheName</span> <span class="o">+</span> <span class="sc">':'</span> <span class="o">+</span> <span class="n">exp</span><span class="o">.</span><span class="na">getValue</span><span class="o">(</span><span class="n">evaluationContext</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
</code></pre></div></div>

<p>获取到了key的值，我们就可以对这个key做很多操作，可以把这个key写入kafka，通知其它系统同步清理key。</p>

<h2 id="五总结">五、总结</h2>

<p>我们通常把缓存操作封装到dao层以简化程序的整体逻辑，当使用springdatajpa作为dao层的实现时，具体的dao方法都是interface，对于在interface上添加的cache注解，没有办法增加额外的其它操作。
当需要对缓存操作做额外处理时，可以通过自定义resolver的方式实现，在cache注解中使用我们自定义的resolver。
这样既没有破环程序的整理逻辑，又扩展了对缓存的操作，是一种比较好的实现方式。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><summary type="html"><![CDATA[本文介绍spring中自定义缓存resolver，通过自定义resolver，可以在spring的cache注解中增加附加处理。 具体的代码参照 示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-redis-resolver]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/redis-resolver.jpg" /><media:content medium="image" url="https://springcamp.cn/assets/images/redis-resolver.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">spring使用kafka的三种方式（listener、container、stream）</title><link href="https://springcamp.cn/spring-kafka/" rel="alternate" type="text/html" title="spring使用kafka的三种方式（listener、container、stream）" /><published>2021-10-31T08:20:00+00:00</published><updated>2021-10-31T08:20:00+00:00</updated><id>https://springcamp.cn/spring-kafka</id><content type="html" xml:base="https://springcamp.cn/spring-kafka/"><![CDATA[<p>本文介绍spring中使用Kafka的三种方式，其中container方式最灵活，但是开发相对较复杂，stream方式使用最简便，listener方式由于提供的最早，使用的较普遍。
具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-kafka">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-kafka</a></p>

<h2 id="一概述">一、概述</h2>

<p>在实际项目中，用到kafka的场景非常普遍，特别是事件驱动的编程模式，kafka基本是标配。</p>

<h2 id="二kafkalistener">二、KafkaListener</h2>

<p>KafkaListener应该是目前使用比较多的一种方式，开发简单，易于理解。但是从易用性的角度，应该会逐步被spring-cloud-stream所替代。
KafkaListener是一个注解，在对应的方法上加上这个注解，方法就可以处理接收到的kakfa消息，注解中通过topics参数指定需要消费的kakfa的topic，topics参数支持SPEL表达式，可以同时消费多个kafka topic：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@KafkaListener</span><span class="o">(</span><span class="n">topics</span> <span class="o">=</span> <span class="s">"test-topic"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">receive</span><span class="o">(</span><span class="nc">ConsumerRecord</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">consumerRecord</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">payload</span> <span class="o">=</span> <span class="n">consumerRecord</span><span class="o">.</span><span class="na">value</span><span class="o">();</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"received payload='{}'"</span><span class="o">,</span> <span class="n">payload</span><span class="o">);</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>带注解的方法的入参是一个ConsumerRecord变量，存放了kafka中接收到的消息。</p>

<p>同时需要对kafka进行配置，可以指定kakfa服务器的地址，以及序列化方式：</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">spring.kafka</span><span class="pi">:</span>
    <span class="na">bootstrap-servers</span><span class="pi">:</span> <span class="s">192.168.1.1:2181</span>
    <span class="na">consumer</span><span class="pi">:</span>
      <span class="na">group-id</span><span class="pi">:</span> <span class="s">utgroup</span>
      <span class="na">auto-offset-reset</span><span class="pi">:</span> <span class="s">earliest</span>
      <span class="na">key-deserializer</span><span class="pi">:</span> <span class="s">org.apache.kafka.common.serialization.StringDeserializer</span>
      <span class="na">value-deserializer</span><span class="pi">:</span> <span class="s">org.apache.kafka.common.serialization.StringDeserializer</span>
</code></pre></div></div>

<h2 id="三concurrentmessagelistenercontainer">三、ConcurrentMessageListenerContainer</h2>

<p>通过ConcurrentMessageListenerContainer可以已可编程的方式来处理kafka消息，这种方式的好处是topic是在程序中指定的，这样可以将topic的配置存贮在任何地方，比如数据库中，也可以按照不同的条件分支指定不同的topic，非常灵活。这是其它两种方式做不到的。</p>

<p>ConcurrentMessageListenerContainer的配置：</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">MessageListenerContainerConsumer</span> <span class="o">{</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">LISTENER_CONTAINER_TOPIC</span> <span class="o">=</span> <span class="s">"container-topic"</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nc">Set</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">consumedMessages</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HashSet</span><span class="o">&lt;&gt;();</span>

    <span class="nd">@PostConstruct</span>
    <span class="kt">void</span> <span class="nf">start</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">MessageListener</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">messageListener</span> <span class="o">=</span> <span class="n">record</span> <span class="o">-&gt;</span> <span class="o">{</span>
            <span class="nc">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">"MessageListenerContainerConsumer received message: "</span> <span class="o">+</span> <span class="n">record</span><span class="o">.</span><span class="na">value</span><span class="o">());</span>
            <span class="n">consumedMessages</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">record</span><span class="o">.</span><span class="na">value</span><span class="o">());</span>
        <span class="o">};</span>

        <span class="nc">ConcurrentMessageListenerContainer</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">container</span> <span class="o">=</span>
                <span class="k">new</span> <span class="nc">ConcurrentMessageListenerContainer</span><span class="o">&lt;&gt;(</span>
                        <span class="n">consumerFactory</span><span class="o">(),</span>
                        <span class="n">containerProperties</span><span class="o">(</span><span class="no">LISTENER_CONTAINER_TOPIC</span><span class="o">,</span> <span class="n">messageListener</span><span class="o">));</span>

        <span class="n">container</span><span class="o">.</span><span class="na">start</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">DefaultKafkaConsumerFactory</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="nf">consumerFactory</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nc">DefaultKafkaConsumerFactory</span><span class="o">&lt;&gt;(</span>
                <span class="k">new</span> <span class="nc">HashMap</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;()</span> <span class="o">{</span>
                    <span class="o">{</span>
                        <span class="n">put</span><span class="o">(</span><span class="no">BOOTSTRAP_SERVERS_CONFIG</span><span class="o">,</span> <span class="nc">System</span><span class="o">.</span><span class="na">getProperty</span><span class="o">(</span><span class="s">"spring.kafka.bootstrap-servers"</span><span class="o">));</span>
                        <span class="n">put</span><span class="o">(</span><span class="no">GROUP_ID_CONFIG</span><span class="o">,</span> <span class="s">"groupId"</span><span class="o">);</span>
                        <span class="n">put</span><span class="o">(</span><span class="no">AUTO_OFFSET_RESET_CONFIG</span><span class="o">,</span> <span class="s">"earliest"</span><span class="o">);</span>
                    <span class="o">}</span>
                <span class="o">},</span>
                <span class="k">new</span> <span class="nf">StringDeserializer</span><span class="o">(),</span>
                <span class="k">new</span> <span class="nf">StringDeserializer</span><span class="o">());</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">ContainerProperties</span> <span class="nf">containerProperties</span><span class="o">(</span><span class="nc">String</span> <span class="n">topic</span><span class="o">,</span> <span class="nc">MessageListener</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">messageListener</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">ContainerProperties</span> <span class="n">containerProperties</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ContainerProperties</span><span class="o">(</span><span class="n">topic</span><span class="o">);</span>
        <span class="n">containerProperties</span><span class="o">.</span><span class="na">setMessageListener</span><span class="o">(</span><span class="n">messageListener</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">containerProperties</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>以上代码定义了MessageListenerContainerConsumer这个类，是一个spring的bean，在PostConstruct这个bean的初始化代码中，我们使用了ConcurrentMessageListenerContainer，并指定了topic <code class="language-plaintext highlighter-rouge">public static final String LISTENER_CONTAINER_TOPIC = "container-topic"</code>，在这个为了便于演示我们使用了一个常量，实际上这个topic的值可以是任意变量，可以从数据库中读取，也可以通过实际的场景动态计算，这样就做到了topic的灵活配置。</p>

<h2 id="四spring-cloud-stream">四、spring-cloud-stream</h2>

<p>spring-cloud-stream是springcloud的一个子项目，这个项目的目标是一个事件驱动（Event-Driven）的编程框架。spring-cloud-stream对kafka进行了非常好的抽象，除了kakfa，还支持RabbitMQ，程序中除了配置文件外，完全看不到kafka的痕迹，意味着我们在开发的时候不需要关心底层的kakfa的细节，如果像从kafka切换到RabbitMQ，只需要修改一下引入的jar包和配置文件。</p>

<p>详细介绍见spring的官方文档： <a href="https://spring.io/projects/spring-cloud-stream">https://spring.io/projects/spring-cloud-stream</a></p>

<p>spring-cloud-stream是用了spring-cloud-function，我们只需要在程序中实现一个function接口，就可以处理kafka消息，编写非常简单。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">i</span><span class="nd">@SpringBootApplication</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Application</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="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">SpringApplication</span><span class="o">.</span><span class="na">run</span><span class="o">(</span><span class="nc">Application</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="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">Function</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;</span> <span class="nf">handle</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nl">String:</span><span class="o">:</span><span class="n">toUpperCase</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>除了配置文件外，代码中只需要一行代码<code class="language-plaintext highlighter-rouge">public Function&lt;String, Object&gt; handle()</code>就实现了kafka消息的处理，这行代码完全看不出跟kakfa有什么关系，就是一个普通的方法，把抽象做到了极致。
需要注意方法的名字于配置文件中的名字要匹配。</p>

<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">cloud.stream</span><span class="pi">:</span>
    <span class="na">bindings</span><span class="pi">:</span>
      <span class="na">handle-in-0</span><span class="pi">:</span>
        <span class="na">destination</span><span class="pi">:</span> <span class="s">testEmbeddedIn</span>
        <span class="na">content-type</span><span class="pi">:</span> <span class="s">text/plain</span>
        <span class="na">group</span><span class="pi">:</span> <span class="s">utgroup</span>
      <span class="na">handle-out-0</span><span class="pi">:</span>
        <span class="na">destination</span><span class="pi">:</span> <span class="s">testEmbeddedOut</span>
    <span class="na">kafka</span><span class="pi">:</span>
      <span class="na">binder</span><span class="pi">:</span>
        <span class="na">brokers</span><span class="pi">:</span> <span class="s">192.168.1.1:2181</span>
        <span class="na">configuration</span><span class="pi">:</span>
          <span class="na">key.serializer</span><span class="pi">:</span> <span class="s">org.apache.kafka.common.serialization.ByteArraySerializer</span>
          <span class="na">value.serializer</span><span class="pi">:</span> <span class="s">org.apache.kafka.common.serialization.ByteArraySerializer</span>
</code></pre></div></div>

<p>注意配置文件中 <code class="language-plaintext highlighter-rouge">handle-in-0</code> 和 <code class="language-plaintext highlighter-rouge">handle-out-0</code> 这2行配置，handle指的就是前面代码中的<code class="language-plaintext highlighter-rouge">public Function&lt;String, Object&gt; handle()</code> handle这个方法。spring-cloud-stream就是通过方法名和配置文件中配置项的名字，来确立代码和配置的匹配关系，这也是约定优于配置的编程思想的体现。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><summary type="html"><![CDATA[本文介绍spring中使用Kafka的三种方式，其中container方式最灵活，但是开发相对较复杂，stream方式使用最简便，listener方式由于提供的最早，使用的较普遍。 具体的代码参照 示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-kafka]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/spring-kafka.jpeg" /><media:content medium="image" url="https://springcamp.cn/assets/images/spring-kafka.jpeg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">springboot集成groovy脚本</title><link href="https://springcamp.cn/spring-groovy/" rel="alternate" type="text/html" title="springboot集成groovy脚本" /><published>2021-05-05T08:20:00+00:00</published><updated>2021-05-05T08:20:00+00:00</updated><id>https://springcamp.cn/spring-groovy</id><content type="html" xml:base="https://springcamp.cn/spring-groovy/"><![CDATA[<p>在我们的应用中引入脚本能力，可以很好的提升灵活性，我们的核心开发工作可以集中在核心平台能力的开发上，具体场景的功能可以通过脚本来实现，例如jenkins就可以通过groovy脚本来编写pipeline，可以很灵活的定制构建过程。
spring本身提供了groovy集成的机制，分为两种方式，一种是用groovy开发程序，跟用java开发类似，需要经过编译。一种是将groovy作为脚本来执行，不需要编译。在此我们介绍的是第二种方式，将groovy作为脚本来使用。
具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-groovy">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-groovy</a></p>

<h2 id="一概述">一、概述</h2>

<p>在spring中集成groovy脚本，主要有2种思路，一种是在groovy脚本中定义bean，这样groovy脚本就融入了整个spring的体系，跟使用普通的bean没有区别。一种是在程序中调用groovy脚本，让groovy脚本成为一个可执行的部件。下面我们分别介绍这2种方式。
在spring中声明groovy脚本中定义的bean有两种方式，一种是传统的xml，一种是spring-framework-4中引入的groovy声明方式。</p>

<h2 id="二在groovy中定义bean">二、在groovy中定义bean</h2>

<p>首先我们定义一个interface：</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">MyService</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="nf">fun</span><span class="o">(</span><span class="nc">MyDomain</span> <span class="n">myDomain</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>这儿提供了一种思路，我们可以用java代码编写默认的interface实现，如果默认实现不满足特定场景的要求时，配合策略模式，用groovy脚本实现特定场景，程序会变的很灵活，配合脚本的热加载机制，当处理逻辑需要变化时，在程序运行的过程中，我们可以随时调整脚本内容且能够及时生效。</p>

<p>在groovy脚本<code class="language-plaintext highlighter-rouge">MyServiceImpl.groovy</code>中实现这个interface：</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">MyServiceImpl</span> <span class="kd">implements</span> <span class="n">MyService</span> <span class="o">{</span>
    <span class="nd">@Autowired</span>
    <span class="n">FunBean</span> <span class="n">useBean</span><span class="o">;</span>

    <span class="n">String</span> <span class="n">myProp</span><span class="o">;</span>

    <span class="n">String</span> <span class="nf">fun</span><span class="o">(</span><span class="n">MyDomain</span> <span class="n">myDomain</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">myDomain</span><span class="o">.</span><span class="na">toString</span><span class="o">()</span> <span class="o">+</span> <span class="n">useBean</span><span class="o">.</span><span class="na">getFunName</span><span class="o">()</span> <span class="o">+</span> <span class="n">myProp</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>下面分别介绍通过xml和groovy两种配置方式来声明bean。</p>

<h3 id="21通过xml配置的方式声明groovy中实现的bean">2.1、通过xml配置的方式声明groovy中实现的bean</h3>

<p>通过xml配置声明bean是spring传统的方法，这种方法近来已经被通过java代码声明的方式取代，但是对于声明groovy脚本中定义的bean来说还是最简单的方法。</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;beans</span> <span class="na">xmlns=</span><span class="s">"http://www.springframework.org/schema/beans"</span>
       <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span>
       <span class="na">xmlns:lang=</span><span class="s">"http://www.springframework.org/schema/lang"</span>
       <span class="na">xsi:schemaLocation=</span><span class="s">"
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/lang https://www.springframework.org/schema/lang/spring-lang.xsd"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;lang:groovy</span> <span class="na">id=</span><span class="s">"myServiceXml"</span> <span class="na">script-source=</span><span class="s">"classpath:MyServiceImpl.groovy"</span> <span class="na">refresh-check-delay=</span><span class="s">"10000"</span> <span class="nt">&gt;</span>
        <span class="nt">&lt;lang:property</span> <span class="na">name=</span><span class="s">"myProp"</span> <span class="na">value=</span><span class="s">" this is xml init prop"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;/lang:groovy&gt;</span>
<span class="nt">&lt;/beans&gt;</span>
</code></pre></div></div>

<p>以上xml代码声明了myServiceXml这个bean，<code class="language-plaintext highlighter-rouge">script-source</code>指定了这个bean的来源是<code class="language-plaintext highlighter-rouge">classpath:MyServiceImpl.groovy</code>这个脚本文件。
将classpath替换为file，可以指定任一位置的脚本文件。</p>

<p><code class="language-plaintext highlighter-rouge">refresh-check-delay</code> 定义了脚本的刷新间隔，当脚本内容发生变化后，可以自动刷新脚本的内容。</p>

<p>property这个标签可以对bean的属性进行初始化赋值。我们分别用xml和groovy两种声明bean的方式给myProp这个属性赋值不同的初始值，在后续的演示代码中可以看到。</p>

<h3 id="22通过groovy配置的方式声明groovy中实现的bean">2.2、通过groovy配置的方式声明groovy中实现的bean</h3>

<p>spring-framework-4中引入了groovy声明bean的方式，我们用groovy来声明myServiceGroovy这个bean，相比于xml的方式，groovy的声明方式可读性更强一些。</p>

<p>详细介绍见spring的官方博文： <a href="https://spring.io/blog/2014/03/03/groovy-bean-configuration-in-spring-framework-4">Groovy Bean Configuration in Spring Framework 4</a></p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">org.springframework.scripting.groovy.GroovyScriptFactory</span>
<span class="kn">import</span> <span class="nn">org.springframework.scripting.support.ScriptFactoryPostProcessor</span>

<span class="n">beans</span> <span class="o">{</span>
    <span class="n">scriptFactoryPostProcessor</span><span class="o">(</span><span class="n">ScriptFactoryPostProcessor</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">defaultRefreshCheckDelay</span> <span class="o">=</span> <span class="mi">10000</span>
    <span class="o">}</span>
    <span class="n">myServiceGroovy</span><span class="o">(</span><span class="n">GroovyScriptFactory</span><span class="o">,</span> <span class="s1">'classpath:MyServiceImpl.groovy'</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">bean</span> <span class="o">-&gt;</span>
            <span class="n">bean</span><span class="o">.</span><span class="na">scope</span> <span class="o">=</span> <span class="s2">"prototype"</span>
            <span class="n">myProp</span> <span class="o">=</span> <span class="s1">' this is Bean Builder init prop'</span>
            <span class="n">bean</span><span class="o">.</span><span class="na">beanDefinition</span><span class="o">.</span><span class="na">setAttribute</span><span class="o">(</span><span class="n">ScriptFactoryPostProcessor</span><span class="o">.</span><span class="na">REFRESH_CHECK_DELAY_ATTRIBUTE</span><span class="o">,</span> <span class="mi">6000</span><span class="o">)</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>通过<code class="language-plaintext highlighter-rouge">GroovyScriptFactory</code>可以指定定义bean的groovy脚本位置。
通过<code class="language-plaintext highlighter-rouge">bean</code>的lambda表达式，可以对bean的属性进行赋值，除了我们定义的myProp这个属性外，还可以定义scope和脚本刷新时间。</p>

<h3 id="23调用groovy中实现的bean">2.3、调用groovy中实现的bean</h3>

<p>前面我们通过xml和groovy两种方式分别声明了2个bean: <code class="language-plaintext highlighter-rouge">myServiceXml</code>和<code class="language-plaintext highlighter-rouge">myServiceGroovy</code>，下面我们在程序中调用这2个bean。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@SpringBootApplication</span>
<span class="nd">@ImportResource</span><span class="o">({</span><span class="s">"classpath:xml-bean-config.xml"</span><span class="o">,</span> <span class="s">"classpath:BeanBuilder.groovy"</span><span class="o">})</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Application</span> <span class="kd">implements</span> <span class="nc">CommandLineRunner</span> <span class="o">{</span>

    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">MyService</span> <span class="n">myServiceXml</span><span class="o">;</span>
    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">MyService</span> <span class="n">myServiceGroovy</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="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">SpringApplication</span><span class="o">.</span><span class="na">run</span><span class="o">(</span><span class="nc">Application</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="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">run</span><span class="o">(</span><span class="nc">String</span><span class="o">...</span> <span class="n">args</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">ScriptException</span><span class="o">,</span> <span class="nc">ResourceException</span><span class="o">,</span> <span class="nc">IllegalAccessException</span><span class="o">,</span> <span class="nc">InstantiationException</span> <span class="o">{</span>
        <span class="nc">MyDomain</span> <span class="n">myDomain</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MyDomain</span><span class="o">();</span>
        <span class="n">myDomain</span><span class="o">.</span><span class="na">setName</span><span class="o">(</span><span class="s">"test"</span><span class="o">);</span>
        <span class="nc">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">myServiceXml</span><span class="o">.</span><span class="na">fun</span><span class="o">(</span><span class="n">myDomain</span><span class="o">));</span>
        <span class="n">myDomain</span><span class="o">.</span><span class="na">setName</span><span class="o">(</span><span class="s">"test2"</span><span class="o">);</span>
        <span class="nc">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">myServiceGroovy</span><span class="o">.</span><span class="na">fun</span><span class="o">(</span><span class="n">myDomain</span><span class="o">));</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>首先我们通过<code class="language-plaintext highlighter-rouge">@ImportResource</code>来引入bean的声明文件，然后就是普通的bean的依赖注入和方法调用，可以看到在bean的使用上，脚本定义的bean和用程序编写的bean没有任何区别。
在run方法中，我们分别调用了myServiceXml和myServiceGroovy的这2个bean的fun方法。
执行run方法可以看到输出到结果：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>MyDomain<span class="o">(</span><span class="nv">name</span><span class="o">=</span><span class="nb">test</span><span class="o">)</span>FunBean this is xml init prop
MyDomain<span class="o">(</span><span class="nv">name</span><span class="o">=</span>test2<span class="o">)</span>FunBean this is Bean Builder init prop
</code></pre></div></div>

<h2 id="三执行groovy脚本">三、执行groovy脚本</h2>

<p>除了前面提到的在groovy中实现bean以外，我们还可以通过groovy提供的GroovyScriptEngine来执行groovy脚本，这种方式不依赖于springframework，普通的java程序中也可以使用。</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">MyEngine</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">GroovyScriptEngine</span> <span class="n">engine</span><span class="o">;</span>

    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">FunBean</span> <span class="n">funBean</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">MyEngine</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span>

        <span class="n">engine</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">GroovyScriptEngine</span><span class="o">(</span><span class="nc">ResourceUtils</span><span class="o">.</span><span class="na">getFile</span><span class="o">(</span><span class="s">"classpath:scripts/"</span><span class="o">).</span><span class="na">getAbsolutePath</span><span class="o">()</span>
                <span class="o">,</span> <span class="k">this</span><span class="o">.</span><span class="na">getClass</span><span class="o">().</span><span class="na">getClassLoader</span><span class="o">());</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">runScript</span><span class="o">(</span><span class="kt">int</span> <span class="n">x</span><span class="o">,</span> <span class="kt">int</span> <span class="n">y</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">IllegalAccessException</span><span class="o">,</span>
            <span class="nc">InstantiationException</span><span class="o">,</span> <span class="nc">ResourceException</span><span class="o">,</span> <span class="nc">ScriptException</span> <span class="o">{</span>
        <span class="nc">Class</span><span class="o">&lt;</span><span class="nc">GroovyObject</span><span class="o">&gt;</span> <span class="n">calcClass</span> <span class="o">=</span> <span class="n">engine</span><span class="o">.</span><span class="na">loadScriptByName</span><span class="o">(</span><span class="s">"CalcScript.groovy"</span><span class="o">);</span>
        <span class="nc">GroovyObject</span> <span class="n">calc</span> <span class="o">=</span> <span class="n">calcClass</span><span class="o">.</span><span class="na">newInstance</span><span class="o">();</span>

        <span class="nc">Object</span> <span class="n">result</span> <span class="o">=</span> <span class="n">calc</span><span class="o">.</span><span class="na">invokeMethod</span><span class="o">(</span><span class="s">"calcSum"</span><span class="o">,</span> <span class="k">new</span> <span class="nc">Object</span><span class="o">[]{</span><span class="n">x</span><span class="o">,</span> <span class="n">y</span><span class="o">});</span>
        <span class="nc">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">"Result of CalcScript.calcSum() method is "</span> <span class="o">+</span> <span class="n">result</span><span class="o">);</span>

        <span class="nc">Binding</span> <span class="n">binding</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Binding</span><span class="o">();</span>
        <span class="n">binding</span><span class="o">.</span><span class="na">setVariable</span><span class="o">(</span><span class="s">"arg"</span><span class="o">,</span> <span class="s">"test"</span><span class="o">);</span>
        <span class="n">binding</span><span class="o">.</span><span class="na">setVariable</span><span class="o">(</span><span class="s">"funBean"</span><span class="o">,</span> <span class="n">funBean</span><span class="o">);</span>
        <span class="nc">Object</span> <span class="n">result1</span> <span class="o">=</span> <span class="n">engine</span><span class="o">.</span><span class="na">run</span><span class="o">(</span><span class="s">"CalcScript.groovy"</span><span class="o">,</span> <span class="n">binding</span><span class="o">);</span>
        <span class="nc">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">"Result of CalcScript.groovy is "</span> <span class="o">+</span> <span class="n">result1</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>首先我们初始化GroovyScriptEngine，在构造方法中传入脚本文件的路径。</p>

<p>执行脚本的方法有2种，一种是获取到GroovyObject，通过invokeMethod来执行脚本中的某个方法，方法的参数通过Object数组传入。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Class</span><span class="o">&lt;</span><span class="nc">GroovyObject</span><span class="o">&gt;</span> <span class="n">calcClass</span> <span class="o">=</span> <span class="n">engine</span><span class="o">.</span><span class="na">loadScriptByName</span><span class="o">(</span><span class="s">"CalcScript.groovy"</span><span class="o">);</span>
<span class="nc">GroovyObject</span> <span class="n">calc</span> <span class="o">=</span> <span class="n">calcClass</span><span class="o">.</span><span class="na">newInstance</span><span class="o">();</span>

<span class="nc">Object</span> <span class="n">result</span> <span class="o">=</span> <span class="n">calc</span><span class="o">.</span><span class="na">invokeMethod</span><span class="o">(</span><span class="s">"calcSum"</span><span class="o">,</span> <span class="k">new</span> <span class="nc">Object</span><span class="o">[]{</span><span class="n">x</span><span class="o">,</span> <span class="n">y</span><span class="o">});</span>

</code></pre></div></div>

<p>第二种是直接运行groovy脚本，可以通过Binding将变量传递到groovy脚本中。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Binding</span> <span class="n">binding</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Binding</span><span class="o">();</span>
<span class="n">binding</span><span class="o">.</span><span class="na">setVariable</span><span class="o">(</span><span class="s">"arg"</span><span class="o">,</span> <span class="s">"test"</span><span class="o">);</span>
<span class="n">binding</span><span class="o">.</span><span class="na">setVariable</span><span class="o">(</span><span class="s">"funBean"</span><span class="o">,</span> <span class="n">funBean</span><span class="o">);</span>
<span class="nc">Object</span> <span class="n">result1</span> <span class="o">=</span> <span class="n">engine</span><span class="o">.</span><span class="na">run</span><span class="o">(</span><span class="s">"CalcScript.groovy"</span><span class="o">,</span> <span class="n">binding</span><span class="o">);</span>
</code></pre></div></div>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><summary type="html"><![CDATA[在我们的应用中引入脚本能力，可以很好的提升灵活性，我们的核心开发工作可以集中在核心平台能力的开发上，具体场景的功能可以通过脚本来实现，例如jenkins就可以通过groovy脚本来编写pipeline，可以很灵活的定制构建过程。 spring本身提供了groovy集成的机制，分为两种方式，一种是用groovy开发程序，跟用java开发类似，需要经过编译。一种是将groovy作为脚本来执行，不需要编译。在此我们介绍的是第二种方式，将groovy作为脚本来使用。 具体的代码参照 示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-groovy]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/groovy.jpeg" /><media:content medium="image" url="https://springcamp.cn/assets/images/groovy.jpeg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">springboot单元测试技术</title><link href="https://springcamp.cn/spring-boot-unit-test/" rel="alternate" type="text/html" title="springboot单元测试技术" /><published>2021-04-18T09:20:00+00:00</published><updated>2021-04-18T09:20:00+00:00</updated><id>https://springcamp.cn/spring-boot-unit-test</id><content type="html" xml:base="https://springcamp.cn/spring-boot-unit-test/"><![CDATA[<p>整个软件交付过程中，单元测试阶段是一个能够最早发现问题，并且可以重复回归问题的阶段，在单元测试阶段做的测试越充分，软件质量就越能得到保证。
具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-unit-test">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-unit-test</a></p>

<h2 id="一概述">一、概述</h2>

<p>一个功能的全链路测试，往往要依赖于很多外部组件，如数据库、redis、kafka、第三方接口等，单元测试的执行环境有可能受网络限制没有办法访问这些外部服务。因此，我们希望通过一些技术手段，能够用单元测试技术进行完整的功能测试，而不依赖于外部服务。</p>

<h2 id="二rest接口的测试">二、REST接口的测试</h2>

<p>springboot提供了testRestTemplate工具用于在单元测试中测试接口，该工具只需指定接口的相对路径，不需要指定域名和端口。这个特性非常有用，因为springboot的单元测试运行环境的web服务是一个随机端口，是通过下面这个注解指定的：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@SpringBootTest</span><span class="o">(</span><span class="n">webEnvironment</span> <span class="o">=</span> <span class="nc">SpringBootTest</span><span class="o">.</span><span class="na">WebEnvironment</span><span class="o">.</span><span class="na">RANDOM_PORT</span><span class="o">)</span>
</code></pre></div></div>

<p>以下是通过testRestTemplate测试我们开发的<code class="language-plaintext highlighter-rouge">/remote</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">testRemoteCallRest</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">resp</span> <span class="o">=</span> <span class="n">testRestTemplate</span><span class="o">.</span><span class="na">getForObject</span><span class="o">(</span><span class="s">"/remote"</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
        <span class="nc">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">"remote result : "</span> <span class="o">+</span> <span class="n">resp</span><span class="o">);</span>
        <span class="n">assertThat</span><span class="o">(</span><span class="n">resp</span><span class="o">,</span> <span class="n">is</span><span class="o">(</span><span class="s">"{\"code\": 200}"</span><span class="o">));</span>
    <span class="o">}</span>
</code></pre></div></div>

<h2 id="三第三方接口的依赖">三、第三方接口的依赖</h2>

<p>上面的例子中，我们的remote接口会调用一个第三方接口 <code class="language-plaintext highlighter-rouge">http://someservice/foo</code>，我们的构建服务器中有可能受网络限制，无法访问这个第三方接口，就会导致单元测试无法执行。我们可以通过springboot提供的 <code class="language-plaintext highlighter-rouge">MockRestServiceServer</code> 工具来解决这个问题。</p>

<p>首先定义一个MockRestServiceServer变量</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="nc">MockRestServiceServer</span> <span class="n">mockRestServiceServer</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">@Before</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">before</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">mockRestServiceServer</span> <span class="o">=</span> <span class="nc">MockRestServiceServer</span><span class="o">.</span><span class="na">bindTo</span><span class="o">(</span><span class="n">restTemplate</span><span class="o">).</span><span class="na">ignoreExpectOrder</span><span class="o">(</span><span class="kc">true</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>

        <span class="k">this</span><span class="o">.</span><span class="na">mockRestServiceServer</span><span class="o">.</span><span class="na">expect</span><span class="o">(</span><span class="n">manyTimes</span><span class="o">(),</span> <span class="nc">MockRestRequestMatchers</span><span class="o">.</span><span class="na">requestTo</span><span class="o">(</span><span class="nc">Matchers</span><span class="o">.</span><span class="na">startsWithIgnoringCase</span><span class="o">(</span><span class="s">"http://someservice/foo"</span><span class="o">)))</span>
                <span class="o">.</span><span class="na">andRespond</span><span class="o">(</span><span class="n">withSuccess</span><span class="o">(</span><span class="s">"{\"code\": 200}"</span><span class="o">,</span> <span class="nc">MediaType</span><span class="o">.</span><span class="na">APPLICATION_JSON</span><span class="o">));</span>

    <span class="o">}</span>
</code></pre></div></div>

<p>这样，当我们的单元测试程序中调用<code class="language-plaintext highlighter-rouge">http://someservice/foo</code>接口时，就会固定返回<code class="language-plaintext highlighter-rouge">{"code": 200}</code>这个返回值，而不是真正的去访问这个第三方接口。</p>

<h2 id="四数据库的依赖">四、数据库的依赖</h2>

<p>数据库的依赖比较简单，直接使用h2这个嵌入式数据库就可以，所有的数据库操作都是在h2这个嵌入式数据库中执行的。</p>

<p>已gradle配置为例：</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">testImplementation</span> <span class="s1">'com.h2database:h2'</span>
</code></pre></div></div>

<p>单元测试配置文件中的数据库连接使用h2:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">spring</span><span class="pi">:</span>
  <span class="na">data</span><span class="pi">:</span>
    <span class="na">url</span><span class="pi">:</span> <span class="s">jdbc:h2:mem:ut;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</span>
    <span class="na">username</span><span class="pi">:</span> <span class="s">sa</span>
    <span class="na">password</span><span class="pi">:</span>
</code></pre></div></div>

<p>单元测试程序中可以直接进行数据库操作：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">MyDomain</span> <span class="n">myDomain</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MyDomain</span><span class="o">();</span>
<span class="n">myDomain</span><span class="o">.</span><span class="na">setName</span><span class="o">(</span><span class="s">"test"</span><span class="o">);</span>
<span class="n">myDomain</span> <span class="o">=</span> <span class="n">myDomainRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">myDomain</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="nc">MyDomain</span> <span class="n">resp</span> <span class="o">=</span> <span class="n">testRestTemplate</span><span class="o">.</span><span class="na">getForObject</span><span class="o">(</span><span class="s">"/db?id="</span> <span class="o">+</span> <span class="n">myDomain</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="nc">MyDomain</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
<span class="nc">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">"db result : "</span> <span class="o">+</span> <span class="n">resp</span><span class="o">);</span>
<span class="n">assertThat</span><span class="o">(</span><span class="n">resp</span><span class="o">.</span><span class="na">getName</span><span class="o">(),</span> <span class="n">is</span><span class="o">(</span><span class="s">"test"</span><span class="o">));</span>
</code></pre></div></div>

<p>当接口返回Page分页数据时，需要做一点特殊处理，否则会报json序列化错误。</p>

<p>定义自己的Page类：</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">TestRestResponsePage</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="kd">extends</span> <span class="nc">PageImpl</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="o">{</span>
    <span class="nd">@JsonCreator</span><span class="o">(</span><span class="n">mode</span> <span class="o">=</span> <span class="nc">JsonCreator</span><span class="o">.</span><span class="na">Mode</span><span class="o">.</span><span class="na">PROPERTIES</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nf">TestRestResponsePage</span><span class="o">(</span><span class="nd">@JsonProperty</span><span class="o">(</span><span class="s">"content"</span><span class="o">)</span> <span class="nc">List</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">content</span><span class="o">,</span>
                                <span class="nd">@JsonProperty</span><span class="o">(</span><span class="s">"number"</span><span class="o">)</span> <span class="kt">int</span> <span class="n">number</span><span class="o">,</span>
                                <span class="nd">@JsonProperty</span><span class="o">(</span><span class="s">"size"</span><span class="o">)</span> <span class="kt">int</span> <span class="n">size</span><span class="o">,</span>
                                <span class="nd">@JsonProperty</span><span class="o">(</span><span class="s">"pageable"</span><span class="o">)</span> <span class="nc">JsonNode</span> <span class="n">pageable</span><span class="o">,</span>
                                <span class="nd">@JsonProperty</span><span class="o">(</span><span class="s">"empty"</span><span class="o">)</span> <span class="kt">boolean</span> <span class="n">empty</span><span class="o">,</span>
                                <span class="nd">@JsonProperty</span><span class="o">(</span><span class="s">"sort"</span><span class="o">)</span> <span class="nc">JsonNode</span> <span class="n">sort</span><span class="o">,</span>
                                <span class="nd">@JsonProperty</span><span class="o">(</span><span class="s">"first"</span><span class="o">)</span> <span class="kt">boolean</span> <span class="n">first</span><span class="o">,</span>
                                <span class="nd">@JsonProperty</span><span class="o">(</span><span class="s">"totalElements"</span><span class="o">)</span> <span class="kt">long</span> <span class="n">totalElements</span><span class="o">,</span>
                                <span class="nd">@JsonProperty</span><span class="o">(</span><span class="s">"totalPages"</span><span class="o">)</span> <span class="kt">int</span> <span class="n">totalPages</span><span class="o">,</span>
                                <span class="nd">@JsonProperty</span><span class="o">(</span><span class="s">"numberOfElements"</span><span class="o">)</span> <span class="kt">int</span> <span class="n">numberOfElements</span><span class="o">)</span> <span class="o">{</span>

        <span class="kd">super</span><span class="o">(</span><span class="n">content</span><span class="o">,</span> <span class="nc">PageRequest</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">number</span><span class="o">,</span> <span class="n">size</span><span class="o">),</span> <span class="n">totalElements</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nf">TestRestResponsePage</span><span class="o">(</span><span class="nc">List</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">content</span><span class="o">)</span> <span class="o">{</span>
        <span class="kd">super</span><span class="o">(</span><span class="n">content</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nf">TestRestResponsePage</span><span class="o">()</span> <span class="o">{</span>
        <span class="kd">super</span><span class="o">(</span><span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;());</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>调用接口返回自定义的Page类：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">RequestEntity</span><span class="o">&lt;</span><span class="nc">Void</span><span class="o">&gt;</span> <span class="n">requestEntity</span> <span class="o">=</span> <span class="nc">RequestEntity</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="s">"/dbpage"</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>
<span class="nc">ResponseEntity</span><span class="o">&lt;</span><span class="nc">TestRestResponsePage</span><span class="o">&lt;</span><span class="nc">MyDomain</span><span class="o">&gt;&gt;</span> <span class="n">pageResp</span> <span class="o">=</span> <span class="n">testRestTemplate</span><span class="o">.</span><span class="na">exchange</span><span class="o">(</span><span class="n">requestEntity</span><span class="o">,</span> <span class="k">new</span> <span class="nc">ParameterizedTypeReference</span><span class="o">&lt;</span><span class="nc">TestRestResponsePage</span><span class="o">&lt;</span><span class="nc">MyDomain</span><span class="o">&gt;&gt;()</span> <span class="o">{</span>
    <span class="o">});</span>
<span class="nc">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">"dbpage result : "</span> <span class="o">+</span> <span class="n">pageResp</span><span class="o">);</span>
<span class="n">assertThat</span><span class="o">(</span><span class="n">pageResp</span><span class="o">.</span><span class="na">getBody</span><span class="o">().</span><span class="na">getTotalElements</span><span class="o">(),</span> <span class="n">is</span><span class="o">(</span><span class="mi">1L</span><span class="o">));</span>
</code></pre></div></div>

<p>由于返回结果是泛型，所以需要使用<code class="language-plaintext highlighter-rouge">testRestTemplate.exchange</code>方法，get方法不支持返回泛型。</p>

<h2 id="五redis的依赖">五、redis的依赖</h2>

<p>网上有一个开源的redis mockserver，模仿了大部分的redis指令，我们只需要引入这个redis-mockserver即可。
最初版本是一个国人开发的，示例中引入的是老外fork的一个版本，补充了一些指令，但是找不到源码了，我又fork了一个版本，补充了setex、zscore两个指令，有需要的可以自己编译。<a href="https://github.com/qihaiyan/redis-mock">代码连接 https://github.com/qihaiyan/redis-mock</a></p>

<p>已gradle配置为例：</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">testImplementation</span> <span class="s1">'com.github.fppt:jedis-mock:1.0.1'</span>
</code></pre></div></div>

<p>单元测试配置文件中的数据库连接使用redis mockserver:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">spring</span><span class="pi">:</span>
  <span class="na">redis</span><span class="pi">:</span>
    <span class="na">port</span><span class="pi">:</span> <span class="m">10033</span>
</code></pre></div></div>

<p>增加一个单独的redis配置文件，用于在单元测试中启动redis mockserver：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@TestConfiguration</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">TestRedisConfiguration</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">RedisServer</span> <span class="n">redisServer</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">TestRedisConfiguration</span><span class="o">(</span><span class="nd">@Value</span><span class="o">(</span><span class="s">"${spring.redis.port}"</span><span class="o">)</span> <span class="kd">final</span> <span class="kt">int</span> <span class="n">redisPort</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span>
        <span class="n">redisServer</span> <span class="o">=</span> <span class="nc">RedisServer</span><span class="o">.</span><span class="na">newRedisServer</span><span class="o">(</span><span class="n">redisPort</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@PostConstruct</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">postConstruct</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span>
        <span class="n">redisServer</span><span class="o">.</span><span class="na">start</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@PreDestroy</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">preDestroy</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">redisServer</span><span class="o">.</span><span class="na">stop</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="六kafka的依赖">六、kafka的依赖</h2>

<p>spring提供了一个kafka的测试组件，可以在单元测试期间启动一个嵌入式的kafka服务EmbeddedKafka，模拟真实的kafka操作。</p>

<p>以gradle配置为例：</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">testImplementation</span> <span class="s2">"org.springframework.kafka:spring-kafka-test"</span>
</code></pre></div></div>

<p>通过ClassRule初始化EmbeddedKafka，有两个topic: testEmbeddedIn 和 testEmbeddedOut 。</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="kd">final</span> <span class="nc">String</span> <span class="no">INPUT_TOPIC</span> <span class="o">=</span> <span class="s">"testEmbeddedIn"</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">OUTPUT_TOPIC</span> <span class="o">=</span> <span class="s">"testEmbeddedOut"</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">GROUP_NAME</span> <span class="o">=</span> <span class="s">"embeddedKafkaApplication"</span><span class="o">;</span>

    <span class="nd">@ClassRule</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">EmbeddedKafkaRule</span> <span class="n">embeddedKafkaRule</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">EmbeddedKafkaRule</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="kc">true</span><span class="o">,</span> <span class="no">INPUT_TOPIC</span><span class="o">,</span> <span class="no">OUTPUT_TOPIC</span><span class="o">);</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">EmbeddedKafkaBroker</span> <span class="n">embeddedKafka</span> <span class="o">=</span> <span class="n">embeddedKafkaRule</span><span class="o">.</span><span class="na">getEmbeddedKafka</span><span class="o">();</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="nc">KafkaTemplate</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">kafkaTemplate</span><span class="o">;</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="nc">Consumer</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">consumer</span><span class="o">;</span>

    <span class="nd">@BeforeClass</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">setup</span><span class="o">()</span> <span class="o">{</span>

        <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;</span> <span class="n">senderProps</span> <span class="o">=</span> <span class="nc">KafkaTestUtils</span><span class="o">.</span><span class="na">producerProps</span><span class="o">(</span><span class="n">embeddedKafka</span><span class="o">);</span>
        <span class="nc">DefaultKafkaProducerFactory</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">pf</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DefaultKafkaProducerFactory</span><span class="o">&lt;&gt;(</span><span class="n">senderProps</span><span class="o">);</span>
        <span class="n">kafkaTemplate</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">KafkaTemplate</span><span class="o">&lt;&gt;(</span><span class="n">pf</span><span class="o">,</span> <span class="kc">true</span><span class="o">);</span>

        <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;</span> <span class="n">consumerProps</span> <span class="o">=</span> <span class="nc">KafkaTestUtils</span><span class="o">.</span><span class="na">consumerProps</span><span class="o">(</span><span class="no">GROUP_NAME</span><span class="o">,</span> <span class="s">"false"</span><span class="o">,</span> <span class="n">embeddedKafka</span><span class="o">);</span>
        <span class="nc">DefaultKafkaConsumerFactory</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">cf</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DefaultKafkaConsumerFactory</span><span class="o">&lt;&gt;(</span><span class="n">consumerProps</span><span class="o">);</span>
        <span class="n">consumer</span> <span class="o">=</span> <span class="n">cf</span><span class="o">.</span><span class="na">createConsumer</span><span class="o">();</span>
        <span class="n">embeddedKafka</span><span class="o">.</span><span class="na">consumeFromAnEmbeddedTopic</span><span class="o">(</span><span class="n">consumer</span><span class="o">,</span> <span class="no">OUTPUT_TOPIC</span><span class="o">);</span>
    <span class="o">}</span>

</code></pre></div></div>

<p>在单元测试程序的配置文件中，可以指定这2个kafka的topic</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">cloud.stream.bindings</span><span class="pi">:</span>
    <span class="na">handle-out-0.destination</span><span class="pi">:</span> <span class="s">testEmbeddedOut</span>
    <span class="na">handle-in-0.destination</span><span class="pi">:</span> <span class="s">testEmbeddedIn</span>
    <span class="na">handle-in-0.group</span><span class="pi">:</span> <span class="s">embeddedKafkaApplication</span>
</code></pre></div></div>

<h2 id="七测试时修改配置属性">七、测试时修改配置属性</h2>

<p>执行测试逻辑时，会遇到需要临时修改一个配置项的值，但是配置文件的内容无法在单元测试执行期间修改，这种情况可以通过 ReflectionTestUtils 处理。</p>

<p>正式代码中有一个配置项：</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">common</span><span class="pi">:</span>
  <span class="na">value</span><span class="pi">:</span> <span class="s">origin</span>
</code></pre></div></div>

<p>该配置项在MyService中通过属性 originValue 进行引用：</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">"${common.value}"</span><span class="o">)</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">originValue</span><span class="o">;</span>
</code></pre></div></div>

<p>在执行某个单元测试时期望将该配置项属性修改为 test:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">ReflectionTestUtils</span><span class="o">.</span><span class="na">setField</span><span class="o">(</span><span class="n">myService</span><span class="o">,</span> <span class="s">"originValue"</span><span class="o">,</span> <span class="s">"test"</span><span class="o">);</span>
</code></pre></div></div>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><summary type="html"><![CDATA[整个软件交付过程中，单元测试阶段是一个能够最早发现问题，并且可以重复回归问题的阶段，在单元测试阶段做的测试越充分，软件质量就越能得到保证。 具体的代码参照 示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-unit-test]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/ut.jpg" /><media:content medium="image" url="https://springcamp.cn/assets/images/ut.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">spring自带线程池使用不当导致的死锁问题</title><link href="https://springcamp.cn/java-concurrent-thread-block/" rel="alternate" type="text/html" title="spring自带线程池使用不当导致的死锁问题" /><published>2021-01-10T10:50:00+00:00</published><updated>2021-01-10T10:50:00+00:00</updated><id>https://springcamp.cn/java-concurrent-thread-block</id><content type="html" xml:base="https://springcamp.cn/java-concurrent-thread-block/"><![CDATA[<p>Spring自带线程池使用很方便，不过在相对复杂的并发编程场景中，使用时还是需要根据使用场景仔细考虑配置，否则可能会遇到本文中提及的坑。
具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-taskexecutor-block">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-taskexecutor-block</a></p>

<h2 id="一概述">一、概述</h2>

<p>spring自带线程池有2个核心配置，一个是线程池的大小，一个是队列的大小。
ThredPoolTaskExcutor的处理流程：
新建线程并处理请求，直到线程数大小等于corePoolSize；
将请求放入workQueue中，线程池中的空闲线程去workQueue中取任务并处理；
当workQueue满时，就新建线程并处理请求，当线程池子大小大小等于maximumPoolSize时，会用RejectedExecutionHandler来做拒绝处理。</p>

<p>Reject策略有四种：</p>

<p>(1)AbortPolicy策略，是默认的策略，拒绝请求并抛出异常RejectedExecutionException。</p>

<p>(2)CallerRunsPolicy策略 ,由调用线程执行任务.</p>

<p>(3)DiscardPolicy策略，拒绝请求但不抛出异常.</p>

<p>(4)DiscardOldestPolicy策略，丢弃最早进入队列的任务.</p>

<!-- more -->

<h2 id="二多个异步处理共用同一个线程池的异常情况">二、多个异步处理共用同一个线程池的异常情况</h2>

<p>模拟一个耗时的操作，该操作通过Async注解设置为异步执行。Async会默认使用名为taskExecutor的线程池。该操作返回一个CompletableFuture，后续的处理中会等待该异步操作执行完成。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">DelayService</span> <span class="o">{</span>
    <span class="nd">@Async</span>
    <span class="kd">public</span> <span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="nf">delayFoo</span><span class="o">(</span><span class="nc">String</span> <span class="n">v</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="mi">1000L</span><span class="o">);</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">InterruptedException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">e</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">();</span>
        <span class="o">}</span>
        <span class="nc">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">v</span> <span class="o">+</span> <span class="s">" runs in thread: "</span> <span class="o">+</span> <span class="nc">Thread</span><span class="o">.</span><span class="na">currentThread</span><span class="o">().</span><span class="na">getName</span><span class="o">());</span>
        <span class="k">return</span> <span class="nc">CompletableFuture</span><span class="o">.</span><span class="na">completedFuture</span><span class="o">(</span><span class="n">v</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>设置线程池，将线程池大小设置为2，队列设置为一个比线程池大的值，此处为10。当队列大小大于等于线程池大小时，就会出现本文遇到的程序阻塞的问题。</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="nc">Executor</span> <span class="nf">taskExecutor</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">ThreadPoolTaskExecutor</span> <span class="n">executor</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ThreadPoolTaskExecutor</span><span class="o">();</span>
        <span class="n">executor</span><span class="o">.</span><span class="na">setCorePoolSize</span><span class="o">(</span><span class="mi">2</span><span class="o">);</span>
        <span class="n">executor</span><span class="o">.</span><span class="na">setMaxPoolSize</span><span class="o">(</span><span class="mi">2</span><span class="o">);</span>
        <span class="n">executor</span><span class="o">.</span><span class="na">setQueueCapacity</span><span class="o">(</span><span class="mi">10</span><span class="o">);</span>
        <span class="n">executor</span><span class="o">.</span><span class="na">setThreadNamePrefix</span><span class="o">(</span><span class="s">"taskExecutor-"</span><span class="o">);</span>
        <span class="n">executor</span><span class="o">.</span><span class="na">setRejectedExecutionHandler</span><span class="o">(</span><span class="k">new</span> <span class="nc">ThreadPoolExecutor</span><span class="o">.</span><span class="na">CallerRunsPolicy</span><span class="o">());</span>
        <span class="n">executor</span><span class="o">.</span><span class="na">setWaitForTasksToCompleteOnShutdown</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
        <span class="n">executor</span><span class="o">.</span><span class="na">initialize</span><span class="o">();</span>
        <span class="k">return</span> <span class="n">executor</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="k">while</span> <span class="o">(</span><span class="kc">true</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="nc">CompletableFuture</span><span class="o">.</span><span class="na">runAsync</span><span class="o">(</span>
                <span class="o">()</span> <span class="o">-&gt;</span> <span class="nc">CompletableFuture</span><span class="o">.</span><span class="na">allOf</span><span class="o">(</span><span class="nc">Stream</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"1"</span><span class="o">,</span> <span class="s">"2"</span><span class="o">,</span> <span class="s">"3"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="n">v</span> <span class="o">-&gt;</span> <span class="n">delayService</span><span class="o">.</span><span class="na">delayFoo</span><span class="o">(</span><span class="n">v</span><span class="o">))</span>
                <span class="o">.</span><span class="na">toArray</span><span class="o">(</span><span class="nc">CompletableFuture</span><span class="o">[]::</span><span class="k">new</span><span class="o">))</span> <span class="c1">// 将数组中的任务提交到线程池中</span>
                <span class="o">.</span><span class="na">join</span><span class="o">(),</span> <span class="n">taskExecutor</span><span class="o">);</span> <span class="c1">// 通过join方法等待任务完成</span>
            <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">e</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">();</span>
            <span class="o">}</span>
        <span class="o">}</span>
</code></pre></div></div>

<h2 id="三问题分析">三、问题分析</h2>

<p>程序启动后，很快就会阻塞，通过jstack查看线程状态，发现taskExecutor-1、taskExecutor-2、main三个线程都处在WAITING状态，等待CompletableFuture.join方法执行完成。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">priority:</span><span class="mi">5</span> <span class="o">-</span> <span class="nl">threadId:</span><span class="mh">0x00007f7f8eb36800</span> <span class="o">-</span> <span class="nl">nativeId:</span><span class="mh">0x3e03</span> <span class="o">-</span> <span class="n">nativeId</span> <span class="o">(</span><span class="n">decimal</span><span class="o">):</span><span class="mi">15875</span> <span class="o">-</span> <span class="nl">state:</span><span class="no">WAITING</span>
<span class="nl">stackTrace:</span>
<span class="n">java</span><span class="o">.</span><span class="na">lang</span><span class="o">.</span><span class="na">Thread</span><span class="o">.</span><span class="na">State</span><span class="o">:</span> <span class="no">WAITING</span> <span class="o">(</span><span class="n">parking</span><span class="o">)</span>
<span class="n">at</span> <span class="n">sun</span><span class="o">.</span><span class="na">misc</span><span class="o">.</span><span class="na">Unsafe</span><span class="o">.</span><span class="na">park</span><span class="o">(</span><span class="nc">Native</span> <span class="nc">Method</span><span class="o">)</span>
<span class="o">-</span> <span class="n">parking</span> <span class="n">to</span> <span class="n">wait</span> <span class="k">for</span> <span class="o">&lt;</span><span class="mh">0x00000007961fe548</span><span class="o">&gt;</span> <span class="o">(</span><span class="n">a</span> <span class="n">java</span><span class="o">.</span><span class="na">util</span><span class="o">.</span><span class="na">concurrent</span><span class="o">.</span><span class="na">CompletableFuture</span><span class="n">$Signaller</span><span class="o">)</span>
<span class="n">at</span> <span class="n">java</span><span class="o">.</span><span class="na">util</span><span class="o">.</span><span class="na">concurrent</span><span class="o">.</span><span class="na">locks</span><span class="o">.</span><span class="na">LockSupport</span><span class="o">.</span><span class="na">park</span><span class="o">(</span><span class="nc">LockSupport</span><span class="o">.</span><span class="na">java</span><span class="o">:</span><span class="mi">175</span><span class="o">)</span>
<span class="n">at</span> <span class="n">java</span><span class="o">.</span><span class="na">util</span><span class="o">.</span><span class="na">concurrent</span><span class="o">.</span><span class="na">CompletableFuture</span><span class="n">$Signaller</span><span class="o">.</span><span class="na">block</span><span class="o">(</span><span class="nc">CompletableFuture</span><span class="o">.</span><span class="na">java</span><span class="o">:</span><span class="mi">1693</span><span class="o">)</span>
<span class="n">at</span> <span class="n">java</span><span class="o">.</span><span class="na">util</span><span class="o">.</span><span class="na">concurrent</span><span class="o">.</span><span class="na">ForkJoinPool</span><span class="o">.</span><span class="na">managedBlock</span><span class="o">(</span><span class="nc">ForkJoinPool</span><span class="o">.</span><span class="na">java</span><span class="o">:</span><span class="mi">3323</span><span class="o">)</span>
<span class="n">at</span> <span class="n">java</span><span class="o">.</span><span class="na">util</span><span class="o">.</span><span class="na">concurrent</span><span class="o">.</span><span class="na">CompletableFuture</span><span class="o">.</span><span class="na">waitingGet</span><span class="o">(</span><span class="nc">CompletableFuture</span><span class="o">.</span><span class="na">java</span><span class="o">:</span><span class="mi">1729</span><span class="o">)</span>
<span class="n">at</span> <span class="n">java</span><span class="o">.</span><span class="na">util</span><span class="o">.</span><span class="na">concurrent</span><span class="o">.</span><span class="na">CompletableFuture</span><span class="o">.</span><span class="na">join</span><span class="o">(</span><span class="nc">CompletableFuture</span><span class="o">.</span><span class="na">java</span><span class="o">:</span><span class="mi">1934</span><span class="o">)</span>
</code></pre></div></div>

<p>通过分析程序的执行过程，不难发现阻塞的原因。
由于线程池设置的Queue的大小大于线程池的大小，当线程池满时，delayFoo方法会处在队列中，随着程序的执行，总会出现线程池中都是CompletableFuture.join方法，队列中都是delayFoo方法的情况。</p>

<p>这时候线程中的join方法在等待队列中的delayFoo方法执行完成，而队列中的delayFoo方法由于等不到可用线程而无法执行，整个程序就陷入了死锁状态。</p>

<p>解决的方法也很简单，就是将队列的大小设置为小于线程数的大小，这样队列中的方法就有机会拿到线程，从而不会因为线程占满而进入死锁状态。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><summary type="html"><![CDATA[Spring自带线程池使用很方便，不过在相对复杂的并发编程场景中，使用时还是需要根据使用场景仔细考虑配置，否则可能会遇到本文中提及的坑。 具体的代码参照 示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-taskexecutor-block]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/threadblock.jpg" /><media:content medium="image" url="https://springcamp.cn/assets/images/threadblock.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">spring打印http接口请求和响应</title><link href="https://springcamp.cn/spring-rest-log-request-response/" rel="alternate" type="text/html" title="spring打印http接口请求和响应" /><published>2020-11-22T03:50:00+00:00</published><updated>2020-11-22T03:50:00+00:00</updated><id>https://springcamp.cn/spring-rest-log-request-response</id><content type="html" xml:base="https://springcamp.cn/spring-rest-log-request-response/"><![CDATA[<p>在程序日志中打印出接口请求和响应的内容是一个基本的技术需求。如果在每个接口中实现请求响应的日志打印，程序编写会很繁琐，我们可以利用spring提供的机制，集中处理接口请求响应的日志打印。
具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-rest-log-request-response">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-rest-log-request-response</a></p>

<h2 id="一概述">一、概述</h2>

<p>基于spring提供的机制，有3种方法可以实现接口请求响应日志的打印，分别是CommonsRequestLoggingFilter、HandlerInterceptor、RequestBodyAdviceAdapter。</p>

<!-- more -->

<h2 id="二修改日志级别打印请求参数">二、修改日志级别打印请求参数</h2>

<p>通过设置 web 的日志级别为 DEBUG，spring会自己打印请求参数。该方法打印的内容覆盖了后面介绍的所有方法中日志的内容，如果不需要做定制打印，并且不介意打印的日志级别是DEBUG，那就足够用了。</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">logging</span><span class="pi">:</span>
  <span class="na">level</span><span class="pi">:</span>
    <span class="na">root</span><span class="pi">:</span> <span class="s">INFO</span>
    <span class="na">web</span><span class="pi">:</span> <span class="s">DEBUG</span>
</code></pre></div></div>

<h2 id="三使用-commonsrequestloggingfilter-打印请求参数">三、使用 CommonsRequestLoggingFilter 打印请求参数</h2>

<p>CommonsRequestLoggingFilter的使用比较简单，只需要实现一个logFilter的bean即可。
只不过logFilter的日志级别是debug，需要在日志配置文件中，将CommonsRequestLoggingFilter类的日志级别设置为debug级别。
同时在生产环境的日志文件中打印debug日志不符合规范。</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="nc">CommonsRequestLoggingFilter</span> <span class="nf">logFilter</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">CommonsRequestLoggingFilter</span> <span class="n">loggingFilter</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">CommonsRequestLoggingFilter</span><span class="o">();</span>

    <span class="n">loggingFilter</span><span class="o">.</span><span class="na">setIncludeQueryString</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
    <span class="n">loggingFilter</span><span class="o">.</span><span class="na">setIncludePayload</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
    <span class="n">loggingFilter</span><span class="o">.</span><span class="na">setMaxPayloadLength</span><span class="o">(</span><span class="mi">2048</span><span class="o">);</span>

    <span class="k">return</span> <span class="n">loggingFilter</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="四使用-handlerinterceptor-打印请求参数">四、使用 HandlerInterceptor 打印请求参数</h2>

<p>HandlerInterceptor 可以获取到接口执行过程中的 HttpServletRequest 和 HttpServletResponse 信息，因此能够打印出接口请求响应内容。</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">LogInterceptorAdapter</span> <span class="kd">extends</span> <span class="nc">HandlerInterceptorAdapter</span> <span class="o">{</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">preHandle</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span>
                             <span class="nc">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span>
                             <span class="nc">Object</span> <span class="n">handler</span><span class="o">)</span> <span class="o">{</span>

        <span class="nc">ServletRequest</span> <span class="n">servletRequest</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ContentCachingRequestWrapper</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
        <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">[]&gt;</span> <span class="n">params</span> <span class="o">=</span> <span class="n">servletRequest</span><span class="o">.</span><span class="na">getParameterMap</span><span class="o">();</span>

        <span class="c1">// 从 request 中读取请求参数并打印</span>
        <span class="n">params</span><span class="o">.</span><span class="na">forEach</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">-&gt;</span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"logInterceptor "</span> <span class="o">+</span> <span class="n">key</span> <span class="o">+</span> <span class="s">"="</span> <span class="o">+</span> <span class="nc">Arrays</span><span class="o">.</span><span class="na">toString</span><span class="o">(</span><span class="n">value</span><span class="o">)));</span>
        <span class="c1">// 避免从 inputStream 中读取body并打印</span>

        <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>这种方式有个缺陷，对于 application/json 这种请求参数放在body中的方式，需要通过InputStream读取内容，而InputStream只能被读取一次，
一旦在 HandlerInterceptor 中进行了 InputStream 的读取操作，后续的处理就读取不到InputStream中的内容，这是一个很严重的问题。
因此 HandlerInterceptor 不能用于打印请求中的body，可以改造一下该方法，只打印get请求参数，post的请求参数用下面介绍的 RequestBodyAdviceAdapter 方法打印。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Slf4j</span>
<span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">LogInterceptorAdapter</span> <span class="kd">extends</span> <span class="nc">HandlerInterceptorAdapter</span> <span class="o">{</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">preHandle</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span>
                             <span class="nc">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span>
                             <span class="nc">Object</span> <span class="n">handler</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="nc">DispatcherType</span><span class="o">.</span><span class="na">REQUEST</span><span class="o">.</span><span class="na">name</span><span class="o">().</span><span class="na">equals</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getDispatcherType</span><span class="o">().</span><span class="na">name</span><span class="o">())</span>
                <span class="o">&amp;&amp;</span> <span class="n">request</span><span class="o">.</span><span class="na">getMethod</span><span class="o">().</span><span class="na">equals</span><span class="o">(</span><span class="nc">HttpMethod</span><span class="o">.</span><span class="na">GET</span><span class="o">.</span><span class="na">name</span><span class="o">()))</span> <span class="o">{</span>

            <span class="nc">ServletRequest</span> <span class="n">servletRequest</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ContentCachingRequestWrapper</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
            <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">[]&gt;</span> <span class="n">params</span> <span class="o">=</span> <span class="n">servletRequest</span><span class="o">.</span><span class="na">getParameterMap</span><span class="o">();</span>

            <span class="c1">// 从 request 中读取请求参数并打印</span>
            <span class="n">params</span><span class="o">.</span><span class="na">forEach</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">-&gt;</span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"logInterceptor "</span> <span class="o">+</span> <span class="n">key</span> <span class="o">+</span> <span class="s">"="</span> <span class="o">+</span> <span class="nc">Arrays</span><span class="o">.</span><span class="na">toString</span><span class="o">(</span><span class="n">value</span><span class="o">)));</span>
            <span class="c1">// 避免从 inputStream 中读取body并打印</span>

        <span class="o">}</span>
        <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="五使用-requestbodyadviceadapter-打印请求参数">五、使用 RequestBodyAdviceAdapter 打印请求参数</h2>

<p>RequestBodyAdviceAdapter 封装了 afterBodyRead 方法，在这个方法中可以通过 Object body 参数获取到body的内容。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@ControllerAdvice</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">CustomRequestBodyAdviceAdapter</span> <span class="kd">extends</span> <span class="nc">RequestBodyAdviceAdapter</span> <span class="o">{</span>

    <span class="nd">@Autowired</span>
    <span class="nc">HttpServletRequest</span> <span class="n">httpServletRequest</span><span class="o">;</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">supports</span><span class="o">(</span><span class="nc">MethodParameter</span> <span class="n">methodParameter</span><span class="o">,</span> <span class="nc">Type</span> <span class="n">type</span><span class="o">,</span> 
                            <span class="nc">Class</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">HttpMessageConverter</span><span class="o">&lt;?&gt;&gt;</span> <span class="n">aClass</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">Object</span> <span class="nf">afterBodyRead</span><span class="o">(</span><span class="nc">Object</span> <span class="n">body</span><span class="o">,</span> <span class="nc">HttpInputMessage</span> <span class="n">inputMessage</span><span class="o">,</span>
                                <span class="nc">MethodParameter</span> <span class="n">parameter</span><span class="o">,</span> <span class="nc">Type</span> <span class="n">targetType</span><span class="o">,</span>
            <span class="nc">Class</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">HttpMessageConverter</span><span class="o">&lt;?&gt;&gt;</span> <span class="n">converterType</span><span class="o">)</span> <span class="o">{</span>

        <span class="c1">// 打印body内容</span>

        <span class="k">return</span> <span class="kd">super</span><span class="o">.</span><span class="na">afterBodyRead</span><span class="o">(</span><span class="n">body</span><span class="o">,</span> <span class="n">inputMessage</span><span class="o">,</span> <span class="n">parameter</span><span class="o">,</span> <span class="n">targetType</span><span class="o">,</span> <span class="n">converterType</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="六使用-responsebodyadvice-打印响应内容">六、使用 ResponseBodyAdvice 打印响应内容</h2>

<p>ResponseBodyAdvice 和 RequestBodyAdviceAdapter 同属于 ControllerAdvice。ResponseBodyAdvice 封装了 beforeBodyWrite 方法，可以获取到响应报文。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@ControllerAdvice</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">CustomResponseBodyAdviceAdapter</span> <span class="kd">implements</span> <span class="nc">ResponseBodyAdvice</span><span class="o">&lt;</span><span class="nc">Object</span><span class="o">&gt;</span> <span class="o">{</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">supports</span><span class="o">(</span><span class="nc">MethodParameter</span> <span class="n">methodParameter</span><span class="o">,</span>
                            <span class="nc">Class</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">HttpMessageConverter</span><span class="o">&lt;?&gt;&gt;</span> <span class="n">aClass</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">Object</span> <span class="nf">beforeBodyWrite</span><span class="o">(</span><span class="nc">Object</span> <span class="n">body</span><span class="o">,</span>
                                  <span class="nc">MethodParameter</span> <span class="n">methodParameter</span><span class="o">,</span>
                                  <span class="nc">MediaType</span> <span class="n">mediaType</span><span class="o">,</span>
                                  <span class="nc">Class</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">HttpMessageConverter</span><span class="o">&lt;?&gt;&gt;</span> <span class="n">aClass</span><span class="o">,</span>
                                  <span class="nc">ServerHttpRequest</span> <span class="n">serverHttpRequest</span><span class="o">,</span>
                                  <span class="nc">ServerHttpResponse</span> <span class="n">serverHttpResponse</span><span class="o">)</span> <span class="o">{</span>

        <span class="k">if</span> <span class="o">(</span><span class="n">serverHttpRequest</span> <span class="k">instanceof</span> <span class="nc">ServletServerHttpRequest</span> <span class="o">&amp;&amp;</span>
                <span class="n">serverHttpResponse</span> <span class="k">instanceof</span> <span class="nc">ServletServerHttpResponse</span><span class="o">)</span> <span class="o">{</span>
            <span class="c1">// 打印响应body</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="n">body</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="七使用-filter-打印请求和响应">七、使用 filter 打印请求和响应</h2>

<p>通过继承spring的 <code class="language-plaintext highlighter-rouge">OncePerRequestFilter</code> 实现自定义filter。在filter中读取请求和响应的body需要做一下特殊处理，因为流只能被读取一次，在filter中被读取了，后续的处理就无法再次读取流的内容了。</p>

<p>spring提供了 <code class="language-plaintext highlighter-rouge">ContentCachingRequestWrapper</code> 和 <code class="language-plaintext highlighter-rouge">ContentCachingResponseWrapper</code> 两个类来解决这个问题。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Slf4j</span>
<span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">AccessLogFilter</span> <span class="kd">extends</span> <span class="nc">OncePerRequestFilter</span> <span class="o">{</span>

    <span class="nd">@Override</span>
    <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">doFilterInternal</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span> <span class="nc">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span> <span class="nc">FilterChain</span> <span class="n">filterChain</span><span class="o">)</span>
            <span class="kd">throws</span> <span class="nc">ServletException</span><span class="o">,</span> <span class="nc">IOException</span> <span class="o">{</span>

        <span class="nc">ContentCachingRequestWrapper</span> <span class="n">req</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ContentCachingRequestWrapper</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
        <span class="nc">ContentCachingResponseWrapper</span> <span class="n">resp</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ContentCachingResponseWrapper</span><span class="o">(</span><span class="n">response</span><span class="o">);</span>

        <span class="k">try</span> <span class="o">{</span>
            <span class="c1">// Execution request chain</span>
            <span class="n">filterChain</span><span class="o">.</span><span class="na">doFilter</span><span class="o">(</span><span class="n">req</span><span class="o">,</span> <span class="n">resp</span><span class="o">);</span>
            <span class="c1">// Get body</span>
            <span class="kt">byte</span><span class="o">[]</span> <span class="n">requestBody</span> <span class="o">=</span> <span class="n">req</span><span class="o">.</span><span class="na">getContentAsByteArray</span><span class="o">();</span>
            <span class="kt">byte</span><span class="o">[]</span> <span class="n">responseBody</span> <span class="o">=</span> <span class="n">resp</span><span class="o">.</span><span class="na">getContentAsByteArray</span><span class="o">();</span>
        
            <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"request body = {}"</span><span class="o">,</span> <span class="k">new</span> <span class="nc">String</span><span class="o">(</span><span class="n">requestBody</span><span class="o">,</span> <span class="nc">StandardCharsets</span><span class="o">.</span><span class="na">UTF_8</span><span class="o">));</span>
            <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"response body = {}"</span><span class="o">,</span> <span class="k">new</span> <span class="nc">String</span><span class="o">(</span><span class="n">responseBody</span><span class="o">,</span> <span class="nc">StandardCharsets</span><span class="o">.</span><span class="na">UTF_8</span><span class="o">));</span>
        <span class="o">}</span> <span class="k">finally</span> <span class="o">{</span>
        <span class="c1">// Finally remember to respond to the client with the cached data.</span>
            <span class="n">resp</span><span class="o">.</span><span class="na">copyBodyToResponse</span><span class="o">();</span>
        <span class="o">}</span>    
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><summary type="html"><![CDATA[在程序日志中打印出接口请求和响应的内容是一个基本的技术需求。如果在每个接口中实现请求响应的日志打印，程序编写会很繁琐，我们可以利用spring提供的机制，集中处理接口请求响应的日志打印。 具体的代码参照 示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-rest-log-request-response]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/log.png" /><media:content medium="image" url="https://springcamp.cn/assets/images/log.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">spring动态数据源</title><link href="https://springcamp.cn/spring-dynamic-datasource/" rel="alternate" type="text/html" title="spring动态数据源" /><published>2020-09-16T07:30:00+00:00</published><updated>2020-09-16T07:30:00+00:00</updated><id>https://springcamp.cn/spring-dynamic-datasource</id><content type="html" xml:base="https://springcamp.cn/spring-dynamic-datasource/"><![CDATA[<p>在实际的业务场景中，我们经常会遇到需要动态配置数据源的情况，只需要修改配置，就能增加新的数据源的接入，而不需要修改程序代码，通过动态数据源技术可以实现这个目标。
具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-dynamic-datasource">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-dynamic-datasource</a></p>

<h2 id="一概述">一、概述</h2>

<p>通常在用springboot开发数据库应用时，我们会在配置文件中配置好数据源，程序中指定数据源进行数据库操作。当需要新增数据源时，就需要修改程序。通过动态数据源技术，我们可以做到只修改配置就能实现新数据源的接入，无需修改代码。这样可以很大的提升开发效率，提升系统的灵活性。</p>

<!-- more -->

<h2 id="二配置文件">二、配置文件</h2>

<p>yml格式的配置文件支持list结构，我们可以把需要访问的数据源配置到list结构中，每个数据源指定各自的url、用户名、密码、查询语句：</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">application</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">dynamicDatasource</span>
<span class="na">dynamic-data</span><span class="pi">:</span>
  <span class="na">schemas</span><span class="pi">:</span>
    <span class="pi">-</span>
      <span class="na">code</span><span class="pi">:</span> <span class="s">dbsource1</span>
      <span class="na">datasource</span><span class="pi">:</span>
        <span class="na">url</span><span class="pi">:</span> <span class="s">jdbc:h2:mem:db1;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</span>
        <span class="na">username</span><span class="pi">:</span> <span class="s">sa</span>
        <span class="na">password</span><span class="pi">:</span>
      <span class="na">query</span><span class="pi">:</span> <span class="pi">|</span>
        <span class="s">select 'datasource1 data'</span>
    <span class="pi">-</span>
      <span class="na">code</span><span class="pi">:</span> <span class="s">dbsource2</span>
      <span class="na">datasource</span><span class="pi">:</span>
        <span class="na">url</span><span class="pi">:</span> <span class="s">jdbc:h2:mem:db2;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</span>
        <span class="na">username</span><span class="pi">:</span> <span class="s">sa</span>
        <span class="na">password</span><span class="pi">:</span>
      <span class="na">query</span><span class="pi">:</span> <span class="pi">|</span>
        <span class="s">select 'datasource2 data'</span>

</code></pre></div></div>

<h2 id="三读取数据源配置">三、读取数据源配置</h2>

<p>我们定义一个配置类<code class="language-plaintext highlighter-rouge">DatabaseConfig</code> 用于读取数据源的配置：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Slf4j</span>
<span class="nd">@Data</span>
<span class="nd">@Component</span>
<span class="nd">@ConfigurationProperties</span><span class="o">(</span><span class="n">prefix</span> <span class="o">=</span> <span class="s">"dynamic-data"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">DatabaseConfig</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">DbSchema</span><span class="o">&gt;</span> <span class="n">schemas</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;();</span>

    <span class="nd">@PostConstruct</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">init</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">for</span> <span class="o">(</span><span class="nc">DbSchema</span> <span class="n">current</span> <span class="o">:</span> <span class="k">this</span><span class="o">.</span><span class="na">getShemas</span><span class="o">())</span> <span class="o">{</span>
            <span class="nc">HikariConfig</span> <span class="n">jdbcConfig</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HikariConfig</span><span class="o">();</span>
            <span class="n">jdbcConfig</span><span class="o">.</span><span class="na">setJdbcUrl</span><span class="o">(</span><span class="n">current</span><span class="o">.</span><span class="na">getDatasource</span><span class="o">().</span><span class="na">getUrl</span><span class="o">());</span>
            <span class="n">jdbcConfig</span><span class="o">.</span><span class="na">setUsername</span><span class="o">(</span><span class="n">current</span><span class="o">.</span><span class="na">getDatasource</span><span class="o">().</span><span class="na">getUsername</span><span class="o">());</span>
            <span class="nc">String</span> <span class="n">password</span> <span class="o">=</span> <span class="n">current</span><span class="o">.</span><span class="na">getDatasource</span><span class="o">().</span><span class="na">getPassword</span><span class="o">();</span>
            <span class="n">jdbcConfig</span><span class="o">.</span><span class="na">setPassword</span><span class="o">(</span><span class="n">password</span><span class="o">);</span>
            <span class="k">try</span> <span class="o">{</span>
                <span class="nc">HikariDataSource</span> <span class="n">hikariDataSource</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HikariDataSource</span><span class="o">(</span><span class="n">jdbcConfig</span><span class="o">);</span>
                <span class="n">current</span><span class="o">.</span><span class="na">setJdbcTemplate</span><span class="o">(</span><span class="k">new</span> <span class="nc">JdbcTemplate</span><span class="o">(</span><span class="n">hikariDataSource</span><span class="o">));</span>
            <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">log</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="s">"connect to "</span> <span class="o">+</span> <span class="n">current</span><span class="o">.</span><span class="na">getDatasource</span><span class="o">().</span><span class="na">getUrl</span><span class="o">()</span> <span class="o">+</span> <span class="s">"  failed."</span><span class="o">);</span>
                <span class="k">throw</span> <span class="n">e</span><span class="o">;</span>
            <span class="o">}</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="nd">@Data</span>
    <span class="nd">@NoArgsConstructor</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">DbSchema</span> <span class="o">{</span>
        <span class="kd">private</span> <span class="nc">String</span> <span class="n">code</span><span class="o">;</span>
        <span class="kd">private</span> <span class="nc">DataSourceProperties</span> <span class="n">datasource</span><span class="o">;</span>
        <span class="kd">private</span> <span class="nc">String</span> <span class="n">query</span><span class="o">;</span>
        <span class="kd">private</span> <span class="nc">JdbcTemplate</span> <span class="n">jdbcTemplate</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>其中的<code class="language-plaintext highlighter-rouge">DbSchema</code>类对应了数据源的各项配置，包括url、用户名、密码、查询语句，另外还定义了一个<code class="language-plaintext highlighter-rouge">JdbcTemplate</code>，我们可以用每个数据源自己的JdbcTemplate去访问本数据源的数据。</p>

<p>在<code class="language-plaintext highlighter-rouge">init</code>方法中初始化<code class="language-plaintext highlighter-rouge">JdbcTemplate</code>。数据库连接池采用HikariCP，这也是springboot默认使用的数据库连接池。配置文件中所有的数据库连接配置都生成一个对应的<code class="language-plaintext highlighter-rouge">DbSchema</code>对象，放到配置类的schemas这个list中。</p>

<p>通过这种方式，我们还可以实现配置文件中数据库密码的加密。配置文件中的数据库密码是加密后的密码，可以在init方法中，对<code class="language-plaintext highlighter-rouge">current.getDatasource().getPassword()</code>解密。这样能够提升系统的安全性，防止数据库密码通过配置文件泄漏。</p>

<h2 id="四读取各数据源的数据">四、读取各数据源的数据</h2>

<p>当数据库连接池完成初始化后，读取数据就变的很简单，我们只需要遍历配置类中的schemas成员，针对每个schema操作<code class="language-plaintext highlighter-rouge">JdbcTemplate</code>就可以。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@SpringBootApplication</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">DemoApplication</span> <span class="kd">implements</span> <span class="nc">CommandLineRunner</span> <span class="o">{</span>

    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">DatabaseConfig</span> <span class="n">databaseConfig</span><span class="o">;</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">run</span><span class="o">(</span><span class="nc">String</span><span class="o">...</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">databaseConfig</span><span class="o">.</span><span class="na">getSchemas</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">r</span> <span class="o">-&gt;</span> <span class="o">!</span><span class="n">r</span><span class="o">.</span><span class="na">getQuery</span><span class="o">().</span><span class="na">isEmpty</span><span class="o">()).</span><span class="na">forEach</span><span class="o">(</span><span class="n">current</span> <span class="o">-&gt;</span> <span class="o">{</span>
            <span class="nc">String</span> <span class="n">result</span> <span class="o">=</span> <span class="n">current</span><span class="o">.</span><span class="na">getJdbcTemplate</span><span class="o">().</span><span class="na">queryForObject</span><span class="o">(</span>
                    <span class="n">current</span><span class="o">.</span><span class="na">getQuery</span><span class="o">(),</span> <span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
            <span class="nc">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">current</span><span class="o">.</span><span class="na">getCode</span><span class="o">()</span> <span class="o">+</span> <span class="s">" content: "</span> <span class="o">+</span> <span class="n">result</span><span class="o">);</span>
        <span class="o">});</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="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">SpringApplication</span><span class="o">.</span><span class="na">run</span><span class="o">(</span><span class="nc">DemoApplication</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>后续如果要新增数据源，只需要在配置文件中的schemas下面新增数据源定义即可。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><summary type="html"><![CDATA[在实际的业务场景中，我们经常会遇到需要动态配置数据源的情况，只需要修改配置，就能增加新的数据源的接入，而不需要修改程序代码，通过动态数据源技术可以实现这个目标。 具体的代码参照 示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-dynamic-datasource]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/dynamic-datasource.png" /><media:content medium="image" url="https://springcamp.cn/assets/images/dynamic-datasource.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">java并发编程</title><link href="https://springcamp.cn/java-concurrency/" rel="alternate" type="text/html" title="java并发编程" /><published>2020-04-12T13:30:00+00:00</published><updated>2020-04-12T13:30:00+00:00</updated><id>https://springcamp.cn/java-concurrency</id><content type="html" xml:base="https://springcamp.cn/java-concurrency/"><![CDATA[<p>常用的java并发编程技术。
具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-currency">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-currency</a></p>

<h2 id="一概述">一、概述</h2>

<p>传统的java并发能力依靠的是多线程，相比于现代的方法是Reactive编程，本文介绍多线程的实现，Reactive编程方法的介绍可参见<a href="https://springcamp.cn/reactive-program-1/">Reactive编程</a>。</p>

<p>多线程并发编程有2个核心概念，原子性和可见性。原子性的介绍随处可见，简单来说就是一组操作要么全部成功，要么全部失败，不存在中间状态。</p>

<p>可见性是指一个线程中数据的变化是否能被其它线程感知。</p>

<p>多线程编程中要一直注意的一个问题点就是check-then-act的处理，我们的程序中存着大量的 条件判断-&gt;执行 的处理，这种简单的处理在单线程中不会存在什么问题，但是在多线程环境中却是极易出错。需要综合考虑原子性和可见性。“竞态条件”表述的就是这个问题。</p>

<p>本文主要介绍的内容：竞态条件、java内存模型（happens-before）、synchronized、原子类、锁、ThreadLocal变量、CountDownLatch、CompletableFuture。</p>

<!-- more -->

<h2 id="二竞态条件">二、竞态条件</h2>

<p>当多个线程对共享资源进行处理的时候，可能由于不同的执行顺序导致产生不同的结果。比较典型的是check-then-act操作，如以下代码：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">Race</span> <span class="o">{</span>
  <span class="kd">private</span> <span class="nc">Long</span> <span class="n">value</span><span class="o">;</span>
  <span class="nc">Long</span> <span class="nf">get</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="kc">null</span> <span class="o">){</span>
      <span class="n">value</span> <span class="o">=</span> <span class="n">initialize</span><span class="o">();</span>
    <span class="o">}</span>
    <span class="k">return</span> <span class="n">value</span><span class="o">;</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>当这个类的同一个对象在多个线程中执行get方法时，由于get方法不是原子操作，initialize方法可能会执行多次。解决这个问题可以通过将get方法改为synchronized方法或者是将value改为原子类。</p>

<p>再来看另外一种竞态条件。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">Waiter</span> <span class="kd">implements</span> <span class="nc">Runnable</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kt">boolean</span> <span class="n">shouldFinish</span><span class="o">;</span>

    <span class="kt">void</span> <span class="nf">finish</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">shouldFinish</span> <span class="o">=</span> <span class="kc">true</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">run</span><span class="o">()</span> <span class="o">{</span>
        <span class="kt">long</span> <span class="n">iteration</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
        <span class="k">while</span> <span class="o">(!</span><span class="n">shouldFinish</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">iteration</span><span class="o">++;</span>
        <span class="o">}</span>

        <span class="nc">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">"Finished after: "</span> <span class="o">+</span> <span class="n">iteration</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">DataRace</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="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">InterruptedException</span> <span class="o">{</span>
        <span class="nc">Waiter</span> <span class="n">waiter</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Waiter</span><span class="o">();</span>
        <span class="nc">Thread</span> <span class="n">waiterThread</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Thread</span><span class="o">(</span><span class="n">waiter</span><span class="o">);</span>
        <span class="n">waiterThread</span><span class="o">.</span><span class="na">start</span><span class="o">();</span>  <span class="c1">// 在另一个的线程中执行waiter的run方法，该方法通过判断shouldFinish变量的值确定是否退出循环</span>
        <span class="n">waiter</span><span class="o">.</span><span class="na">finish</span><span class="o">();</span> <span class="c1">// 在主线程中修改shouldFinish变量的值</span>
        <span class="n">waiterThread</span><span class="o">.</span><span class="na">join</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>正常情况下在执行完waiter的finish方法后，run方法中的循环会退出，但是也有可能run方法会进入死循环。我们可以通过延迟waiter.finish()的执行来模拟这种情况。将main方法做一下修改：</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">DataRace</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="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">InterruptedException</span> <span class="o">{</span>
        <span class="nc">Waiter</span> <span class="n">waiter</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Waiter</span><span class="o">();</span>
        <span class="nc">Thread</span> <span class="n">waiterThread</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Thread</span><span class="o">(</span><span class="n">waiter</span><span class="o">);</span>
        <span class="n">waiterThread</span><span class="o">.</span><span class="na">start</span><span class="o">();</span>
        <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="mi">10L</span><span class="o">);</span>  <span class="c1">// 延迟10毫秒后再调用finish方法，会发现程序会一直运行不退出，在run方法中shouldFinish一直是false</span>
        <span class="n">waiter</span><span class="o">.</span><span class="na">finish</span><span class="o">();</span>
        <span class="n">waiterThread</span><span class="o">.</span><span class="na">join</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>再次执行这个程序，会发现run方法进入了死循环，即使waiter.finish()已经将shouldFinish设置成true，循环仍然没有退出。产生这个问题的原因就是在另一个的线程中读到的shouldFinish变量的值是脏数据。</p>

<p>可以通过将 <code class="language-plaintext highlighter-rouge">shouldFinish</code> 变量声明为 <code class="language-plaintext highlighter-rouge">volatile</code> 来解决这个问题。</p>

<p>这种现象是源于java内存模型的happens-before规则，一个线程对变量的写入操作的结果只有符合happens-before规则情况下才会被其它线程读取到。 <code class="language-plaintext highlighter-rouge">synchronized</code>和<code class="language-plaintext highlighter-rouge">volatile</code>结构，以及<code class="language-plaintext highlighter-rouge">Thread.start()</code>和<code class="language-plaintext highlighter-rouge">Thread.join()</code>方法均可构成happens-before关系。该规则的描述如下（<a href="https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/package-summary.html">原文</a>）：</p>

<ol>
  <li>
    <p>程序的顺序性规则：一个线程中，按照程序的顺序，前面的操作happens-before后续的任何操作。</p>
  </li>
  <li>
    <p>volatile规则：对一个volatile变量的写操作，happens-before后续对这个变量的读操作。</p>
  </li>
  <li>
    <p>锁规则：对一个锁的解锁操作，happens-before后续对这个锁的加锁操作。</p>
  </li>
  <li>
    <p>线程start()规则：主线程A启动线程B，线程B中可以看到主线程启动B之前的操作。也就是start() happens before 线程B中的操作。</p>
  </li>
  <li>
    <p>线程join()规则：主线程A等待子线程B完成，当子线程B执行完毕后，主线程A可以看到线程B的所有操作。也就是说，子线程B中的任意操作，happens-before join()的返回。</p>
  </li>
  <li>
    <p>传递性规则：如果A happens-before B，B happens-before C，那么A happens-before C。</p>
  </li>
</ol>

<p>所以将 <code class="language-plaintext highlighter-rouge">shouldFinish</code> 变量声明为 <code class="language-plaintext highlighter-rouge">volatile</code>后，符合规则3，执行finish方法后对<code class="language-plaintext highlighter-rouge">shouldFinish</code>的修改会被读线程读取到修改后的结果。如果没有加<code class="language-plaintext highlighter-rouge">volatile</code>关键字，就没有符合happens-before规则。</p>

<h2 id="三synchronized">三、synchronized</h2>

<p><code class="language-plaintext highlighter-rouge">synchronized</code> 提供了一种悲观锁机制，synchronized声明的代码块具有排他性，同一时间只有一个线程能够获得锁，通过这种方式确保原子性和可见性。synchronized可以用在方法上，也可以用在一段代码块上。当synchronized可以用在static方法上时，用的是类锁，否则是对象锁。</p>

<p>对代码块枷锁：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">SynchronizedBlock</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kt">int</span> <span class="n">counter0</span><span class="o">;</span>

    <span class="kt">void</span> <span class="nf">increment</span><span class="o">()</span> <span class="o">{</span>
        <span class="kd">synchronized</span> <span class="o">(</span><span class="k">this</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">counter0</span><span class="o">++;</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="kd">class</span> <span class="nc">SynchronizedMethod</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kt">int</span> <span class="n">counter0</span><span class="o">;</span>

    <span class="kd">synchronized</span> <span class="kt">void</span> <span class="nf">increment</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">counter0</span><span class="o">++;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="四threadlocal">四、ThreadLocal</h2>

<p>虽然通过<code class="language-plaintext highlighter-rouge">synchronized</code>可以实现原子性，但是由于使用的是悲观锁机制，对性能会有影响。如果多个线程之间的变量不需要共享，可以采用<code class="language-plaintext highlighter-rouge">ThreadLocal</code>变量，避免多个线程同时修改同一个变量导致出现并发问题。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">ThreadLocalDemo</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">ThreadLocal</span><span class="o">&lt;</span><span class="nc">Transaction</span><span class="o">&gt;</span> <span class="n">currentTransaction</span> <span class="o">=</span> <span class="nc">ThreadLocal</span><span class="o">.</span><span class="na">withInitial</span><span class="o">(</span><span class="nl">NullTransaction:</span><span class="o">:</span><span class="k">new</span><span class="o">);</span>

    <span class="nc">Transaction</span> <span class="nf">currentTransaction</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">Transaction</span> <span class="n">current</span> <span class="o">=</span> <span class="n">currentTransaction</span><span class="o">.</span><span class="na">get</span><span class="o">();</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">current</span><span class="o">.</span><span class="na">isNull</span><span class="o">())</span> <span class="o">{</span>
            <span class="n">current</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">TransactionImpl</span><span class="o">();</span>
            <span class="n">currentTransaction</span><span class="o">.</span><span class="na">set</span><span class="o">(</span><span class="n">current</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="n">current</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="kd">interface</span> <span class="nc">Transaction</span> <span class="o">{</span>
    <span class="kt">boolean</span> <span class="nf">isNull</span><span class="o">();</span>
<span class="o">}</span>

<span class="kd">class</span> <span class="nc">NullTransaction</span> <span class="kd">implements</span> <span class="nc">Transaction</span> <span class="o">{</span>
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">isNull</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="kd">class</span> <span class="nc">TransactionImpl</span> <span class="kd">implements</span> <span class="nc">Transaction</span> <span class="o">{</span>
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">isNull</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="o">}</span>
</code></pre></div></div>

<h2 id="五atomics">五、Atomics</h2>

<p>另外一种简化并发编程的方式是采用原子数据结构，这种数据结构本身保证了原子性和可见性，可以方便的使用，能够避免多线程环境下check-then-act的问题。</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">Atomic</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="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">AtomicRun</span> <span class="n">atomicRun</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">AtomicRun</span><span class="o">();</span>
        <span class="nc">Thread</span> <span class="n">waiterThread1</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Thread</span><span class="o">(</span><span class="n">atomicRun</span><span class="o">);</span>
        <span class="nc">Thread</span> <span class="n">waiterThread2</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Thread</span><span class="o">(</span><span class="n">atomicRun</span><span class="o">);</span>
        <span class="n">waiterThread1</span><span class="o">.</span><span class="na">start</span><span class="o">();</span>
        <span class="n">waiterThread2</span><span class="o">.</span><span class="na">start</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="kd">class</span> <span class="nc">AtomicRun</span> <span class="kd">implements</span> <span class="nc">Runnable</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">AtomicBoolean</span> <span class="n">shouldFinish</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">AtomicBoolean</span><span class="o">(</span><span class="kc">false</span><span class="o">);</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">run</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">shouldFinish</span><span class="o">.</span><span class="na">compareAndSet</span><span class="o">(</span><span class="kc">false</span><span class="o">,</span> <span class="kc">true</span><span class="o">))</span> <span class="o">{</span>
            <span class="nc">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">"initialized only once"</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>由于shouldFinish是一个原子对象，shouldFinish.compareAndSet是一个原子操作，因此不会出现读取到脏数据的问题。</p>

<h2 id="六locks">六、Locks</h2>

<p>java.util.concurrent.locks包提供了与<code class="language-plaintext highlighter-rouge">synchronized</code>相同的功能，在此基础上又进行了扩展，例如可以获取锁的状态，可以中断锁。对于读多写少的情况，还可以通过ReadWriteLock来提升性能。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">LockDemo</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Lock</span> <span class="n">lock</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ReentrantLock</span><span class="o">();</span>
    <span class="kd">private</span> <span class="kt">int</span> <span class="n">counter0</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="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">LockDemo</span> <span class="n">lockDemo</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">LockDemo</span><span class="o">();</span>
        <span class="n">lockDemo</span><span class="o">.</span><span class="na">increment</span><span class="o">();</span>
        <span class="nc">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">"count is: "</span> <span class="o">+</span> <span class="n">lockDemo</span><span class="o">.</span><span class="na">getCounter0</span><span class="o">());</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">int</span> <span class="nf">getCounter0</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">counter0</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kt">void</span> <span class="nf">increment</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">lock</span><span class="o">.</span><span class="na">lock</span><span class="o">();</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="n">counter0</span><span class="o">++;</span>
        <span class="o">}</span> <span class="k">finally</span> <span class="o">{</span>
            <span class="n">lock</span><span class="o">.</span><span class="na">unlock</span><span class="o">();</span>
        <span class="o">}</span>

    <span class="o">}</span>
<span class="o">}</span>

<span class="kd">class</span> <span class="nc">ReadWriteLockDemo</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">ReadWriteLock</span> <span class="n">lock</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ReentrantReadWriteLock</span><span class="o">();</span>
    <span class="kd">private</span> <span class="kt">int</span> <span class="n">counter1</span><span class="o">;</span>

    <span class="kt">void</span> <span class="nf">increment</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">lock</span><span class="o">.</span><span class="na">writeLock</span><span class="o">().</span><span class="na">lock</span><span class="o">();</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="n">counter1</span><span class="o">++;</span>
        <span class="o">}</span> <span class="k">finally</span> <span class="o">{</span>
            <span class="n">lock</span><span class="o">.</span><span class="na">writeLock</span><span class="o">().</span><span class="na">unlock</span><span class="o">();</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="kt">int</span> <span class="nf">current</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">lock</span><span class="o">.</span><span class="na">readLock</span><span class="o">().</span><span class="na">lock</span><span class="o">();</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="k">return</span> <span class="n">counter1</span><span class="o">;</span>
        <span class="o">}</span> <span class="k">finally</span> <span class="o">{</span>
            <span class="n">lock</span><span class="o">.</span><span class="na">readLock</span><span class="o">().</span><span class="na">unlock</span><span class="o">();</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>在使用locks时，要注意一定要在finally方法中执行unlock操作，因为程序出现异常后，不会自动释放锁，如果不在finally方法中执行unlock，会导致程序进入死锁状态。</p>

<h2 id="七countdownlatch">七、CountDownLatch</h2>

<p>CountDownLatch一般用于同步多个线程的执行进度，例如有一个线程需要等其它三个线程执行完成后，再继续往下执行，可以用CountDownLatch来处理。</p>

<p>CountDownLatch类似于一个计数器，当一个线程调用CountDownLatch的await方法时会进入阻塞状态，其它线程调用countDown方法对计数器减一，当计数器减为0时，被await方法阻塞的操作才会解除阻塞状态继续执行。</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">CountDownLatchDemo</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="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">InterruptedException</span> <span class="o">{</span>
        <span class="nc">ExecutorService</span> <span class="n">executorService</span> <span class="o">=</span> <span class="nc">Executors</span><span class="o">.</span><span class="na">newFixedThreadPool</span><span class="o">(</span><span class="mi">2</span><span class="o">);</span>

        <span class="nc">CountDownLatch</span> <span class="n">latch</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">CountDownLatch</span><span class="o">(</span><span class="mi">1</span><span class="o">);</span>
        <span class="nc">Receiver</span> <span class="n">receiver</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Receiver</span><span class="o">(</span><span class="n">latch</span><span class="o">);</span>
        <span class="n">executorService</span><span class="o">.</span><span class="na">submit</span><span class="o">(</span><span class="n">receiver</span><span class="o">);</span>
        <span class="n">latch</span><span class="o">.</span><span class="na">await</span><span class="o">();</span>
        <span class="nc">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">"latch done"</span><span class="o">);</span>
        <span class="n">executorService</span><span class="o">.</span><span class="na">shutdown</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="kd">class</span> <span class="nc">Receiver</span> <span class="kd">implements</span> <span class="nc">Runnable</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="nc">CountDownLatch</span> <span class="n">latch</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">Receiver</span><span class="o">(</span><span class="nc">CountDownLatch</span> <span class="n">latch</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">latch</span> <span class="o">=</span> <span class="n">latch</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">run</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">latch</span><span class="o">.</span><span class="na">countDown</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="八completablefuture">八、CompletableFuture</h2>

<p>CompletableFuture是一种java8提供的常用的多线程并发编程方法，虽然parallelStream也提供了多线程并发能力，但是在选择上要遵循一个原则：有IO操作的用CompletableFuture，没有IO操作纯计算的用parallelStream。</p>

<p>原因在于parallelStream使用的是jvm的默认ForkJoinPool线程池，该线程池一般只会分配很少的线程数（默认是CPU的核数），不能指定其它线程池。当有IO操作或者类似的延迟较高的操作时，很容易把线程池占满。而CompletableFuture允许指定线程池，可以为不同的处理指定不同的线程池，能够分业务进行线程池的隔离。</p>

<p>首先我们模拟一个延迟IO方法，用于后续的演示：</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="nc">Long</span> <span class="nf">getPrice</span><span class="o">(</span><span class="nc">String</span> <span class="n">prod</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">delay</span><span class="o">();</span>  <span class="c1">//模拟服务响应的延迟</span>
        <span class="nc">Long</span> <span class="n">price</span> <span class="o">=</span> <span class="nc">ThreadLocalRandom</span><span class="o">.</span><span class="na">current</span><span class="o">().</span><span class="na">nextLong</span><span class="o">(</span><span class="mi">0</span><span class="o">,</span> <span class="mi">1000</span><span class="o">);</span>
        <span class="nc">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">"Executing in "</span> <span class="o">+</span> <span class="nc">Thread</span><span class="o">.</span><span class="na">currentThread</span><span class="o">().</span><span class="na">getName</span><span class="o">()</span> <span class="o">+</span> <span class="s">", get price for "</span> <span class="o">+</span> <span class="n">prod</span> <span class="o">+</span> <span class="s">" is "</span> <span class="o">+</span> <span class="n">price</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">price</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">delay</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="mi">1000</span><span class="o">);</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">InterruptedException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">e</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">();</span>
        <span class="o">}</span>
    <span class="o">}</span>
</code></pre></div></div>

<h3 id="1-supplyasync该方法用于创建一个异步任务">1. supplyAsync：该方法用于创建一个异步任务</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">ExecutorService</span> <span class="n">executor</span> <span class="o">=</span> <span class="nc">Executors</span><span class="o">.</span><span class="na">newFixedThreadPool</span><span class="o">(</span><span class="mi">10</span><span class="o">);</span>

<span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">Long</span><span class="o">&gt;</span> <span class="n">future</span> <span class="o">=</span> <span class="nc">CompletableFuture</span><span class="o">.</span><span class="na">supplyAsync</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">getPrice</span><span class="o">(</span><span class="s">"accept"</span><span class="o">),</span> <span class="n">executor</span><span class="o">);</span>
</code></pre></div></div>

<p>supplyAsync是一个工厂方法，该方法会返回一个CompletableFuture对象，入参是 Supplier 或者 Runnable 的实现类，可以用Lambda表达式来表示。同时指定了executor作为执行用的线程池。</p>

<h3 id="2-thenacceptasync该方法接收completablefuture的执行结果将结果作为输入执行指定的方法">2. thenAcceptAsync：该方法接收CompletableFuture的执行结果，将结果作为输入执行指定的方法</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">future</span><span class="o">.</span><span class="na">thenAccept</span><span class="o">(</span><span class="n">p</span> <span class="o">-&gt;</span> <span class="o">{</span>
            <span class="nc">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">"Executing in "</span> <span class="o">+</span> <span class="nc">Thread</span><span class="o">.</span><span class="na">currentThread</span><span class="o">().</span><span class="na">getName</span><span class="o">()</span> <span class="o">+</span> <span class="s">", async price is: "</span> <span class="o">+</span> <span class="n">p</span><span class="o">);</span>
        <span class="o">},</span> <span class="n">executor</span><span class="o">);</span>
</code></pre></div></div>

<p>将第一步中的返回结果（getPrice的返回值）作为输入执行操作。</p>

<h3 id="3-thenapply该方法接收completablefuture的执行结果进行计算返回一个新的completablefuture类似于stream的map操作">3. thenApply：该方法接收CompletableFuture的执行结果进行计算，返回一个新的CompletableFuture，类似于stream的map操作。</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">result</span> <span class="o">=</span> <span class="n">future</span><span class="o">.</span><span class="na">thenApply</span><span class="o">(</span><span class="n">p</span> <span class="o">-&gt;</span> <span class="n">p</span> <span class="o">+</span> <span class="s">"1"</span><span class="o">);</span>
</code></pre></div></div>

<p>该步操作将第一步中的<code class="language-plaintext highlighter-rouge">CompletableFuture&lt;Long&gt;</code> 转换为了 <code class="language-plaintext highlighter-rouge">CompletableFuture&lt;String&gt;</code>。</p>

<h3 id="4-thencompose用于组合2个completablefuture第一个的计算结果作为第二个的输入">4. thenCompose：用于组合2个CompletableFuture，第一个的计算结果作为第二个的输入。</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">Long</span><span class="o">&gt;</span> <span class="n">future1</span> <span class="o">=</span> <span class="nc">CompletableFuture</span>
                <span class="o">.</span><span class="na">supplyAsync</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">getPrice</span><span class="o">(</span><span class="s">"compose"</span><span class="o">));</span>
<span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">result</span> <span class="o">=</span> <span class="n">future1</span><span class="o">.</span><span class="na">thenCompose</span><span class="o">(</span>
    <span class="n">i</span> <span class="o">-&gt;</span> <span class="nc">CompletableFuture</span><span class="o">.</span><span class="na">supplyAsync</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="o">{</span>
        <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="mi">2000</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">i</span> <span class="o">+</span> <span class="s">"World"</span><span class="o">;</span>
    <span class="o">})</span>
<span class="o">);</span>
</code></pre></div></div>

<h3 id="5-thencombine-将两个completablefuture的计算结果做进一步的计算">5. thenCombine： 将两个CompletableFuture的计算结果做进一步的计算</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">Long</span><span class="o">&gt;</span> <span class="n">future1</span> <span class="o">=</span> <span class="nc">CompletableFuture</span><span class="o">.</span><span class="na">supplyAsync</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">getPrice</span><span class="o">(</span><span class="s">"combine1"</span><span class="o">));</span>
<span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">Long</span><span class="o">&gt;</span> <span class="n">future2</span> <span class="o">=</span> <span class="nc">CompletableFuture</span><span class="o">.</span><span class="na">supplyAsync</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">getPrice</span><span class="o">(</span><span class="s">"combine2"</span><span class="o">));</span>
<span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">Long</span><span class="o">&gt;</span> <span class="n">result</span> <span class="o">=</span> <span class="n">future1</span><span class="o">.</span><span class="na">thenCombine</span><span class="o">(</span><span class="n">future2</span><span class="o">,</span> <span class="o">(</span><span class="n">f1</span><span class="o">,</span> <span class="n">f2</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="n">f1</span> <span class="o">+</span> <span class="n">f2</span><span class="o">);</span>
</code></pre></div></div>

<p>这段代码会在future1和future2都计算完成后，把两个future的计算结果进行相加，返回新的CompletableFuture。</p>

<h3 id="6-exceptionally-异常处理">6. exceptionally： 异常处理</h3>

<p>exceptionally是CompletableFuture最简便的一种异常处理方法。该方法会在异常发生后返回一个默认值。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">Long</span><span class="o">&gt;</span> <span class="n">future1</span> <span class="o">=</span> <span class="nc">CompletableFuture</span><span class="o">.</span><span class="na">supplyAsync</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">getPrice</span><span class="o">(</span><span class="s">"exception1"</span><span class="o">));</span>

<span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">Long</span><span class="o">&gt;</span> <span class="n">future2</span> <span class="o">=</span> <span class="nc">CompletableFuture</span>
                <span class="o">.</span><span class="na">supplyAsync</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="o">(</span><span class="mi">1L</span> <span class="o">/</span> <span class="mi">0</span><span class="o">)</span> <span class="o">)</span> <span class="c1">//模拟抛出一个异常</span>
                <span class="c1">// 出现异常时返回默认值，如果此处没有exceptionally处理，异常会在后续的join中抛出</span>
                <span class="o">.</span><span class="na">exceptionally</span><span class="o">((</span><span class="n">ex</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="o">{</span>
                    <span class="nc">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">"Executing in "</span> <span class="o">+</span> <span class="nc">Thread</span><span class="o">.</span><span class="na">currentThread</span><span class="o">().</span><span class="na">getName</span><span class="o">()</span> <span class="o">+</span> <span class="s">", get excetion "</span> <span class="o">+</span> <span class="n">ex</span><span class="o">);</span>
                    <span class="k">return</span> <span class="mi">0L</span><span class="o">;</span>
                <span class="o">});</span>

<span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">Long</span><span class="o">&gt;</span> <span class="n">result</span> <span class="o">=</span> <span class="n">future1</span><span class="o">.</span><span class="na">thenCombine</span><span class="o">(</span><span class="n">future2</span><span class="o">,</span> <span class="o">(</span><span class="n">f1</span><span class="o">,</span> <span class="n">f2</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="n">f1</span> <span class="o">+</span> <span class="n">f2</span><span class="o">);</span>

<span class="k">try</span> <span class="o">{</span>
            <span class="nc">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">"Executing in "</span> <span class="o">+</span> <span class="nc">Thread</span><span class="o">.</span><span class="na">currentThread</span><span class="o">().</span><span class="na">getName</span><span class="o">()</span> <span class="o">+</span> <span class="s">" ,combine price is: "</span> <span class="o">+</span> <span class="n">result</span><span class="o">.</span><span class="na">join</span><span class="o">());</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">CompletionException</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">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">"Executing in "</span> <span class="o">+</span> <span class="nc">Thread</span><span class="o">.</span><span class="na">currentThread</span><span class="o">().</span><span class="na">getName</span><span class="o">()</span> <span class="o">+</span> <span class="s">" ,combine price error: "</span> <span class="o">+</span> <span class="n">ex</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="7-并行执行completablefuture">7. 并行执行CompletableFuture</h3>

<p>假设我们有一个数组，数组中的每一项都需要调用getPrice方法获取价格，可以采用stream和CompletableFuture组合使用的方式。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">List</span><span class="o">&lt;</span><span class="nc">Long</span><span class="o">&gt;</span> <span class="n">prices</span> <span class="o">=</span> <span class="nc">Stream</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"1"</span><span class="o">,</span> <span class="s">"2"</span><span class="o">,</span> <span class="s">"3"</span><span class="o">,</span> <span class="s">"4"</span><span class="o">,</span> <span class="s">"5"</span><span class="o">,</span> <span class="s">"6"</span><span class="o">,</span> <span class="s">"7"</span><span class="o">,</span> <span class="s">"8"</span><span class="o">,</span> <span class="s">"9"</span><span class="o">,</span> <span class="s">"10"</span><span class="o">,</span> <span class="s">"11"</span><span class="o">,</span> <span class="s">"12"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="n">p</span> <span class="o">-&gt;</span> <span class="nc">CompletableFuture</span><span class="o">.</span><span class="na">supplyAsync</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">getPrice</span><span class="o">(</span><span class="s">"exception1"</span><span class="o">),</span> <span class="n">executor</span><span class="o">))</span> <span class="c1">//通过stream的map操作，为数组中的每一个元素都启动一个CompletableFuture</span>
                <span class="o">.</span><span class="na">collect</span><span class="o">(</span><span class="nc">Collectors</span><span class="o">.</span><span class="na">toList</span><span class="o">())</span>
                <span class="o">.</span><span class="na">stream</span><span class="o">()</span>
                <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">CompletableFuture:</span><span class="o">:</span><span class="n">join</span><span class="o">)</span> <span class="c1">//等待所有的CompletableFuture都完成计算</span>
                <span class="o">.</span><span class="na">collect</span><span class="o">(</span><span class="nc">Collectors</span><span class="o">.</span><span class="na">toList</span><span class="o">());</span>
</code></pre></div></div>

<p>注意这儿要有两段collect处理，不能简化为以下写法：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ist</span><span class="o">&lt;</span><span class="nc">Long</span><span class="o">&gt;</span> <span class="n">prices2</span> <span class="o">=</span> <span class="nc">Stream</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"1"</span><span class="o">,</span> <span class="s">"2"</span><span class="o">,</span> <span class="s">"3"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="n">p</span> <span class="o">-&gt;</span> <span class="nc">CompletableFuture</span><span class="o">.</span><span class="na">supplyAsync</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">getPrice</span><span class="o">(</span><span class="s">"exception1"</span><span class="o">)))</span>
                <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">CompletableFuture:</span><span class="o">:</span><span class="n">join</span><span class="o">)</span>
                <span class="o">.</span><span class="na">collect</span><span class="o">(</span><span class="nc">Collectors</span><span class="o">.</span><span class="na">toList</span><span class="o">());</span>
</code></pre></div></div>

<p>这样写看上去更简洁，但是存在严重的问题。因为对于每一个元素，在第一个map生成CompletableFuture后，会立即执行join阻塞操作，相当于变成了串行。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><summary type="html"><![CDATA[常用的java并发编程技术。 具体的代码参照 示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-currency]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/java-concurrency.jpg" /><media:content medium="image" url="https://springcamp.cn/assets/images/java-concurrency.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Reactive编程（三）:一个简单的HTTP服务</title><link href="https://springcamp.cn/reactive-program-3/" rel="alternate" type="text/html" title="Reactive编程（三）:一个简单的HTTP服务" /><published>2018-03-25T02:15:00+00:00</published><updated>2018-03-25T02:15:00+00:00</updated><id>https://springcamp.cn/reactive-program-3</id><content type="html" xml:base="https://springcamp.cn/reactive-program-3/"><![CDATA[<p>书接上文 <a href="https://springcamp.cn/reactive-program-2/">Reactive编程</a> ，我们已经了解了基础的API，现在我们开始编写实际的应用。Reactive对并发编程进行了很好的抽象，也有很多底层的特性需要我们去关注。当使用这些特性时，我们可以对之前隐藏在容器、平台、框架中的细节进行控制。</p>

<h2 id="spring-mvc由阻塞转向reactive">Spring MVC由阻塞转向Reactive</h2>

<p>Reactive要求我们以不同的思路来看待问题。区别于传统的request-&gt;response模式，所有的数据都是发布为一个序列 (Publisher) 然后进行订阅（Subscriber）。区别于同步等待返回结果，改为注册一个回调。只要我们习惯了这种方式，就不会觉得很复杂。但是没办法让整个环境都同时变为Reactive模式，所以避免不了要跟老式的阻塞API打交道。</p>

<p>假设我们有一个返回HttpStatus的阻塞方法：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="nc">RestTemplate</span> <span class="n">restTemplate</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">RestTemplate</span><span class="o">();</span>

<span class="kd">private</span> <span class="nc">HttpStatus</span> <span class="nf">block</span><span class="o">(</span><span class="kt">int</span> <span class="n">value</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">restTemplate</span><span class="o">.</span><span class="na">getForEntity</span><span class="o">(</span><span class="s">"http://example.com/{value}"</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="n">value</span><span class="o">)</span>
            <span class="o">.</span><span class="na">getStatusCode</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<!-- more -->

<p>我们需要传递不同的参数来重复调用这个方法，并对返回的结果进行处理。这是一个典型的 “scatter-gather”应用场景。例如从多个页面中提取前N条数据。</p>

<p>这是一个采用错误方式的例子：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Flux</span><span class="o">.</span><span class="na">range</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">10</span><span class="o">)</span> <span class="o">(</span><span class="mi">1</span><span class="o">)</span>
    <span class="o">.</span><span class="na">log</span><span class="o">()</span>
    <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="k">this</span><span class="o">::</span><span class="n">block</span><span class="o">)</span> <span class="o">(</span><span class="mi">2</span><span class="o">)</span>
    <span class="o">.</span><span class="na">collect</span><span class="o">(</span><span class="nl">Result:</span><span class="o">:</span><span class="k">new</span><span class="o">,</span> <span class="nl">Result:</span><span class="o">:</span><span class="n">add</span><span class="o">)</span> <span class="o">(</span><span class="mi">3</span><span class="o">)</span>
    <span class="o">.</span><span class="na">doOnSuccess</span><span class="o">(</span><span class="nl">Result:</span><span class="o">:</span><span class="n">stop</span><span class="o">)</span> <span class="o">(</span><span class="mi">4</span><span class="o">)</span>
</code></pre></div></div>

<ol>
  <li>调用10次接口</li>
  <li>产生阻塞</li>
  <li>将结果进行汇总后放入一个对象</li>
  <li>最后结束处理 (结果是一个 <code class="language-plaintext highlighter-rouge">Mono&lt;Result&gt;</code>)</li>
</ol>

<p>不要采用这种方式来编写代码。这是一种错误的实现方式，这样会阻塞住调用线程，这跟循环调用block()没什么区别。好的实现应该是把<code class="language-plaintext highlighter-rouge">block()</code>的调用放到工作线程中。我们可以采用一个返回<code class="language-plaintext highlighter-rouge">Mono&lt;HttpStatus&gt;</code>的方法：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="nc">Mono</span><span class="o">&lt;</span><span class="nc">HttpStatus</span><span class="o">&gt;</span> <span class="nf">fetch</span><span class="o">(</span><span class="kt">int</span> <span class="n">value</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nc">Mono</span><span class="o">.</span><span class="na">fromCallable</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">block</span><span class="o">(</span><span class="n">value</span><span class="o">))</span> <span class="o">(</span><span class="mi">1</span><span class="o">)</span>
        <span class="o">.</span><span class="na">subscribeOn</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">scheduler</span><span class="o">);</span>            <span class="o">(</span><span class="mi">2</span><span class="o">)</span>
<span class="o">}</span>
</code></pre></div></div>

<ol>
  <li>将阻塞调用放到一个 ```Callable`` 中</li>
  <li>在工作线程中进行订阅</li>
</ol>

<p><code class="language-plaintext highlighter-rouge">scheduler</code> 单独定义为一个共享变量：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
  <span class="nc">Scheduler</span> <span class="n">scheduler</span> <span class="o">=</span> <span class="nc">Schedulers</span><span class="o">.</span><span class="na">parallel</span><span class="o">()</span>

</code></pre></div></div>

<p>然后用 <code class="language-plaintext highlighter-rouge">flatMap()</code> 代替 <code class="language-plaintext highlighter-rouge">map()</code></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nc">Flux</span><span class="o">.</span><span class="na">range</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">10</span><span class="o">)</span>
    <span class="o">.</span><span class="na">log</span><span class="o">()</span>
    <span class="o">.</span><span class="na">flatMap</span><span class="o">(</span>                             <span class="o">(</span><span class="mi">1</span><span class="o">)</span>
        <span class="k">this</span><span class="o">::</span><span class="n">fetch</span><span class="o">,</span> <span class="mi">4</span><span class="o">)</span>                   <span class="o">(</span><span class="mi">2</span><span class="o">)</span>
    <span class="o">.</span><span class="na">collect</span><span class="o">(</span><span class="nl">Result:</span><span class="o">:</span><span class="k">new</span><span class="o">,</span> <span class="nl">Result:</span><span class="o">:</span><span class="n">add</span><span class="o">)</span>
    <span class="o">.</span><span class="na">doOnSuccess</span><span class="o">(</span><span class="nl">Result:</span><span class="o">:</span><span class="n">stop</span><span class="o">)</span>

</code></pre></div></div>

<ol>
  <li>在新的publisher中并行处理</li>
  <li>flatMap的并行参数</li>
</ol>

<h2 id="嵌入-non-reactive-服务">嵌入 Non-Reactive 服务</h2>

<p>如果想将上面的代码放入一个servlet这种的Non-Reactive的服务中，可以使用Spring MVC：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">"/parallel"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">Result</span><span class="o">&gt;</span> <span class="nf">parallel</span><span class="o">()</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nc">Flux</span><span class="o">.</span><span class="na">range</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">10</span><span class="o">)</span>
      <span class="o">...</span>
      <span class="o">.</span><span class="na">doOnSuccess</span><span class="o">(</span><span class="nl">Result:</span><span class="o">:</span><span class="n">stop</span><span class="o">)</span>
      <span class="o">.</span><span class="na">toFuture</span><span class="o">();</span>
<span class="o">}</span>

</code></pre></div></div>

<p>在阅读了 <code class="language-plaintext highlighter-rouge">@RequestMapping</code> 的javadoc以后，我们会发现这个方法会返回一个 <code class="language-plaintext highlighter-rouge">CompletableFuture</code> ，应用会选择在单独的线程中返回值。在我们的例子中这个单独的线程由 <code class="language-plaintext highlighter-rouge">scheduler</code> 提供。</p>

<h2 id="没有免费的午餐">没有免费的午餐</h2>

<p>利用工作线程进行 scatter-gather 计算是一个好的模式，但是也不完美 - 没有阻塞调用方，但还是阻塞了一些东西，只不过是把问题转移了。我们有一个非阻塞IO的 HTTP 服务，将处理放入线程池，一个请求一个线程 - 这是servlet容器的机制（例如tomcat）。请求是异步处理的，因此tomcat内部的工作线程没有被阻塞，我们的 <code class="language-plaintext highlighter-rouge">scheduler</code> 会创建4个线程。在处理10个请求时，理论上处理性能会提高4倍。简单来说，如果我们在单个线程中顺序处理10个请求要花1000ms，我们所采用的方式只需250ms。</p>

<p>我们可以通过增加线程来进一步提升性能（分配16个线程）：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="kd">private</span> <span class="nc">Scheduler</span> <span class="n">scheduler</span> <span class="o">=</span> <span class="nc">Schedulers</span><span class="o">.</span><span class="na">newParallel</span><span class="o">(</span><span class="s">"sub"</span><span class="o">,</span> <span class="mi">16</span><span class="o">);</span>

</code></pre></div></div>

<blockquote>
  <p>Tomcat 默认会分配100个线程来处理请求，当所有的请求同时处理时，我们的 scheduler 线程池会成为一个瓶颈。我们的 scheduler 线程池数量远小于 Tomcat 的线程池数量。这说明性能调优不是一个简单的事情，需要考虑各个参数和资源的匹配情况。</p>
</blockquote>

<p>相比固定数量的线程池，我们可以采用更灵活的线程池，可以根据需要动态调整线程数量。Reactor 已经提供了这种机制，使用 <code class="language-plaintext highlighter-rouge">Schedulers.elastic()</code> 后可以看到当请求增多时，线程数量会随之增加。</p>

<h2 id="全面采用-reactive">全面采用 Reactive</h2>

<p>从阻塞调用到reactive的桥接是一种有效的模式，并且用Spring MVC的技术很容易实现。接下来我们将完全弃用阻塞模式，采用新的API和新的工具。最终我们实现全栈Reactive。</p>

<p>在我们的例子中，第一步先用 <code class="language-plaintext highlighter-rouge">spring-boot-starter-web-reactive</code> 替换 <code class="language-plaintext highlighter-rouge">spring-boot-starter-web</code> :</p>

<p>Maven:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nt">&lt;dependencies&gt;</span>
  <span class="nt">&lt;dependency&gt;</span>
   <span class="nt">&lt;groupId&gt;</span>org.springframework.boot.experimental<span class="nt">&lt;/groupId&gt;</span>
     <span class="nt">&lt;artifactId&gt;</span>spring-boot-starter-web-reactive<span class="nt">&lt;/artifactId&gt;</span>
  <span class="nt">&lt;/dependency&gt;</span>
  ...
<span class="nt">&lt;/dependencies&gt;</span>
    <span class="nt">&lt;dependencyManagement&gt;</span>
     <span class="nt">&lt;dependencies&gt;</span>
       <span class="nt">&lt;dependency&gt;</span>
         <span class="nt">&lt;groupId&gt;</span>org.springframework.boot.experimental<span class="nt">&lt;/groupId&gt;</span>
         <span class="nt">&lt;artifactId&gt;</span>spring-boot-dependencies-web-reactive<span class="nt">&lt;/artifactId&gt;</span>
         <span class="nt">&lt;version&gt;</span>0.1.0.M1<span class="nt">&lt;/version&gt;</span>
         <span class="nt">&lt;type&gt;</span>pom<span class="nt">&lt;/type&gt;</span>
         <span class="nt">&lt;scope&gt;</span>import<span class="nt">&lt;/scope&gt;</span>
      <span class="nt">&lt;/dependency&gt;</span>
     <span class="nt">&lt;/dependencies&gt;</span>
    <span class="nt">&lt;/dependencyManagement&gt;</span>

</code></pre></div></div>

<p>Gradle:</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="n">dependencies</span> <span class="o">{</span>
	<span class="n">compile</span><span class="o">(</span><span class="s1">'org.springframework.boot.experimental:spring-boot-starter-web-reactive'</span><span class="o">)</span>
    <span class="o">...</span>
<span class="o">}</span>
<span class="n">dependencyManagement</span> <span class="o">{</span>
	<span class="n">imports</span> <span class="o">{</span>
		<span class="n">mavenBom</span> <span class="s2">"org.springframework.boot.experimental:spring-boot-dependencies-web-reactive:0.1.0.M1"</span>
	<span class="o">}</span>
<span class="o">}</span>

</code></pre></div></div>

<p>在controller中，不再使用 <code class="language-plaintext highlighter-rouge">CompletableFuture</code> 代之以返回一个 <code class="language-plaintext highlighter-rouge">Mono</code> :</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">"/parallel"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">Mono</span><span class="o">&lt;</span><span class="nc">Result</span><span class="o">&gt;</span> <span class="nf">parallel</span><span class="o">()</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nc">Flux</span><span class="o">.</span><span class="na">range</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">10</span><span class="o">)</span>
            <span class="o">.</span><span class="na">log</span><span class="o">()</span>
            <span class="o">.</span><span class="na">flatMap</span><span class="o">(</span><span class="k">this</span><span class="o">::</span><span class="n">fetch</span><span class="o">,</span> <span class="mi">4</span><span class="o">)</span>
            <span class="o">.</span><span class="na">collect</span><span class="o">(</span><span class="nl">Result:</span><span class="o">:</span><span class="k">new</span><span class="o">,</span> <span class="nl">Result:</span><span class="o">:</span><span class="n">add</span><span class="o">)</span>
            <span class="o">.</span><span class="na">doOnSuccess</span><span class="o">(</span><span class="nl">Result:</span><span class="o">:</span><span class="n">stop</span><span class="o">);</span>
<span class="o">}</span>

</code></pre></div></div>

<p>将这段代码放到SpringBoot应用中，可以运行在 Tomcat, Jetty 或者 Netty, 取决于classpath引入了哪个包。Tomcat 是默认的容器，如果想用别的容器，需要把 Tomcat 从 classpath 中去掉，然后引入其它的容器。这3个容器在启动时间、内存使用和运行时资源上相差不大。</p>

<p>我们仍然调用 <code class="language-plaintext highlighter-rouge">block()</code> 阻塞服务接口，所以我们仍需在工作线程中订阅以免阻塞调用方。我们也可以采用一个非阻塞的客户端，例如用新的 <code class="language-plaintext highlighter-rouge">WebClient</code> 替换 <code class="language-plaintext highlighter-rouge">RestTemplate</code> :</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="kd">private</span> <span class="nc">WebClient</span> <span class="n">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">WebClient</span><span class="o">(</span><span class="k">new</span> <span class="nc">ReactorHttpClientRequestFactory</span><span class="o">());</span>

<span class="kd">private</span> <span class="nc">Mono</span><span class="o">&lt;</span><span class="nc">HttpStatus</span><span class="o">&gt;</span> <span class="nf">fetch</span><span class="o">(</span><span class="kt">int</span> <span class="n">value</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">client</span><span class="o">.</span><span class="na">perform</span><span class="o">(</span><span class="nc">HttpRequestBuilders</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="s">"http://example.com"</span><span class="o">))</span>
            <span class="o">.</span><span class="na">extract</span><span class="o">(</span><span class="nc">WebResponseExtractors</span><span class="o">.</span><span class="na">response</span><span class="o">(</span><span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">))</span>
            <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="n">response</span> <span class="o">-&gt;</span> <span class="n">response</span><span class="o">.</span><span class="na">getStatusCode</span><span class="o">());</span>
<span class="o">}</span>

</code></pre></div></div>

<p>注意 <code class="language-plaintext highlighter-rouge">WebClient.perform()</code> 的返回值是一个转换为 <code class="language-plaintext highlighter-rouge">Mono&lt;HttpStatus&gt;</code> 的Reactive类型，但是我们没有订阅它。订阅的工作由框架来完成。</p>

<h2 id="控制反转">控制反转</h2>

<p>现在我们去掉 <code class="language-plaintext highlighter-rouge">fetch()</code> 调用之后的并发参数：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">"/netty"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">Mono</span><span class="o">&lt;</span><span class="nc">Result</span><span class="o">&gt;</span> <span class="nf">netty</span><span class="o">()</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nc">Flux</span><span class="o">.</span><span class="na">range</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">10</span><span class="o">)</span> <span class="o">(</span><span class="mi">1</span><span class="o">)</span>
        <span class="o">.</span><span class="na">log</span><span class="o">()</span> <span class="c1">//</span>
        <span class="o">.</span><span class="na">flatMap</span><span class="o">(</span><span class="k">this</span><span class="o">::</span><span class="n">fetch</span><span class="o">)</span> <span class="o">(</span><span class="mi">2</span><span class="o">)</span>
        <span class="o">.</span><span class="na">collect</span><span class="o">(</span><span class="nl">Result:</span><span class="o">:</span><span class="k">new</span><span class="o">,</span> <span class="nl">Result:</span><span class="o">:</span><span class="n">add</span><span class="o">)</span>
        <span class="o">.</span><span class="na">doOnSuccess</span><span class="o">(</span><span class="nl">Result:</span><span class="o">:</span><span class="n">stop</span><span class="o">);</span>
<span class="o">}</span>

</code></pre></div></div>

<ol>
  <li>进行10次调用</li>
  <li>在新的publisher中并行处理</li>
</ol>

<p>由于不再使用额外的订阅线程，相比于阻塞与Reactive桥接模式中的代码简洁了很多，现在是全面Reactive模式了。<code class="language-plaintext highlighter-rouge">WebClient</code> 返回一个 <code class="language-plaintext highlighter-rouge">Mono</code> ，在然而然的我们需要在转换链中使用 <code class="language-plaintext highlighter-rouge">flatMap()</code> 。编写这种代码是一个很好的体验，易于理解便于维护。同时不再需要线程池和并发参数，也没有了影响性能的魔法数字4 。性能取决于系统资源而不是应用的线程控制。</p>

<p>应用可以运行在 Tomcat, Jetty 或 Netty 上. Tomcat 和 Jetty 的支持基于 Servlet 3.1 的异步处理，受限于一个请求一个线程。而运行在 Netty 上则没有这个限制。只要客户端不阻塞，会尽快的分发客户端请求。由于Netty服务不是一个请求一个线程，因此不会使用大量的线程。</p>

<blockquote>
  <p>注意，很多应用的阻塞调用不只是HTTP，还有数据库操作。当前很少的数据库支持非阻塞的客户端（除了 MongoDB 和 Couchbase）。线程池和 blocking-to-reactive 模式会长期存在。</p>
</blockquote>

<h2 id="仍然没有免费的午餐">仍然没有免费的午餐</h2>

<p>首先，我们的代码是声明式的，不方便调试，错误发生时不容易定位。使用原生的API，例如不通过Spring框架而直接使用Reactor，会使情况变的更糟，因为我们自己要做很多的错误处理，每次进行网络调用都要写很多样板代码。通过组合使用 Spring 和 Reactor 我们可以方便的查看堆栈信息和未捕获的异常。由于运行的线程不受我们控制，因此在理解上会有困难。</p>

<p>其次，一旦编写错误导致一个Reactive回调被阻塞，在同一线程上的所有请求都会挂起。在servlet容器中，由于是一个请求一个线程，一个请求阻塞时，其它的请求不会受影响。而在Reactive中，一个请求被阻塞会导致所有请求的延迟都增加。</p>

<h2 id="总结">总结</h2>

<p>在异步处理中能够控制所有的环节是非常好的：每一个层级都有线程池和队列。我们可以使一些层级具有弹性能力，可以根据负载动态调整。但是这也是一种负担，我们期望有更加简洁的方式。可扩展性的分析结果趋向于减少多余的线程，不要超出硬件资源的限制条件。</p>

<p>Reactive 不能解决所有问题的方案，事实上它本身不是一个方案，它只是促进了某一类问题的解决方案的产生。学习的成本、程序的调整、后续的维护成本可能远大于其所带来的益处。所以在是否使用 Reactive 这个问题上要非常谨慎。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><summary type="html"><![CDATA[书接上文 Reactive编程 ，我们已经了解了基础的API，现在我们开始编写实际的应用。Reactive对并发编程进行了很好的抽象，也有很多底层的特性需要我们去关注。当使用这些特性时，我们可以对之前隐藏在容器、平台、框架中的细节进行控制。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/reactivehttp.png" /><media:content medium="image" url="https://springcamp.cn/assets/images/reactivehttp.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Reactive编程（二）:代码演示</title><link href="https://springcamp.cn/reactive-program-2/" rel="alternate" type="text/html" title="Reactive编程（二）:代码演示" /><published>2018-03-18T06:38:00+00:00</published><updated>2018-03-18T06:38:00+00:00</updated><id>https://springcamp.cn/reactive-program-2</id><content type="html" xml:base="https://springcamp.cn/reactive-program-2/"><![CDATA[<p>书接上文 <a href="https://springcamp.cn/reactive-program-1/">Reactive编程</a> ，我们继续用真实的代码来解释一些概念。我们会更进一步理解Reactive的与众不同以及它的功能。这些例子很抽象，但能够让我们更进一步理解用到的API和编程风格，真实的感受它的与众不同。我们将会看到Reactive的核心元素，学习如何控制数据流，如果需要的话还会用到后台线程进行处理。</p>

<h2 id="建立项目">建立项目</h2>

<p>我们用Reactor库来进行演示。当然也可以用其它的工具。如果不想拷贝黏贴代码，可以直接使用 <a href="https://github.com/dsyer/reactive-notes">github</a> 上的示例项目。</p>

<p>通过 <a href="https://start.spring.io">https://start.spring.io</a> 来建立一个空的项目并添加Reactor Core依赖。</p>

<p>可以使用Maven:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;dependency&gt;</span>
  <span class="nt">&lt;groupId&gt;</span>io.projectreactor<span class="nt">&lt;/groupId&gt;</span>
  <span class="nt">&lt;artifactId&gt;</span>reactor-core<span class="nt">&lt;/artifactId&gt;</span>
  <span class="nt">&lt;version&gt;</span>3.0.0.RC2<span class="nt">&lt;/version&gt;</span>
<span class="nt">&lt;/dependency&gt;</span>
</code></pre></div></div>

<p>也可以使用Gradle:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">compile</span> <span class="err">'</span><span class="n">io</span><span class="o">.</span><span class="na">projectreactor</span><span class="o">:</span><span class="n">reactor</span><span class="o">-</span><span class="nl">core:</span><span class="mf">3.0</span><span class="o">.</span><span class="mi">0</span><span class="o">.</span><span class="na">RC2</span><span class="err">'</span>
</code></pre></div></div>

<h2 id="工作原理">工作原理</h2>

<p>Reactive由一系列事件以及发布和订阅这些事件的2个参与方组成的一个序列。我们也可以称之为stream。如果需要，我们使用streams这个名词，但是java8有一个java.util.Stream库，与我们在这儿要讲的概念是不同的，不要将这2个概念混淆。我们尽量集中阐述publisher和subscriber（Reactive Streams的行为）。</p>

<p>我们会使用<code class="language-plaintext highlighter-rouge">Reactor</code>库，把publisher称为 <code class="language-plaintext highlighter-rouge">Flux</code>（实现了Reactive Streams的<code class="language-plaintext highlighter-rouge">Publisher</code>接口），在RxJava库中的名称是<code class="language-plaintext highlighter-rouge">Observable</code> ，代表的是类似的概念。(Reactor2.0中的名称为Stream，很容易跟Java 8 的 Streams混淆，因此我们只使用Reactor 3.0中的新定义)。</p>

<!-- more -->

<h2 id="创建">创建</h2>

<p><code class="language-plaintext highlighter-rouge">Flux</code> 是POJO类型的事件序列的一个Publisher，例如 <code class="language-plaintext highlighter-rouge">Flux&lt;T&gt;</code> 是类型 <code class="language-plaintext highlighter-rouge">T</code> 的一个Publisher。<code class="language-plaintext highlighter-rouge">Flux</code> 有一系列从不同的来源创建示例的静态方法。例如从数组创建 <code class="language-plaintext highlighter-rouge">Flux</code> ：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nc">Flux</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">flux</span> <span class="o">=</span> <span class="nc">Flux</span><span class="o">.</span><span class="na">just</span><span class="o">(</span><span class="s">"red"</span><span class="o">,</span> <span class="s">"white"</span><span class="o">,</span> <span class="s">"blue"</span><span class="o">);</span>

</code></pre></div></div>
<p>我们创建了一个Flux，现在我们开始用它做一些事情。实际上只有2件事可以做：操作（转换或与其它序列组合）和订阅。</p>

<h2 id="单值序列">单值序列</h2>

<p>我们经常遇到的序列往往只有一个元素，或者是没有元素，例如通过id查找记录。在Reactor中Mono表示单值Flux或空Flux。Mono的API与Flux类似，但是更简洁，因为不是所有的操作对单值序列有意义。RxJava中类似的类型叫Single，空序列叫Completable。在Reactor中空序列是<code class="language-plaintext highlighter-rouge">Mono&lt;Void&gt;</code>。</p>

<h2 id="操作符">操作符</h2>

<p>Flux的绝大部分方法都是操作。在这儿我们不会把所有的方法都讲一遍（可以看javadoc），我们只需要弄明白操作是什么，可以做什么。
例如，可以用<code class="language-plaintext highlighter-rouge">log()</code>将Flux的内部事件显示出来，或者用<code class="language-plaintext highlighter-rouge">map()</code>进行转换：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nc">Flux</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">flux</span> <span class="o">=</span> <span class="nc">Flux</span><span class="o">.</span><span class="na">just</span><span class="o">(</span><span class="s">"red"</span><span class="o">,</span> <span class="s">"white"</span><span class="o">,</span> <span class="s">"blue"</span><span class="o">);</span>

<span class="nc">Flux</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">upper</span> <span class="o">=</span> <span class="n">flux</span>
  <span class="o">.</span><span class="na">log</span><span class="o">()</span>
  <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">String:</span><span class="o">:</span><span class="n">toUpperCase</span><span class="o">);</span>

</code></pre></div></div>

<p>这段代码将输入的字符串转换成大写，非常简单明了。同时很有意思的一点是（时刻注意，虽然刚开始不太习惯），数据并没有开始处理。什么都不会显示，因为什么都没有发生（可以自己运行一下代码），调用Flux的操作符仅仅是建立了一个执行计划。操作符实现的逻辑只有当数据开始流动时才会执行，当某一方订阅这个Flux的时候。</p>

<p>Java 8 的 Streams 也有类似的处理数据流的方式：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nc">Stream</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">stream</span> <span class="o">=</span> <span class="nc">Streams</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"red"</span><span class="o">,</span> <span class="s">"white"</span><span class="o">,</span> <span class="s">"blue"</span><span class="o">);</span>
<span class="nc">Stream</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">upper</span> <span class="o">=</span> <span class="n">stream</span><span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="n">value</span> <span class="o">-&gt;</span> <span class="o">{</span>
    <span class="nc">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="k">return</span> <span class="n">value</span><span class="o">.</span><span class="na">toUpperCase</span><span class="o">();</span>
<span class="o">});</span>

</code></pre></div></div>
<p>但是Flux 和 Stream有非常大的差异，Stream的API不适用于Reactive。</p>

<h2 id="订阅">订阅</h2>

<p>要让数据流生效，我们需要用subscribe()方法来订阅Flux，这些方法会回溯我们之前定义的操作链，请求publisher 产生数据。在我们的简单示例中，字符串集合会被遍历进行处理。在更复杂的场景中，可能是从文件系统读取文件，或者从数据库中读取数据，或者是调用一个http服务。</p>

<p>开始调用subscribe()：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nc">Flux</span><span class="o">.</span><span class="na">just</span><span class="o">(</span><span class="s">"red"</span><span class="o">,</span> <span class="s">"white"</span><span class="o">,</span> <span class="s">"blue"</span><span class="o">)</span>
  <span class="o">.</span><span class="na">log</span><span class="o">()</span>
  <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">String:</span><span class="o">:</span><span class="n">toUpperCase</span><span class="o">)</span>
<span class="o">.</span><span class="na">subscribe</span><span class="o">();</span>

</code></pre></div></div>

<p>输出内容：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
09:17:59.665 [main] INFO reactor.core.publisher.FluxLog -  onSubscribe(reactor.core.publisher.FluxIterable$IterableSubscription@3ffc5af1)
09:17:59.666 [main] INFO reactor.core.publisher.FluxLog -  request(unbounded)
09:17:59.666 [main] INFO reactor.core.publisher.FluxLog -  onNext(red)
09:17:59.667 [main] INFO reactor.core.publisher.FluxLog -  onNext(white)
09:17:59.667 [main] INFO reactor.core.publisher.FluxLog -  onNext(blue)
09:17:59.667 [main] INFO reactor.core.publisher.FluxLog -  onComplete()

</code></pre></div></div>

<p>可以看到当subscribe()没有参数时，会请求 publisher 发送所有的数据 - 只有一个request并且是 “unbounded”。我们还可以看到发布的每一项的回调(onNext())，结束的回调(onComplete())，以及原始订阅的回调(onSubscribe())。如果需要，我们还可以用Flux的doOn*()方法来监听这些事件的回调。</p>

<p>subscribe()方法是重载的，有很多变体。其中一个重要且常用的形式是带回调参数。第一个参数是 Consumer ，用于每一个数据项的回调，还可以增加一个可选的 Consumer 用于错误处理，以及一个序列完成后执行的 Runnable 。</p>

<p>例如，为每一个数据项增加回调：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nc">Flux</span><span class="o">.</span><span class="na">just</span><span class="o">(</span><span class="s">"red"</span><span class="o">,</span> <span class="s">"white"</span><span class="o">,</span> <span class="s">"blue"</span><span class="o">)</span>
    <span class="o">.</span><span class="na">log</span><span class="o">()</span>
    <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">String:</span><span class="o">:</span><span class="n">toUpperCase</span><span class="o">)</span>
<span class="o">.</span><span class="na">subscribe</span><span class="o">(</span><span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">::</span><span class="n">println</span><span class="o">);</span>

</code></pre></div></div>

<p>输出为：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
09:56:12.680 [main] INFO reactor.core.publisher.FluxLog -  onSubscribe(reactor.core.publisher.FluxArray$ArraySubscription@59f99ea)
09:56:12.682 [main] INFO reactor.core.publisher.FluxLog -  request(unbounded)
09:56:12.682 [main] INFO reactor.core.publisher.FluxLog -  onNext(red)
RED
09:56:12.682 [main] INFO reactor.core.publisher.FluxLog -  onNext(white)
WHITE
09:56:12.682 [main] INFO reactor.core.publisher.FluxLog -  onNext(blue)
BLUE
09:56:12.682 [main] INFO reactor.core.publisher.FluxLog -  onComplete()

</code></pre></div></div>

<p>我们可以通过多种方法控制数据流使它变成 “bounded” 。用于控制的内部接口是从 Subscriber 获取到的 Subscription 。与前面简单调用 subscribe() 等价的复杂形式是：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="o">.</span><span class="na">subscribe</span><span class="o">(</span><span class="k">new</span> <span class="nc">Subscriber</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;()</span> <span class="o">{</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onSubscribe</span><span class="o">(</span><span class="nc">Subscription</span> <span class="n">s</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">s</span><span class="o">.</span><span class="na">request</span><span class="o">(</span><span class="nc">Long</span><span class="o">.</span><span class="na">MAX_VALUE</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">onNext</span><span class="o">(</span><span class="nc">String</span> <span class="n">t</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">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">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">onError</span><span class="o">(</span><span class="nc">Throwable</span> <span class="n">t</span><span class="o">)</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">onComplete</span><span class="o">()</span> <span class="o">{</span>
    <span class="o">}</span>

<span class="o">});</span>

</code></pre></div></div>

<p>想要控制数据流为一次消费2个数据项，可以更加智能的使用Subscription ：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="o">.</span><span class="na">subscribe</span><span class="o">(</span><span class="k">new</span> <span class="nc">Subscriber</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;()</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kt">long</span> <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">Subscription</span> <span class="n">subscription</span><span class="o">;</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onSubscribe</span><span class="o">(</span><span class="nc">Subscription</span> <span class="n">subscription</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">subscription</span> <span class="o">=</span> <span class="n">subscription</span><span class="o">;</span>
        <span class="n">subscription</span><span class="o">.</span><span class="na">request</span><span class="o">(</span><span class="mi">2</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">onNext</span><span class="o">(</span><span class="nc">String</span> <span class="n">t</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">count</span><span class="o">++;</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">count</span><span class="o">&gt;=</span><span class="mi">2</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
            <span class="n">subscription</span><span class="o">.</span><span class="na">request</span><span class="o">(</span><span class="mi">2</span><span class="o">);</span>
        <span class="o">}</span>
     <span class="o">}</span>
<span class="o">...</span>

</code></pre></div></div>

<p>这个 Subscriber 每次会打包2个数据项。这个场景很普遍，因此我们会考虑把实现提取到一个专门的类中以方便使用。输出如下：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
09:47:13.562 [main] INFO reactor.core.publisher.FluxLog -  onSubscribe(reactor.core.publisher.FluxArray$ArraySubscription@61832929)
09:47:13.564 [main] INFO reactor.core.publisher.FluxLog -  request(2)
09:47:13.564 [main] INFO reactor.core.publisher.FluxLog -  onNext(red)
09:47:13.565 [main] INFO reactor.core.publisher.FluxLog -  onNext(white)
09:47:13.565 [main] INFO reactor.core.publisher.FluxLog -  request(2)
09:47:13.565 [main] INFO reactor.core.publisher.FluxLog -  onNext(blue)
09:47:13.565 [main] INFO reactor.core.publisher.FluxLog -  onComplete()

</code></pre></div></div>

<p>事实上批量订阅是一个非常普遍的场景，因此 Flux 已经包含了相关的方法。上面的例子可以实现为：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nc">Flux</span><span class="o">.</span><span class="na">just</span><span class="o">(</span><span class="s">"red"</span><span class="o">,</span> <span class="s">"white"</span><span class="o">,</span> <span class="s">"blue"</span><span class="o">)</span>
  <span class="o">.</span><span class="na">log</span><span class="o">()</span>
  <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">String:</span><span class="o">:</span><span class="n">toUpperCase</span><span class="o">)</span>
<span class="o">.</span><span class="na">subscribe</span><span class="o">(</span><span class="kc">null</span><span class="o">,</span> <span class="mi">2</span><span class="o">);</span>

</code></pre></div></div>

<p>（注意subscribe方法带了一个请求限制参数）输出为：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
10:25:43.739 [main] INFO reactor.core.publisher.FluxLog -  onSubscribe(reactor.core.publisher.FluxArray$ArraySubscription@4667ae56)
10:25:43.740 [main] INFO reactor.core.publisher.FluxLog -  request(2)
10:25:43.740 [main] INFO reactor.core.publisher.FluxLog -  onNext(red)
10:25:43.741 [main] INFO reactor.core.publisher.FluxLog -  onNext(white)
10:25:43.741 [main] INFO reactor.core.publisher.FluxLog -  request(2)
10:25:43.741 [main] INFO reactor.core.publisher.FluxLog -  onNext(blue)
10:25:43.741 [main] INFO reactor.core.publisher.FluxLog -  onComplete()

</code></pre></div></div>

<h2 id="线程调度和后台处理">线程、调度和后台处理</h2>

<p>上面的示例中有一个有趣的特点是所有的log方法都是在主线程中执行的，即 subscribe() 调用者的线程。这是一个关键点：Reactor以尽可能少的线程来实现高性能。过去5年我们习惯于使用多线程、线程池和异步处理来提升系统性能。对于这种新的思路可能会比较诧异。但是事实是：即使是JVM这种专门对线程处理做过优化的技术，线程切换的成本也是很高的。在单个线程上进行计算总是要快的多。Reactor给了我们进行异步编程的方法，并且假设我们知道我们在做什么。</p>

<p>Flux提供了一些方法来控制线程的边界。例如，可以使用 <code class="language-plaintext highlighter-rouge">Flux.subscribeOn()</code> 配置一个订阅在后台线程中进行处理:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nc">Flux</span><span class="o">.</span><span class="na">just</span><span class="o">(</span><span class="s">"red"</span><span class="o">,</span> <span class="s">"white"</span><span class="o">,</span> <span class="s">"blue"</span><span class="o">)</span>
  <span class="o">.</span><span class="na">log</span><span class="o">()</span>
  <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">String:</span><span class="o">:</span><span class="n">toUpperCase</span><span class="o">)</span>
  <span class="o">.</span><span class="na">subscribeOn</span><span class="o">(</span><span class="nc">Schedulers</span><span class="o">.</span><span class="na">parallel</span><span class="o">())</span>
<span class="o">.</span><span class="na">subscribe</span><span class="o">(</span><span class="kc">null</span><span class="o">,</span> <span class="mi">2</span><span class="o">);</span>

</code></pre></div></div>

<p>输出结果：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
13:43:41.279 [parallel-1-1] INFO reactor.core.publisher.FluxLog -  onSubscribe(reactor.core.publisher.FluxArray$ArraySubscription@58663fc3)
13:43:41.280 [parallel-1-1] INFO reactor.core.publisher.FluxLog -  request(2)
13:43:41.281 [parallel-1-1] INFO reactor.core.publisher.FluxLog -  onNext(red)
13:43:41.281 [parallel-1-1] INFO reactor.core.publisher.FluxLog -  onNext(white)
13:43:41.281 [parallel-1-1] INFO reactor.core.publisher.FluxLog -  request(2)
13:43:41.281 [parallel-1-1] INFO reactor.core.publisher.FluxLog -  onNext(blue)
13:43:41.281 [parallel-1-1] INFO reactor.core.publisher.FluxLog -  onComplete()

</code></pre></div></div>

<p>可以看到订阅和所有的处理都在 “parallel-1-1” 这个后台线程中。单线程对于CPU密集型的处理来说是没问题的。然而如果是IO密集型的处理就可能会阻塞。在这个场景中，我们希望处理尽可能的完成不至于阻塞调用方。一个线程池仍会提供很大的帮助，我们可以用 <code class="language-plaintext highlighter-rouge">Schedulers.parallel()</code> 获取线程池。将单个数据项的处理拆分到独立的线程中进行处理，我们需要把它放到独立的发布方中，每个发布方都在后台线程中请求执行结果。一种方法是调用 flatMap() 操作，会把数据项映射到一个 Publisher 并返回一个新类型的序列：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nc">Flux</span><span class="o">.</span><span class="na">just</span><span class="o">(</span><span class="s">"red"</span><span class="o">,</span> <span class="s">"white"</span><span class="o">,</span> <span class="s">"blue"</span><span class="o">)</span>
  <span class="o">.</span><span class="na">log</span><span class="o">()</span>
  <span class="o">.</span><span class="na">flatMap</span><span class="o">(</span><span class="n">value</span> <span class="o">-&gt;</span>
     <span class="nc">Mono</span><span class="o">.</span><span class="na">just</span><span class="o">(</span><span class="n">value</span><span class="o">.</span><span class="na">toUpperCase</span><span class="o">())</span>
       <span class="o">.</span><span class="na">subscribeOn</span><span class="o">(</span><span class="nc">Schedulers</span><span class="o">.</span><span class="na">parallel</span><span class="o">()),</span>
     <span class="mi">2</span><span class="o">)</span>
<span class="o">.</span><span class="na">subscribe</span><span class="o">(</span><span class="n">value</span> <span class="o">-&gt;</span> <span class="o">{</span>
  <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"Consumed: "</span> <span class="o">+</span> <span class="n">value</span><span class="o">);</span>
<span class="o">})</span>

</code></pre></div></div>

<p>注意 <code class="language-plaintext highlighter-rouge">flatMap()</code> 把数据项放入一个子 publisher ，这样可以控制每个子项的订阅而不是整个序列的订阅。Reactor内部的默认行为可以尽可能长的挂起在一个线程上，因此如果需要特定的数据项在后台线程中处理，必须要明确的指明。事实上这是一系列强制进行并行计算的方法中的一种。</p>

<p>输出内容：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
15:24:36.596 [main] INFO reactor.core.publisher.FluxLog -  onSubscribe(reactor.core.publisher.FluxIterable$IterableSubscription@6f1fba17)
15:24:36.610 [main] INFO reactor.core.publisher.FluxLog -  request(2)
15:24:36.610 [main] INFO reactor.core.publisher.FluxLog -  onNext(red)
15:24:36.613 [main] INFO reactor.core.publisher.FluxLog -  onNext(white)
15:24:36.613 [parallel-1-1] INFO com.example.FluxFeaturesTests - Consumed: RED
15:24:36.613 [parallel-1-1] INFO reactor.core.publisher.FluxLog -  request(1)
15:24:36.613 [parallel-1-1] INFO reactor.core.publisher.FluxLog -  onNext(blue)
15:24:36.613 [parallel-1-1] INFO reactor.core.publisher.FluxLog -  onComplete()
15:24:36.614 [parallel-3-1] INFO com.example.FluxFeaturesTests - Consumed: BLUE
15:24:36.617 [parallel-2-1] INFO com.example.FluxFeaturesTests - Consumed: WHITE

</code></pre></div></div>

<p>现在是多个线程在进行处理，并且 flatMap() 中的批量参数保证只要可能每次都会处理2个数据项。Reactor会让自己尽可能的聪明，预先从 Publisher 中提取数据项，并且估算订阅方的等待时间。</p>

<p>Flux 还有一个 publishOn() 方法的作用类似，只不过控制的是发布方的行为：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nc">Flux</span><span class="o">.</span><span class="na">just</span><span class="o">(</span><span class="s">"red"</span><span class="o">,</span> <span class="s">"white"</span><span class="o">,</span> <span class="s">"blue"</span><span class="o">)</span>
  <span class="o">.</span><span class="na">log</span><span class="o">()</span>
  <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">String:</span><span class="o">:</span><span class="n">toUpperCase</span><span class="o">)</span>
  <span class="o">.</span><span class="na">subscribeOn</span><span class="o">(</span><span class="nc">Schedulers</span><span class="o">.</span><span class="na">newParallel</span><span class="o">(</span><span class="s">"sub"</span><span class="o">))</span>
  <span class="o">.</span><span class="na">publishOn</span><span class="o">(</span><span class="nc">Schedulers</span><span class="o">.</span><span class="na">newParallel</span><span class="o">(</span><span class="s">"pub"</span><span class="o">),</span> <span class="mi">2</span><span class="o">)</span>
<span class="o">.</span><span class="na">subscribe</span><span class="o">(</span><span class="n">value</span> <span class="o">-&gt;</span> <span class="o">{</span>
    <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"Consumed: "</span> <span class="o">+</span> <span class="n">value</span><span class="o">);</span>
<span class="o">});</span>

</code></pre></div></div>

<p>输出内容：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
15:12:09.750 [sub-1-1] INFO reactor.core.publisher.FluxLog -  onSubscribe(reactor.core.publisher.FluxIterable$IterableSubscription@172ed57)
15:12:09.758 [sub-1-1] INFO reactor.core.publisher.FluxLog -  request(2)
15:12:09.759 [sub-1-1] INFO reactor.core.publisher.FluxLog -  onNext(red)
15:12:09.759 [sub-1-1] INFO reactor.core.publisher.FluxLog -  onNext(white)
15:12:09.770 [pub-1-1] INFO com.example.FluxFeaturesTests - Consumed: RED
15:12:09.771 [pub-1-1] INFO com.example.FluxFeaturesTests - Consumed: WHITE
15:12:09.777 [sub-1-1] INFO reactor.core.publisher.FluxLog -  request(2)
15:12:09.777 [sub-1-1] INFO reactor.core.publisher.FluxLog -  onNext(blue)
15:12:09.777 [sub-1-1] INFO reactor.core.publisher.FluxLog -  onComplete()
15:12:09.783 [pub-1-1] INFO com.example.FluxFeaturesTests - Consumed: BLUE

</code></pre></div></div>

<p>注意订阅方的回调（内容为 “Consumed: …​”）执行在发布方线程 <code class="language-plaintext highlighter-rouge">pub-1-1</code> 上。如果把 subscribeOn() 方法去掉，会发现所有的数据项的处理都在线程 <code class="language-plaintext highlighter-rouge">pub-1-1</code> 上。这再一次说明 Reactor 使用尽可能少的线程 - 如果没有明确的指定要切换线程，下一个调用会在当前调用的线程上执行。</p>

<h2 id="提取器有副作用的订阅者">提取器：有副作用的订阅者</h2>

<p>另一种订阅序列的方式是调用 <code class="language-plaintext highlighter-rouge">Mono.block()</code> 或 <code class="language-plaintext highlighter-rouge">Mono.toFuture()</code> 或 <code class="language-plaintext highlighter-rouge">Flux.toStream()</code> (这些是提取器方法，将 Reactive 类型转换为阻塞类型)。Flux 还有 collectList() 和 collectMap() 将 Flux 转换成 Mono。他们并没有真正的订阅序列，但是他们会抛弃控制订阅单个数据项的能力。</p>

<p>警告：
一个黄金规则是“永远不要调用提取器”。当然有一些例外，例如在测试程序中需要能够通过阻塞来汇总结果。</p>

<p>这些方法用于将 Reactive 转换为阻塞模式，当我们需要适配一个老式的API，例如Spring MVC的时候。在调用 Mono.block() 的时候，我们放弃了 Reactive Streams 所有优势。这是 Reactive Streams 和 Java 8 Streams 的关键区别 - Java Stream只有 “all or nothing” 的订阅模式，等同于 Mono.block()。当然 subscribe() 也会阻塞调用线程，因此与转换方法一样危险，但是有足够的控制手段 - 可以用 subscribeOn() 防止阻塞，也可以通过背压来将数据项进行溢出并且定时的决定是否继续处理。</p>

<h2 id="总结">总结</h2>

<p>这篇文章我们讲述了 Reactive Streams 和 Reactor API 的基本概念。可以通过 <a href="https://github.com/dsyer/reactive-notes">GitHub</a> 上的示例代码或者是 <a href="https://github.com/reactor/lite-rx-api-hands-on">Lite RX Hands On</a> 实验项目来进一步了解。在下一篇文章中我们会更深入的发掘 Reactive 模型中的阻塞、分发、异步等方面，并且展示能够真正受益的机会。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><summary type="html"><![CDATA[书接上文 Reactive编程 ，我们继续用真实的代码来解释一些概念。我们会更进一步理解Reactive的与众不同以及它的功能。这些例子很抽象，但能够让我们更进一步理解用到的API和编程风格，真实的感受它的与众不同。我们将会看到Reactive的核心元素，学习如何控制数据流，如果需要的话还会用到后台线程进行处理。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/code.jpg" /><media:content medium="image" url="https://springcamp.cn/assets/images/code.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Reactive编程（一）:Reactive编程的背景</title><link href="https://springcamp.cn/reactive-program-1/" rel="alternate" type="text/html" title="Reactive编程（一）:Reactive编程的背景" /><published>2018-03-04T06:38:00+00:00</published><updated>2018-03-04T06:38:00+00:00</updated><id>https://springcamp.cn/reactive-program-1</id><content type="html" xml:base="https://springcamp.cn/reactive-program-1/"><![CDATA[<p>Reactive编程很有趣，现在也有各种各样的讨论，概念上不是很容易理解。本文会以具体的形式介绍相关的概念。Reactive编程跟并发和高性能在概念上有一些重合，但在原理上完全不同。Reactive编程跟函数式编程是非常类似的。一些人认为Reactive编程并不是什么新概念，他们在日常工作中经常使用（例如javascript）。另一些人认为这是微软做出的新发明（Reactive这个名字最早来源于C#）。在java编程中最近也有了类似的技术（参见<a href="http://www.reactive-streams.org/">Reactive Streams initiative</a>）。在何时和何处使用Reactive编程这个问题上很容易犯错误。</p>

<h2 id="1是什么">1.是什么？</h2>

<p>Reactive编程是一种通过将智能路由和事件消费组合起来改变行为的微架构风格。这个定义有些抽象，你在网上会接触到其它的各种各样的定义。
Reactive编程的概念可以追溯到1970年或者更早，因此并不是什么新鲜概念，但是因为有了微服务和多核处理器，在现在的企业应用中有了新的用法。
下面有几个简单明了的解释：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Reactive编程背后的基本思想是指表示随时间变化的值的特定数据类型。处理这些随时间变化的值的方法里本身也包含着随时间变化的值。
</code></pre></div></div>
<p>以及</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>可以简单的把程序当作是一个电子表格，把变量当作是表格里的单元格。当表格里的某些单元格的值变化后，如果其它的单元格引用了这些单元格的值，那么其它的单元格的值也会跟着改变。这跟函数式编程是一样的。
</code></pre></div></div>

<p>函数式编程往往代表着高性能、并发、异步、非阻塞IO。开始时我们可以不必非用函数式编程，Reactive模型可以很自然的处理这些问题。我们真正关心的是解决这些问题的实现。我们可以用同步和单线程的方式来实现一个有效的函数式编程框架，但是这么做并没什么意义。</p>

<h2 id="2-reactive编程的应用场景">2. Reactive编程的应用场景</h2>

<p>类似于“有什么好处？”这样的问题对于新手来说是很难回答的。有几个例子来解释通用的适用场景：</p>

<!-- more -->

<h3 id="外部服务调用">外部服务调用</h3>

<p>当今的后端系统很多都是RESTful的，底层协议是阻塞和同步的。并且服务之间会相互调用，必须要等到第一个请求调用完成后才能调用下一个请求。客户端可能在服务端处理完成之前就放弃请求了。因此外部服务调用，特别是需要调用多个服务才能完成处理的时候，是一个需要去优化的场景。</p>

<h3 id="高并发的消息消费">高并发的消息消费</h3>

<p>高并发的消息处理是企业应用的常见场景，Reactive模式非常适合处理消息（事件可以很容易的转换为消息）。</p>

<h3 id="电子表格">电子表格</h3>

<p>这不是一个企业应用场景，但是Reactive模式可以很轻松的处理这种需求。</p>

<h3 id="对异步调用进行抽象">对异步调用进行抽象</h3>

<p>Reactive编程可以让我们不用关心调用是同步的还是异步的，单纯的异步编程是很繁琐的，Reactive模式可以简化异步编程。</p>

<h2 id="3-比较">3. 比较</h2>

<p>以下是几个与Reactive编程有类似概念的技术：</p>

<h3 id="ruby-event-machine">Ruby Event-Machine</h3>

<p>Event-Machine是一个并发编程的抽象。可以让Ruby用一个单线程来处理高并发的请求。</p>

<h3 id="actor-model">Actor Model</h3>

<p>与面向对象编程类似，Actor Model是计算机科学的一个重要研究方向，早在七十年代就有了。Actor是计算的一个抽象，可以用于并发系统。Actor之间相互发送消息，因此在某种意义上也是反应式的。Actor和Reactive在概念上有很高的重合度。区别往往在实现层面（例如<a href="https://doc.akka.io/docs/akka/current/java.html">Akka</a>可以用于进程间通信，是这个框架的显著特点）。</p>

<h3 id="deferred-results-futures">Deferred results (Futures)</h3>

<p>Java 1.5引入了很多新特性，包括Doug Lea 的 “java.util.concurrent”，其中有deferred result这个概念，被封装成Future。这是对异步编程进行抽象的一个很好的例子，可以非异步的形式来开发异步程序。在简单的并发任务场景下，Future是很好用的，但是当这些任务相互依赖的时候，会陷入“nested callback hell”。而Reactive可以避免这种情况的出现。</p>

<h3 id="map-reduce-and-fork-join">Map-reduce and fork-join</h3>

<p>将并行处理进行抽象是很有意义的，这样的例子很多。Java编程中最近有了Map-reduce 和 fork-join。Map-reduce用于Hadoop，fork-join是jdk1.7之后的版本自带的功能。这2个技术与Deferred results类似，不能应对复杂的组合调用场景。</p>

<h3 id="coroutines">Coroutines</h3>

<p>coroutine可以相互之间传递控制权，不用由调用者统一协调，可以简化并发编程。可以用coroutine来实现Reactive编程。Fibers和Generators都属于coroutine技术。</p>

<h2 id="reactive-programming-in-java">Reactive Programming in Java</h2>

<p>java不是“reactive”的语言，不能原生支持Coroutine。在JVM上的其它语言（Scala 和 Clojure）可以很好的支持Coroutine，java直到jdk9才支持。但是很多技术已经在jvm上实现了Reactive的支持：</p>

<h3 id="reactive-streams">Reactive Streams</h3>

<p>一个非常底层的约定，提供了Publisher和Subscriber接口。被集成到jdk9的java.util.concurrent.Flow包中。</p>

<h3 id="rxjava">RxJava</h3>

<p>Netflix开源的技术，内部已经使用Reactive编程已经很长时间了。根据David Karnok的<a href="https://akarnokd.blogspot.co.uk/2016/03/operator-fusion-part-1.html">Generations of Reactive</a>中的分类，RxJava是第二代Reactive技术。</p>

<h3 id="reactor">Reactor</h3>

<p>Spring团队开源的技术。</p>

<p>根据David Karnok的<a href="https://akarnokd.blogspot.co.uk/2016/03/operator-fusion-part-1.html">Generations of Reactive</a>中的分类，RxJava是第四代Reactive技术。</p>

<h3 id="spring-framework-50">Spring Framework 5.0</h3>

<p>内置了Reactive的特性，包含了构建HTTP服务和客户端的工具。Spring构建于Reactor技术之上，但是用户可以自由选择底层是使用Reactor还是RxJava。服务器支持omcat, Jetty, Netty 和 Undertow。</p>

<h3 id="ratpack"><a href="https://ratpack.io/">Ratpack</a></h3>

<p>一组构建高性能web应用的工具，Spring Boot可以直接使用该框架。</p>

<h3 id="akka">Akka</h3>

<p>一个实现Actor模型的开发框架，可以在Scala和Java中使用，是第三代的Reactive技术。</p>

<h2 id="4-为什么是现在">4. 为什么是现在？</h2>

<p>Reactive技术的热度在不断提升，是因为该技术可以帮助我们节省服务器资源，可以用少量的线程支持更高的负载。Reactive、非阻塞、异步提供了很好的解决问题的方法。但是天下没有免费的午餐，每种技术都有适用场景。Reactive并不能直接解决我们的问题，只是让我们在解决问题时多了一种选择。</p>

<h2 id="5-总结">5. 总结</h2>

<p>此文从概念上介绍了Reactive编程，在<a href="https://springcamp.cn/reactive-program-2/">下一节</a>中我们看一些具体的代码示例。最重要的是，我们会告诉你何时选择Reactive技术，何时选择现有的技术。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="java" /><summary type="html"><![CDATA[Reactive编程很有趣，现在也有各种各样的讨论，概念上不是很容易理解。本文会以具体的形式介绍相关的概念。Reactive编程跟并发和高性能在概念上有一些重合，但在原理上完全不同。Reactive编程跟函数式编程是非常类似的。一些人认为Reactive编程并不是什么新概念，他们在日常工作中经常使用（例如javascript）。另一些人认为这是微软做出的新发明（Reactive这个名字最早来源于C#）。在java编程中最近也有了类似的技术（参见Reactive Streams initiative）。在何时和何处使用Reactive编程这个问题上很容易犯错误。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/background.jpg" /><media:content medium="image" url="https://springcamp.cn/assets/images/background.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">SpringBoot项目中使用AOP</title><link href="https://springcamp.cn/spring-boot-aop/" rel="alternate" type="text/html" title="SpringBoot项目中使用AOP" /><published>2018-02-18T13:31:00+00:00</published><updated>2018-02-18T13:31:00+00:00</updated><id>https://springcamp.cn/spring-boot-aop</id><content type="html" xml:base="https://springcamp.cn/spring-boot-aop/"><![CDATA[<h2 id="1概述">1.概述</h2>

<p>将通用的逻辑用AOP技术实现可以极大的简化程序的编写，例如验签、鉴权等。Spring的声明式事务也是通过AOP技术实现的。</p>

<p>具体的代码参照 <a href="https://github.com/qihaiyan/springcamp/tree/main/spring-aop">示例项目 https://github.com/qihaiyan/springcamp/tree/main/spring-aop</a></p>

<p>Spring的AOP技术主要有4个核心概念：</p>

<ol>
  <li>
    <p>Pointcut: 切点，用于定义哪个方法会被拦截，例如 <code class="language-plaintext highlighter-rouge">execution(* cn.springcamp.springaop.service.*.*(..))</code></p>
  </li>
  <li>
    <p>Advice: 拦截到方法后要执行的动作</p>
  </li>
  <li>
    <p>Aspect: 切面，把Pointcut和Advice组合在一起形成一个切面</p>
  </li>
  <li>
    <p>Join Point: 在执行时Pointcut的一个实例</p>
  </li>
  <li>
    <p>Weaver: 实现AOP的框架，例如 AspectJ 或 Spring AOP</p>
  </li>
</ol>

<h2 id="2-切点定义">2. 切点定义</h2>

<p>常用的Pointcut定义有 execution 和 @annotation 两种。execution 定义对方法无侵入，用于实现比较通用的切面。@annotation 可以作为注解加到特定的方法上，例如Spring的Transaction注解。</p>

<p>execution切点定义应该放在一个公共的类中，集中管理切点定义。</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">CommonJoinPointConfig</span> <span class="o">{</span>
    <span class="nd">@Pointcut</span><span class="o">(</span><span class="s">"execution(* cn.springcamp.springaop.service.*.*(..))"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">serviceLayerExecution</span><span class="o">()</span> <span class="o">{}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>这样在具体的Aspect类中可以通过 <code class="language-plaintext highlighter-rouge">CommonJoinPointConfig.serviceLayerExecution()</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">BeforeAspect</span> <span class="o">{</span>
    <span class="nd">@Before</span><span class="o">(</span><span class="s">"CommonJoinPointConfig.serviceLayerExecution()"</span><span class="o">)</span>
    <span class="o">...</span>
<span class="o">}</span>
</code></pre></div></div>

<p>当切点需要改变时，只需修改CommonJoinPointConfig类即可，不用修改每个Aspect类。</p>

<!-- more -->

<h2 id="3-常用的切面">3. 常用的切面</h2>

<ol>
  <li>
    <p>Before: 在方法执行之前执行Advice，常用于验签、鉴权等。</p>
  </li>
  <li>
    <p>After: 在方法执行完成后执行，无论是执行成功还是抛出异常.</p>
  </li>
  <li>
    <p>AfterReturning: 仅在方法执行成功后执行.</p>
  </li>
  <li>
    <p>AfterThrowing: 仅在方法执抛出异常后执行.</p>
  </li>
</ol>

<p>一个简单的Aspect：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Aspect</span>
<span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">BeforeAspect</span> <span class="o">{</span>
    <span class="nd">@Before</span><span class="o">(</span><span class="s">"CommonJoinPointConfig.serviceLayerExecution()"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">before</span><span class="o">(</span><span class="nc">JoinPoint</span> <span class="n">joinPoint</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">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">" -------------&gt; Before Aspect "</span><span class="o">);</span>
        <span class="nc">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">" -------------&gt; before execution of "</span> <span class="o">+</span> <span class="n">joinPoint</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="4-自定义注解">4. 自定义注解</h2>

<p>假设我们想收集特定方法的执行时间，一种比较合理的方式是自定义一个注解，然后在需要收集执行时间的方法上加上这个注解。</p>

<p>首先定义一个注解TrackTime：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Target</span><span class="o">({</span><span class="nc">ElementType</span><span class="o">.</span><span class="na">METHOD</span><span class="o">,</span> <span class="nc">ElementType</span><span class="o">.</span><span class="na">TYPE</span><span class="o">})</span>
<span class="nd">@Retention</span><span class="o">(</span><span class="nc">RetentionPolicy</span><span class="o">.</span><span class="na">RUNTIME</span><span class="o">)</span>
<span class="kd">public</span> <span class="nd">@interface</span> <span class="nc">TrackTime</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="nf">param</span><span class="o">()</span> <span class="k">default</span> <span class="s">""</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<p>然后再定义一个Aspect类，用于实现注解的行为：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Aspect</span>
<span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">TrackTimeAspect</span> <span class="o">{</span>
    <span class="nd">@Around</span><span class="o">(</span><span class="s">"@annotation(trackTime)"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">Object</span> <span class="nf">around</span><span class="o">(</span><span class="nc">ProceedingJoinPoint</span> <span class="n">joinPoint</span><span class="o">,</span> <span class="nc">TrackTime</span> <span class="n">trackTime</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Throwable</span> <span class="o">{</span>
        <span class="nc">Object</span> <span class="n">result</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
        <span class="kt">long</span> <span class="n">startTime</span> <span class="o">=</span> <span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">();</span>
        <span class="n">result</span> <span class="o">=</span> <span class="n">joinPoint</span><span class="o">.</span><span class="na">proceed</span><span class="o">();</span>
        <span class="kt">long</span> <span class="n">timeTaken</span> <span class="o">=</span> <span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">()</span> <span class="o">-</span> <span class="n">startTime</span><span class="o">;</span>
        <span class="nc">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">" -------------&gt; Time Taken by "</span> <span class="o">+</span> <span class="n">joinPoint</span> <span class="o">+</span> <span class="s">" with param["</span> <span class="o">+</span> <span class="n">trackTime</span><span class="o">.</span><span class="na">param</span><span class="o">()</span> <span class="o">+</span> <span class="s">"] is "</span> <span class="o">+</span> <span class="n">timeTaken</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="o">}</span>
</code></pre></div></div>

<p>在某个方法上使用这个注解，就可以收集这个方法的执行时间：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@TrackTime</span><span class="o">(</span><span class="n">param</span> <span class="o">=</span> <span class="s">"myService"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">String</span> <span class="nf">runFoo</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">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">" -------------&gt; foo"</span><span class="o">);</span>
    <span class="k">return</span> <span class="s">"foo"</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<p>注意 <code class="language-plaintext highlighter-rouge">@TrackTime(param = "myService")</code> 注解是可以传参的。</p>

<p>为了让注解可以传参数，需要在定义注解时指定一个参数<code class="language-plaintext highlighter-rouge">String param() default "默认值"</code>，</p>

<p>同时在Aspect类中，around方法上加上相应的参数，@Around注解中也需要用参数的变量名trackTime，而不能用类名TrackTime。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Around</span><span class="o">(</span><span class="s">"@annotation(trackTime)"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">Object</span> <span class="nf">around</span><span class="o">(</span><span class="nc">ProceedingJoinPoint</span> <span class="n">joinPoint</span><span class="o">,</span> <span class="nc">TrackTime</span> <span class="n">trackTime</span><span class="o">)</span>
</code></pre></div></div>

<h2 id="5总结">5.总结</h2>

<p>在运行示例项目时，控制台会输出以下内容：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> -------------&gt; Before Aspect 
 -------------&gt; before execution of execution(String cn.springcamp.springaop.service.MyService.runFoo())
 -------------&gt; foo
 -------------&gt; Time Taken by execution(String cn.springcamp.springaop.service.MyService.runFoo()) with param[myService] is 8
 -------------&gt; After Aspect 
 -------------&gt; after execution of execution(String cn.springcamp.springaop.service.MyService.runFoo())
 -------------&gt; AfterReturning Aspect 
 -------------&gt; execution(String cn.springcamp.springaop.service.MyService.runFoo()) returned with value foo
</code></pre></div></div>

<p>可以看出几种 Aspect 的执行顺序依次为 Before After Around AfterReturning(AfterThrowing)</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><summary type="html"><![CDATA[1.概述]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/aop.png" /><media:content medium="image" url="https://springcamp.cn/assets/images/aop.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">如何避免java程序内存泄漏</title><link href="https://springcamp.cn/java-memeory-leak/" rel="alternate" type="text/html" title="如何避免java程序内存泄漏" /><published>2018-01-27T11:28:00+00:00</published><updated>2018-01-27T11:28:00+00:00</updated><id>https://springcamp.cn/java-memeory-leak</id><content type="html" xml:base="https://springcamp.cn/java-memeory-leak/"><![CDATA[<p>虽然jvm有垃圾回收机制，如果程序编写不注意某些特定规则，仍然会导致java程序内存泄漏，最终可能出现OutOfMemory异常。</p>

<h3 id="1java内存泄漏的原因">1.Java内存泄漏的原因</h3>

<p>java中的对象从使用上分为2种类型，被引用（referenced）的和不被引用（unreferenced）的。垃圾回收只会回收不被引用的对象。被引用的对象，即使已经不再使用了，也不会被回收。因此如果程序中有大量的被引用的无用对象时，就是出现内存泄漏。</p>

<h3 id="2java堆内存heap泄漏">2.java堆内存（Heap）泄漏</h3>

<p>jvm堆内存的大小是通过 -Xms 和 -Xmx两个参数指定的。</p>

<h4 id="21-对象被静态成员引用">2.1 对象被静态成员引用</h4>

<p>当大对象被静态成员引用时，会造成内存泄漏。</p>

<p>示例：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="nc">Random</span> <span class="n">random</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Random</span><span class="o">();</span>

<span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">ArrayList</span><span class="o">&lt;</span><span class="nc">Double</span><span class="o">&gt;</span> <span class="n">list</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;</span><span class="nc">Double</span><span class="o">&gt;(</span><span class="mi">1000000</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">&lt;</span> <span class="mi">1000000</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span> <span class="n">list</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">random</span><span class="o">.</span><span class="na">nextDouble</span><span class="o">());</span> <span class="o">}</span>
</code></pre></div></div>

<p>ArrayList是在堆上动态分配的对象，正常情况下使用完毕后，会被gc回收，但是在此示例中，由于被静态成员list引用，而静态成员是不会被回收的，所以会导致这个很大的ArrayList一直停留在堆内存中。</p>

<p>因此需要特别注意静态成员的使用方式，避免静态成员引用大对象或集合类型的对象（如ArrayList等）。</p>

<!-- more -->

<h4 id="22-string的intern方法">2.2 String的intern方法</h4>

<p>在大字符串上调用String.intern() 方法，intern()会将String放在jvm的内存池中（PermGen ），而jvm的内存池是不会被gc的。因此如果大字符串调用intern()方法后，会产生大量的无法gc的内存，导致内存泄漏。</p>

<p>如果必须要使用大字符串的intern方法，应该通过-XX:MaxPermSize参数调整PermGen内存的大小。</p>

<h3 id="23-读取流后没有关闭">2.3 读取流后没有关闭</h3>

<p>开发中经常忘记关闭流，这样会导致内存泄漏。因为每个流在操作系统层面都对应了打开的文件句柄，流没有关闭，会导致操作系统的文件句柄一直处于打开状态，而jvm会消耗内存来跟踪操作系统打开的文件句柄。
示例：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">BufferedReader</span> <span class="n">br</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">BufferedReader</span><span class="o">(</span><span class="k">new</span> <span class="nc">FileReader</span><span class="o">(</span><span class="n">path</span><span class="o">));</span>
<span class="k">return</span> <span class="n">br</span><span class="o">.</span><span class="na">readLine</span><span class="o">();</span>
</code></pre></div></div>

<p>要解决这个问题，在java8之前的版本中可以在finally中加入关闭操作：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nc">BufferedReader</span> <span class="n">br</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">BufferedReader</span><span class="o">(</span><span class="k">new</span> <span class="nc">FileReader</span><span class="o">(</span><span class="n">path</span><span class="o">));</span>
    <span class="k">try</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">br</span><span class="o">.</span><span class="na">readLine</span><span class="o">();</span>
    <span class="o">}</span> <span class="k">finally</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">br</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="n">br</span><span class="o">.</span><span class="na">close</span><span class="o">();</span>
    <span class="o">}</span>
</code></pre></div></div>

<p>java8中可以使用try-with-resources语句：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span> <span class="o">(</span><span class="nc">BufferedReader</span> <span class="n">br</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">BufferedReader</span><span class="o">(</span><span class="k">new</span> <span class="nc">FileReader</span><span class="o">(</span><span class="n">path</span><span class="o">)))</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">br</span><span class="o">.</span><span class="na">readLine</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<p>对于网络连接和数据库连接等也要注意连接的关闭，如果采用了连接池，那关闭操作是由连接池负责的，程序中可以不用处理。</p>

<h4 id="24-将没有实现hashcode和equals方法的对象加入到hashset中">2.4 将没有实现hashCode()和equals()方法的对象加入到HashSet中</h4>

<p>这是一个简单却很常见的场景。正常情况下Set会过滤重复的对象，但是如果没有hashCode() 和 equals()实现，重复对象会不断被加入到Set中，并且再也没有机会去移除。</p>

<p>因此给类都加上hashCode() 和 equals()方法的实现是一个好的编程习惯。可以通过Lombok的@EqualsAndHashCode很方便实现这种功能。</p>

<h3 id="3-查找内存泄漏的方法">3. 查找内存泄漏的方法</h3>

<h4 id="31-记录gc日志">3.1 记录gc日志</h4>

<p>通过在jvm参数中指定-verbose:gc，可以记录每次gc的详细情况，用于分析内存的使用。</p>

<h4 id="32-进行profiling">3.2 进行profiling</h4>

<p>通过Visual VM或jdk自带的Java Mission Control，进行内存分析。</p>

<h4 id="33-代码审查">3.3 代码审查</h4>

<p>通过代码审查和静态代码检查，发现导致内存泄漏问题的错误代码。</p>

<h3 id="4-总结">4. 总结</h3>

<p>代码层面的检查可以帮助发现部分内存泄漏的问题，但是生产环境中的内存泄漏往往不容易提前发现，因为很多问题是在大并发场景下才会出现。因此还需要通过压力测试工具进行压力测试，提前发现潜在的内存泄漏问题。</p>]]></content><author><name>qihaiyan</name></author><category term="java" /><category term="java" /><summary type="html"><![CDATA[虽然jvm有垃圾回收机制，如果程序编写不注意某些特定规则，仍然会导致java程序内存泄漏，最终可能出现OutOfMemory异常。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/memoryleak.png" /><media:content medium="image" url="https://springcamp.cn/assets/images/memoryleak.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">SpringBoot项目中使用redis缓存</title><link href="https://springcamp.cn/spring-data-cache/" rel="alternate" type="text/html" title="SpringBoot项目中使用redis缓存" /><published>2017-12-03T07:15:00+00:00</published><updated>2017-12-03T07:15:00+00:00</updated><id>https://springcamp.cn/spring-data-cache</id><content type="html" xml:base="https://springcamp.cn/spring-data-cache/"><![CDATA[<h2 id="1概述">1.概述</h2>

<p>在应用中有效的利用redis缓存可以很好的提升系统性能，特别是对于查询操作，可以有效的减少数据库压力。</p>

<p>具体的代码参照该 <a href="https://github.com/qihaiyan/boot-multi-datasource">示例项目</a></p>

<h2 id="2添加引用">2.添加引用</h2>

<p>在build.gradle加入</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>compile('org.springframework.boot:spring-boot-starter-data-redis')
</code></pre></div></div>
<p>SpringBoot会自动引入redis相关的jar包。加入该引用后，需要在本地安装redis并启动，否则程序启动时会报错。</p>

<h2 id="3通过注解启用缓存">3.通过注解启用缓存</h2>

<p>在SpringBoot中启用redis非常简单，只需要在Application主类上添加<code class="language-plaintext highlighter-rouge">@EnableCaching</code>注解，之后在需要启用缓存的查询方法上添加<code class="language-plaintext highlighter-rouge">@Cacheable</code>注解。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@SpringBootApplication</span>
<span class="nd">@EnableCaching</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">DemoApplication</span> <span class="kd">implements</span> <span class="nc">CommandLineRunner</span><span class="o">{</span>
<span class="o">...</span>
</code></pre></div></div>

<!-- more -->

<p>查询接口：</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">TestRepository</span> <span class="kd">extends</span> <span class="nc">JpaRepository</span><span class="o">&lt;</span><span class="nc">Test</span><span class="o">,</span> <span class="nc">Integer</span><span class="o">&gt;</span> <span class="o">{</span>
    <span class="nd">@Cacheable</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"testCache"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">Test</span> <span class="nf">findOne</span><span class="o">(</span><span class="nc">Integer</span> <span class="n">id</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>实体类需要实现Serializable接口，否则程序会报错，因为无法把java对象序列化到redis中。SpringBoot中redis默认使用DefaultSerializer，这个用的是jdk自身的序列化方法。</p>

<p>总共有以下几种序列化方法，具体的使用场景可以参考<a href="https://docs.spring.io/spring-data/redis/docs/1.8.9.RELEASE/reference/html/#redis:serializer">官方文档</a></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. GenericJackson2JsonRedisSerializer
2. GenericToStringSerializer
3. Jackson2JsonRedisSerializer
4. JacksonJsonRedisSerializer
5. JdkSerializationRedisSerializer
6. OxmSerializer
7. StringRedisSerializer
</code></pre></div></div>

<p>至此我们的程序就具有了从redis缓存中查询数据的能力，如果对redis中存储的KEY的美观程度不介意的话，工作到此就结束了。</p>

<h2 id="4美观的key">4.美观的KEY</h2>

<p>执行我们的程序以后，在redis-cli中执行<code class="language-plaintext highlighter-rouge">KEY *</code>命令，会发现key的值是一堆类似于乱码的东西:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"testCache:\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\x01"
</code></pre></div></div>

<p>这中key值对于redis的运维人员来说估计是不可接受的，我们要想办法让key值变的好看一些，至少要让人能看得懂。</p>

<p>出现上面的key值的原因就是spring中默认采用了SimpleKey这个类来生成redis的key。</p>

<p>解决方法也很简单，增加缓存配置，指定redis生成key的方式：</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">CacheConfig</span> <span class="kd">extends</span> <span class="nc">CachingConfigurerSupport</span> <span class="o">{</span>

    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">RedisTemplate</span> <span class="n">redisTemplate</span><span class="o">;</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">CacheManager</span> <span class="nf">cacheManager</span><span class="o">()</span> <span class="o">{</span>

        <span class="n">redisTemplate</span><span class="o">.</span><span class="na">setKeySerializer</span><span class="o">(</span><span class="k">new</span> <span class="nc">GenericToStringSerializer</span><span class="o">&lt;</span><span class="nc">Object</span><span class="o">&gt;(</span><span class="nc">Object</span><span class="o">.</span><span class="na">class</span><span class="o">));</span>

        <span class="nc">RedisCacheManager</span> <span class="n">cacheManager</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">RedisCacheManager</span><span class="o">(</span><span class="n">redisTemplate</span><span class="o">);</span>
        <span class="n">cacheManager</span><span class="o">.</span><span class="na">setDefaultExpiration</span><span class="o">(</span><span class="mi">3600</span><span class="o">);</span>
        <span class="n">cacheManager</span><span class="o">.</span><span class="na">setUsePrefix</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
        <span class="n">cacheManager</span><span class="o">.</span><span class="na">setCachePrefix</span><span class="o">(</span><span class="k">new</span> <span class="nc">RedisCachePrefix</span><span class="o">()</span> <span class="o">{</span>
            <span class="kd">private</span> <span class="kd">final</span> <span class="nc">RedisSerializer</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">serializer</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">StringRedisSerializer</span><span class="o">();</span>
            <span class="kd">private</span> <span class="kd">final</span> <span class="nc">String</span> <span class="n">delimiter</span> <span class="o">=</span> <span class="s">":"</span><span class="o">;</span>

            <span class="kd">public</span> <span class="kt">byte</span><span class="o">[]</span> <span class="nf">prefix</span><span class="o">(</span><span class="nc">String</span> <span class="n">cacheName</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">serializer</span>
                        <span class="o">.</span><span class="na">serialize</span><span class="o">(</span><span class="n">cacheName</span><span class="o">.</span><span class="na">concat</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">delimiter</span><span class="o">));</span>
            <span class="o">}</span>
        <span class="o">});</span>

        <span class="k">return</span> <span class="n">cacheManager</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">redisTemplate</span><span class="o">.</span><span class="na">setKeySerializer</span><span class="o">(</span><span class="k">new</span> <span class="nc">GenericToStringSerializer</span><span class="o">&lt;</span><span class="nc">Object</span><span class="o">&gt;(</span><span class="nc">Object</span><span class="o">.</span><span class="na">class</span><span class="o">));</span>
</code></pre></div></div>

<p>这行代码指定了redis中key值的生成方式，<code class="language-plaintext highlighter-rouge">GenericToStringSerializer</code>这个序列化方法会把java对象转换为字符串存储到redis中。</p>

<h2 id="5总结">5.总结</h2>

<p>在SpringBoot中启用redis缓存非常简单，只需要加几个注解即可。同时我们可以通过增加缓存配置的方式，让存储到redis中的key值具有良好的可读性，而不是一堆类似于乱码的数据。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="redis" /><summary type="html"><![CDATA[1.概述]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/cache.png" /><media:content medium="image" url="https://springcamp.cn/assets/images/cache.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">SpringBoot项目中的多数据源支持</title><link href="https://springcamp.cn/spring-jpa-multi-datasource/" rel="alternate" type="text/html" title="SpringBoot项目中的多数据源支持" /><published>2017-10-20T02:00:00+00:00</published><updated>2017-10-20T02:00:00+00:00</updated><id>https://springcamp.cn/spring-jpa-multi-datasource</id><content type="html" xml:base="https://springcamp.cn/spring-jpa-multi-datasource/"><![CDATA[<h2 id="1概述">1.概述</h2>

<p>项目中经常会遇到一个应用需要访问多个数据源的情况，本文介绍在SpringBoot项目中利用SpringDataJpa技术如何支持多个数据库的数据源。</p>

<p>具体的代码参照该 <a href="https://github.com/qihaiyan/boot-multi-datasource">示例项目</a></p>

<h2 id="2建立实体类entity">2.建立实体类（Entity）</h2>

<p>首先，我们创建两个简单的实体类，分别属于两个不同的数据源，用于演示多数据源数据的保存和查询。</p>

<p>Test实体类：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">com.example.demo.test.data</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">javax.persistence.Entity</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">javax.persistence.Id</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">javax.persistence.Table</span><span class="o">;</span>

<span class="nd">@Entity</span>
<span class="nd">@Table</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"test"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Test</span> <span class="o">{</span>

    <span class="nd">@Id</span>
    <span class="kd">private</span> <span class="nc">Integer</span> <span class="n">id</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">Test</span><span class="o">(){</span>

    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">Integer</span> <span class="nf">getId</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">id</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">setId</span><span class="o">(</span><span class="nc">Integer</span> <span class="n">id</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="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Other实体类：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">com.example.demo.other.data</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">javax.persistence.Entity</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">javax.persistence.Id</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">javax.persistence.Table</span><span class="o">;</span>

<span class="nd">@Entity</span>
<span class="nd">@Table</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"other"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Other</span> <span class="o">{</span>

    <span class="nd">@Id</span>
    <span class="kd">private</span> <span class="nc">Integer</span> <span class="n">id</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nc">Integer</span> <span class="nf">getId</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">id</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">setId</span><span class="o">(</span><span class="nc">Integer</span> <span class="n">id</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="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>需要注意的是，这两个实体类分属于不同的package，这一点极为重要，spring会根据实体类所属的package来决定用那一个数据源进行操作。</p>

<!-- more -->

<h2 id="3建立repository">3.建立Repository</h2>

<p>分别建立两个实体类对应的Repository，用于进行数据操作。</p>

<p>TestRepository:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">com.example.demo.test.data</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">org.springframework.data.jpa.repository.JpaRepository</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">TestRepository</span> <span class="kd">extends</span> <span class="nc">JpaRepository</span><span class="o">&lt;</span><span class="nc">Test</span><span class="o">,</span> <span class="nc">Integer</span><span class="o">&gt;</span> <span class="o">{</span>
<span class="o">}</span>
</code></pre></div></div>

<p>OtherRepository:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">com.example.demo.other.data</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">org.springframework.data.jpa.repository.JpaRepository</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">OtherRepository</span> <span class="kd">extends</span> <span class="nc">JpaRepository</span><span class="o">&lt;</span><span class="nc">Other</span><span class="o">,</span> <span class="nc">Integer</span><span class="o">&gt;</span> <span class="o">{</span>
<span class="o">}</span>
</code></pre></div></div>

<p>得益于spring-data-jpa优秀的封装，我们只需创建一个接口，就拥有了对实体类的操作能力。</p>

<h2 id="3对多数据源进行配置">3.对多数据源进行配置</h2>

<p>分别对Test和Other两个实体类配置对应的数据源。配置的内容主要包含三个要素：</p>

<ol>
  <li>dataSource，数据源的连接信息</li>
  <li>entityManagerFactory，数据处理</li>
  <li>transactionManager，事务管理</li>
</ol>

<p>Test实体类的数据源配置 TestDataConfig：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">com.example.demo.config</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">org.springframework.beans.factory.annotation.Autowired</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.beans.factory.annotation.Qualifier</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.boot.autoconfigure.orm.jpa.JpaProperties</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.boot.context.properties.ConfigurationProperties</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.context.annotation.Bean</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.context.annotation.Configuration</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.context.annotation.Primary</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.data.jpa.repository.config.EnableJpaRepositories</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.orm.jpa.JpaTransactionManager</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.transaction.PlatformTransactionManager</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.transaction.annotation.EnableTransactionManagement</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">javax.persistence.EntityManagerFactory</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">javax.sql.DataSource</span><span class="o">;</span>

<span class="nd">@Configuration</span>
<span class="nd">@EnableTransactionManagement</span>
<span class="nd">@EnableJpaRepositories</span><span class="o">(</span>
        <span class="n">entityManagerFactoryRef</span> <span class="o">=</span> <span class="s">"entityManagerFactory"</span><span class="o">,</span>
        <span class="n">basePackages</span> <span class="o">=</span> <span class="o">{</span><span class="s">"com.example.demo.test.data"</span><span class="o">}</span>
<span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">TestDataConfig</span> <span class="o">{</span>

    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">JpaProperties</span> <span class="n">jpaProperties</span><span class="o">;</span>

    <span class="nd">@Primary</span>
    <span class="nd">@Bean</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"dataSource"</span><span class="o">)</span>
    <span class="nd">@ConfigurationProperties</span><span class="o">(</span><span class="n">prefix</span> <span class="o">=</span> <span class="s">"spring.datasource"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">DataSource</span> <span class="nf">dataSource</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">DataSourceBuilder</span><span class="o">.</span><span class="na">create</span><span class="o">().</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Primary</span>
    <span class="nd">@Bean</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"entityManagerFactory"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">LocalContainerEntityManagerFactoryBean</span> <span class="nf">entityManagerFactory</span><span class="o">(</span>
            <span class="nc">EntityManagerFactoryBuilder</span> <span class="n">builder</span><span class="o">,</span>
            <span class="nd">@Qualifier</span><span class="o">(</span><span class="s">"dataSource"</span><span class="o">)</span> <span class="nc">DataSource</span> <span class="n">dataSource</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">dataSource</span><span class="o">(</span><span class="n">dataSource</span><span class="o">)</span>
                <span class="o">.</span><span class="na">packages</span><span class="o">(</span><span class="s">"com.example.demo.test.data"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">properties</span><span class="o">(</span><span class="n">jpaProperties</span><span class="o">.</span><span class="na">getHibernateProperties</span><span class="o">(</span><span class="n">dataSource</span><span class="o">))</span>
                <span class="o">.</span><span class="na">persistenceUnit</span><span class="o">(</span><span class="s">"test"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Primary</span>
    <span class="nd">@Bean</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"transactionManager"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">PlatformTransactionManager</span> <span class="nf">transactionManager</span><span class="o">(</span>
            <span class="nd">@Qualifier</span><span class="o">(</span><span class="s">"entityManagerFactory"</span><span class="o">)</span> <span class="nc">EntityManagerFactory</span> <span class="n">entityManagerFactory</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">JpaTransactionManager</span><span class="o">(</span><span class="n">entityManagerFactory</span><span class="o">);</span>
    <span class="o">}</span>

<span class="o">}</span>

</code></pre></div></div>

<p>代码中的Primary注解表示这是默认数据源。</p>

<p>Other实体类的数据源配置 OtherDataConfig：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">com.example.demo.config</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">org.springframework.beans.factory.annotation.Autowired</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.beans.factory.annotation.Qualifier</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.boot.autoconfigure.orm.jpa.JpaProperties</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.boot.context.properties.ConfigurationProperties</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.context.annotation.Bean</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.context.annotation.Configuration</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.data.jpa.repository.config.EnableJpaRepositories</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.orm.jpa.JpaTransactionManager</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.transaction.PlatformTransactionManager</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.transaction.annotation.EnableTransactionManagement</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">javax.persistence.EntityManagerFactory</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">javax.sql.DataSource</span><span class="o">;</span>

<span class="nd">@Configuration</span>
<span class="nd">@EnableTransactionManagement</span>
<span class="nd">@EnableJpaRepositories</span><span class="o">(</span>
        <span class="n">entityManagerFactoryRef</span> <span class="o">=</span> <span class="s">"otherEntityManagerFactory"</span><span class="o">,</span>
        <span class="n">transactionManagerRef</span> <span class="o">=</span> <span class="s">"otherTransactionManager"</span><span class="o">,</span>
        <span class="n">basePackages</span> <span class="o">=</span> <span class="o">{</span><span class="s">"com.example.demo.other.data"</span><span class="o">}</span>
<span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">OtherDataConfig</span> <span class="o">{</span>

    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">JpaProperties</span> <span class="n">jpaProperties</span><span class="o">;</span>

    <span class="nd">@Bean</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"otherDataSource"</span><span class="o">)</span>
    <span class="nd">@ConfigurationProperties</span><span class="o">(</span><span class="n">prefix</span> <span class="o">=</span> <span class="s">"other.datasource"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">DataSource</span> <span class="nf">otherDataSource</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">DataSourceBuilder</span><span class="o">.</span><span class="na">create</span><span class="o">().</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Bean</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"otherEntityManagerFactory"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">LocalContainerEntityManagerFactoryBean</span> <span class="nf">otherEntityManagerFactory</span><span class="o">(</span>
            <span class="nc">EntityManagerFactoryBuilder</span> <span class="n">builder</span><span class="o">,</span>
            <span class="nd">@Qualifier</span><span class="o">(</span><span class="s">"otherDataSource"</span><span class="o">)</span> <span class="nc">DataSource</span> <span class="n">otherDataSource</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">dataSource</span><span class="o">(</span><span class="n">otherDataSource</span><span class="o">)</span>
                <span class="o">.</span><span class="na">packages</span><span class="o">(</span><span class="s">"com.example.demo.other.data"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">properties</span><span class="o">(</span><span class="n">jpaProperties</span><span class="o">.</span><span class="na">getHibernateProperties</span><span class="o">(</span><span class="n">otherDataSource</span><span class="o">))</span>
                <span class="o">.</span><span class="na">persistenceUnit</span><span class="o">(</span><span class="s">"other"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Bean</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"otherTransactionManager"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">PlatformTransactionManager</span> <span class="nf">otherTransactionManager</span><span class="o">(</span>
            <span class="nd">@Qualifier</span><span class="o">(</span><span class="s">"otherEntityManagerFactory"</span><span class="o">)</span> <span class="nc">EntityManagerFactory</span> <span class="n">otherEntityManagerFactory</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">JpaTransactionManager</span><span class="o">(</span><span class="n">otherEntityManagerFactory</span><span class="o">);</span>
    <span class="o">}</span>

<span class="o">}</span>

</code></pre></div></div>

<h2 id="3数据操作">3.数据操作</h2>

<p>我们创建一个Service类TestService来分别对两个数据源进行数据的操作。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">com.example.demo.service</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">com.example.demo.other.data.Other</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.example.demo.other.data.OtherRepository</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.example.demo.test.data.Test</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.example.demo.test.data.TestRepository</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.beans.factory.annotation.Autowired</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.beans.factory.annotation.Value</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.stereotype.Component</span><span class="o">;</span>

<span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">TestService</span> <span class="o">{</span>

    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">TestRepository</span> <span class="n">testRepository</span><span class="o">;</span>

    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">OtherRepository</span> <span class="n">otherRepository</span><span class="o">;</span>

    <span class="nd">@Value</span><span class="o">(</span><span class="s">"${name:World}"</span><span class="o">)</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">name</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getHelloMessage</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">Test</span> <span class="n">test</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Test</span><span class="o">();</span>
        <span class="n">test</span><span class="o">.</span><span class="na">setId</span><span class="o">(</span><span class="mi">1</span><span class="o">);</span>
        <span class="n">test</span> <span class="o">=</span> <span class="n">testRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">test</span><span class="o">);</span>

        <span class="nc">Other</span> <span class="n">other</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Other</span><span class="o">();</span>
        <span class="n">other</span><span class="o">.</span><span class="na">setId</span><span class="o">(</span><span class="mi">2</span><span class="o">);</span>
        <span class="n">other</span> <span class="o">=</span> <span class="n">otherRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">other</span><span class="o">);</span>

        <span class="k">return</span> <span class="s">"Hello "</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="s">" : test's value = "</span> <span class="o">+</span> <span class="n">test</span><span class="o">.</span><span class="na">getId</span><span class="o">()</span> <span class="o">+</span> <span class="s">" , other's value = "</span> <span class="o">+</span> <span class="n">other</span><span class="o">.</span><span class="na">getId</span><span class="o">();</span>

    <span class="o">}</span>

<span class="o">}</span>

</code></pre></div></div>

<p>对Test和Other分别进行数据插入和读取操作，程序运行后会打印出两个数据源各自的数据。
数据库采用的mysql，连接信息在application.yml进行配置。</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">datasource</span><span class="pi">:</span>
    <span class="na">url</span><span class="pi">:</span> <span class="s">jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&amp;useSSL=false</span>
    <span class="na">testWhileIdle</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">validationQuery</span><span class="pi">:</span> <span class="s">SELECT 1 from dual</span>
    <span class="na">username</span><span class="pi">:</span> <span class="s">test</span>
    <span class="na">password</span><span class="pi">:</span> <span class="m">11111111</span>
    <span class="na">driverClassName</span><span class="pi">:</span> <span class="s">com.mysql.jdbc.Driver</span>
  <span class="na">jpa</span><span class="pi">:</span>
    <span class="na">database</span><span class="pi">:</span> <span class="s">MYSQL</span>
    <span class="na">show-sql</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">hibernate</span><span class="pi">:</span>
      <span class="na">show-sql</span><span class="pi">:</span> <span class="no">true</span>
      <span class="na">ddl-auto</span><span class="pi">:</span> <span class="s">create</span>
      <span class="na">naming-strategy</span><span class="pi">:</span> <span class="s">org.hibernate.cfg.ImprovedNamingStrategy</span>
    <span class="na">properties</span><span class="pi">:</span>
      <span class="na">hibernate.dialect</span><span class="pi">:</span> <span class="s">org.hibernate.dialect.MySQL5Dialect</span>
<span class="na">other</span><span class="pi">:</span>
  <span class="na">datasource</span><span class="pi">:</span>
    <span class="na">url</span><span class="pi">:</span> <span class="s">jdbc:mysql://localhost:3306/other?characterEncoding=utf-8&amp;useSSL=false</span>
    <span class="na">testWhileIdle</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">validationQuery</span><span class="pi">:</span> <span class="s">SELECT </span><span class="m">1</span>
    <span class="na">username</span><span class="pi">:</span> <span class="s">other</span>
    <span class="na">password</span><span class="pi">:</span> <span class="m">11111111</span>
    <span class="na">driverClassName</span><span class="pi">:</span> <span class="s">com.mysql.jdbc.Driver</span>
  <span class="na">jpa</span><span class="pi">:</span>
    <span class="na">database</span><span class="pi">:</span> <span class="s">MYSQL</span>
    <span class="na">show-sql</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">hibernate</span><span class="pi">:</span>
      <span class="na">show-sql</span><span class="pi">:</span> <span class="no">true</span>
      <span class="na">ddl-auto</span><span class="pi">:</span> <span class="s">create</span>
      <span class="na">naming-strategy</span><span class="pi">:</span> <span class="s">org.hibernate.cfg.ImprovedNamingStrategy</span>
    <span class="na">properties</span><span class="pi">:</span>
      <span class="na">hibernate.dialect</span><span class="pi">:</span> <span class="s">org.hibernate.dialect.MySQL5Dialect</span>
</code></pre></div></div>

<p>Test实体对应的是主数据源，采用了spring-boot的默认数据源配置项，Other实体单独配置数据源连接。具体应该读取哪一段配置内容，是在配置类OtherDataConfig中这行代码指定的。</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">"other.datasource"</span><span class="o">)</span>
</code></pre></div></div>

<p>本示例需要建立的数据库用户和库可以通过以下命令处理：</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CREATE USER <span class="s1">'test'</span>@<span class="s1">'localhost'</span> IDENTIFIED BY <span class="s1">'11111111'</span><span class="p">;</span>
GRANT ALL PRIVILEGES ON <span class="k">*</span>.<span class="k">*</span> TO <span class="s1">'test'</span>@<span class="s1">'localhost'</span><span class="p">;</span>
CREATE USER <span class="s1">'other'</span>@<span class="s1">'localhost'</span> IDENTIFIED BY <span class="s1">'11111111'</span><span class="p">;</span>
GRANT ALL PRIVILEGES ON <span class="k">*</span>.<span class="k">*</span> TO <span class="s1">'other'</span>@<span class="s1">'localhost'</span><span class="p">;</span>
create database <span class="nb">test</span><span class="p">;</span>
create database other<span class="p">;</span>
</code></pre></div></div>

<h2 id="4总结">4.总结</h2>

<p>spring-data-jpa极大的简化了数据库操作，对于多数据源的支持，也只是需要增加一下配置文件和配置类而已。其中的关键内容有3点：</p>

<ol>
  <li>
    <p>配置文件中数据源的配置</p>
  </li>
  <li>
    <p>配置类的编写</p>
  </li>
  <li>
    <p>实体类所在的package必须与配置类中指定的package一致，如OtherDataConfig中指定的basePackages = {“com.example.demo.other.data”}</p>
  </li>
</ol>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><summary type="html"><![CDATA[1.概述]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/datasource.jpg" /><media:content medium="image" url="https://springcamp.cn/assets/images/datasource.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">用docker搭建elasticsearch集群</title><link href="https://springcamp.cn/docker-elasticsearch-cluster/" rel="alternate" type="text/html" title="用docker搭建elasticsearch集群" /><published>2017-09-08T13:00:00+00:00</published><updated>2017-09-08T13:00:00+00:00</updated><id>https://springcamp.cn/docker-elasticsearch-cluster</id><content type="html" xml:base="https://springcamp.cn/docker-elasticsearch-cluster/"><![CDATA[<h2 id="概述">概述</h2>

<p>用docker进行elasticsearch的部署非常简单，如果要实现集群配置，需要进行一些特殊的处理，本文介绍如何利用docker进行elasticsearch集群的搭建。</p>

<p>具体的配置可以参照该 <a href="https://github.com/qihaiyan/fluentd-boot">示例</a></p>

<h2 id="主节点配置">主节点配置</h2>

<h3 id="docker-composeyml配置文件">docker-compose.yml配置文件</h3>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">es</span><span class="pi">:</span>
  <span class="na">image</span><span class="pi">:</span> <span class="s">elasticsearch</span>
  <span class="na">volumes</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">./es:/usr/share/elasticsearch/data</span>
    <span class="pi">-</span> <span class="s">./elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml</span>
  <span class="na">ports</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">9200:9200</span>
    <span class="pi">-</span> <span class="s">9300:9300</span>
</code></pre></div></div>
<!-- more -->

<p>其中的<code class="language-plaintext highlighter-rouge">./es:/usr/share/elasticsearch/data</code>是将elasticsearch的数据文件挂在到本机的一个目录上，这儿指定的本机目录是./es，可以修改为其它有权限的目录。</p>

<h3 id="elasticsearchyml配置文件">elasticsearch.yml配置文件</h3>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">cluster.name</span><span class="pi">:</span> <span class="s">elasticsearch_cluster</span>
<span class="na">node.name</span><span class="pi">:</span> <span class="s">node-master</span>
<span class="na">node.master</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">node.data</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">http.port</span><span class="pi">:</span> <span class="m">9200</span>
<span class="na">network.host</span><span class="pi">:</span> <span class="s">0.0.0.0</span>
<span class="na">network.publish_host</span><span class="pi">:</span> <span class="s">master-ip</span>
<span class="na">discovery.zen.ping.unicast.hosts</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">master-ip"</span><span class="pi">]</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">network.publish_host: master-ip</code>指定了本机ip，需要将master-ip修改为真实的机器ip。<code class="language-plaintext highlighter-rouge">discovery.zen.ping.unicast.hosts</code>中的master-ip同样需要修改为真实的机器ip。</p>

<h3 id="启动服务">启动服务</h3>

<p>首先确认一下<code class="language-plaintext highlighter-rouge">/etc/sysctl.conf</code>配置文件中的<code class="language-plaintext highlighter-rouge">vm.max_map_count</code>是否大于655360，如果不是，或者配置文件中没有该配置，则用root用户将该配置修改为<code class="language-plaintext highlighter-rouge">vm.max_map_count=655360</code>，并执行命令<code class="language-plaintext highlighter-rouge">sysctl -p</code>否则启动时elasticsearch会报错。</p>

<p>执行<code class="language-plaintext highlighter-rouge">docker-compose up -d</code>，就可以正常启动了。</p>

<h2 id="数据节点配置">数据节点配置</h2>

<h3 id="docker-composeyml配置文件-1">docker-compose.yml配置文件</h3>

<p>与主节点的配置相同。</p>

<h3 id="elasticsearchyml配置文件-1">elasticsearch.yml配置文件</h3>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">cluster.name</span><span class="pi">:</span> <span class="s">elasticsearch_cluster</span>
<span class="na">node.name</span><span class="pi">:</span> <span class="s">node-data-1</span>
<span class="na">node.master</span><span class="pi">:</span> <span class="no">false</span>
<span class="na">node.data</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">http.port</span><span class="pi">:</span> <span class="m">9200</span>
<span class="na">network.host</span><span class="pi">:</span> <span class="s">0.0.0.0</span>
<span class="na">network.publish_host</span><span class="pi">:</span> <span class="s">data-ip</span>
<span class="na">discovery.zen.ping.unicast.hosts</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">master-ip"</span><span class="pi">]</span>
</code></pre></div></div>

<p>与主节点配置的区别在于以下几点：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>node.name: node-data-1
node.master: false
network.publish_host: data-ip
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">node.name</code>是数据节点的名字，<code class="language-plaintext highlighter-rouge">node.master</code>要设置为false，<code class="language-plaintext highlighter-rouge">network.publish_host</code>设置为数据节点的机器ip。</p>

<h3 id="启动服务-1">启动服务</h3>

<p>启动步骤同主节点。</p>

<p>主节点和数据节点都启动完成后，在主节点服务器上执行 <code class="language-plaintext highlighter-rouge">curl http://master-ip:9200/_cat/nodes</code> 命令（将命令中的master-ip修改为主节点的机器ip）可以看到集群中节点的状态。</p>]]></content><author><name>qihaiyan</name></author><category term="docker" /><category term="docker" /><category term="elasticsearch" /><summary type="html"><![CDATA[概述]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/elasticsearch.png" /><media:content medium="image" url="https://springcamp.cn/assets/images/elasticsearch.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">程序常驻后台运行的原理和方法</title><link href="https://springcamp.cn/sighup-term/" rel="alternate" type="text/html" title="程序常驻后台运行的原理和方法" /><published>2017-06-11T13:28:00+00:00</published><updated>2017-06-11T13:28:00+00:00</updated><id>https://springcamp.cn/sighup-term</id><content type="html" xml:base="https://springcamp.cn/sighup-term/"><![CDATA[<p>linux中进程组织结构为session包含一个前台进程组及一个或多个后台进程组，一个进程组包含多个进程。</p>

<p>一个session可能会有一个session首进程，而一个session首进程可能会有一个控制终端。一个进程组可能会有一个进程组首进程。(
这儿是可能会有，在一定情况之下是没有的)</p>

<p>进程组首进程的PID与该进程组ID相等。</p>

<p>与终端交互的进程是前台进程，否则便是后台进程。</p>

<p>SIGHUP会在以下3种情况下被发送给相应的进程：</p>

<p>1、终端关闭时，该信号被发送到首进程以及作为job提交的进程（即用 &amp; 符号提交的进程）</p>

<p>2、session首进程退出时，该信号被发送到该session中的前台进程组中的每一个进程</p>

<p>3、若父进程退出导致进程组成为孤儿进程组，且该进程组中有进程处于停止状态（收到SIGSTOP或SIGTSTP信号），该信号会被发送到该进程组中的每一个进程。</p>

<!-- more -->

<p>系统对信号的默认处理是终止收到该信号的进程。</p>

<p>所以若程序中没有捕捉该信号，当收到该信号时，进程就会退出。</p>

<p>下面观察几种因终端关闭导致进程退出的情况，在这儿进程退出是因为收到了SIGHUP信号。</p>

<p>login shell是session首进程。</p>

<p>首先写一个测试程序，代码如下：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;signal.h&gt;</span><span class="cp">
</span>
<span class="kt">char</span> <span class="o">**</span><span class="n">args</span><span class="p">;</span>

<span class="kt">void</span> <span class="nf">exithandle</span><span class="p">(</span><span class="kt">int</span> <span class="n">sig</span><span class="p">)</span>
<span class="p">{</span>
       <span class="n">printf</span><span class="p">(</span><span class="s">"%s : sighupreceived</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="n">args</span><span class="p">[</span><span class="mi">1</span><span class="p">]);</span>
<span class="p">}</span>

<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span><span class="kt">char</span> <span class="o">**</span><span class="n">argv</span><span class="p">)</span>
<span class="p">{</span>
       <span class="n">args</span><span class="o">=</span><span class="n">argv</span><span class="p">;</span>
       <span class="n">signal</span><span class="p">(</span><span class="n">SIGHUP</span><span class="p">,</span><span class="n">exithandle</span><span class="p">);</span>
       <span class="n">pause</span><span class="p">();</span>
       <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>程序中捕捉SIGHUP信号后打印一条信息，pause()使程序暂停。</p>

<p>编译后的执行文件为sigtest</p>

<p>1、命令： sigtest front &gt; tt.txt</p>

<p>操作： 关闭终端</p>

<p>结果： tt文件的内容为front: sighup received</p>

<p>原因：</p>

<p>sigtest是前台进程，终端关闭后，根据上面提到的第1种情况， loginshell作为session首进程，会收到SIGHUP信号然后退出，</p>

<p>根据第2种情况，sigtest作为前台进程，会收到login shell发出的SIGHUP信号。</p>

<p>2、命令：</p>

<p>sigtest back &gt; tt.txt &amp;</p>

<p>操作：</p>

<p>关闭终端</p>

<p>结果：</p>

<p>tt文件的内容为back: sighup received</p>

<p>原因：</p>

<p>sigtest是提交的job，根据上面提到的第1种情况，sigtest会收到SIGHUP信号</p>

<p>3、写一个shell，内容为</p>

<p>sigtest back &gt; tt.txt &amp;</p>

<p>执行该shell</p>

<p>操作： 关闭终端</p>

<p>结果： 执行<code class="language-plaintext highlighter-rouge">ps -ef | grep sigtest</code>命令，会看到该进程还在，tt文件为空</p>

<p>原因：</p>

<p>执行该shell时，sigtest作为job提交，然后该shell退出，致使sigtest变成了孤儿进程，不再是当前session的job，</p>

<p>因此sigtest即不是session首进程也不是job，不会收到SIGHUP。</p>

<p>同时孤儿进程属于后台进程，因此loginshell退出后不会发送SIGHUP给sigtest，因为它只将该信号发送给前台进程。</p>

<p>第3条说过若进程组变成孤儿进程组的时候，若有进程处于停止状态，也会收到SIGHUP信号，但sigtest没有处于停止状态，所以不会收到SIGHUP信号。</p>

<p>4、nohup sigtest &gt; tt</p>

<p>操作：</p>

<p>关闭终端</p>

<p>结果： tt文件为空</p>

<p>原因：nohup可以防止进程收到SIGHUP信号</p>

<p>至此，我们就清楚了何种情况下终端关闭后进程会退出，何种情况下不会退出。</p>

<p>要想终端关闭后进程不退出有以下几种方法，均为通过shell的方式：</p>

<p>1、 编写shell，内容如下</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">trap</span> <span class="s2">""</span> SIGHUP  <span class="c">#该句的作用是屏蔽SIGHUP信号，trap可以屏蔽很多信号</span>
sigtest
</code></pre></div></div>

<p>2、nohup sigtest可以直接在命令行执行，</p>

<p>若想做完该操作后继续别的操作，可以执行</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">nohup </span>sigtest &amp;
</code></pre></div></div>

<p>3、 编写shell，内容如下</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sigtest &amp;
</code></pre></div></div>

<p>总结：</p>

<p>可以采用任何将进程变为孤儿进程的方式，让程序常驻后台运行，包括fork后父进程马上退出，这是C语言中实现daemon程序的常用技巧。</p>]]></content><author><name>qihaiyan</name></author><category term="linux" /><category term="linux" /><summary type="html"><![CDATA[linux中进程组织结构为session包含一个前台进程组及一个或多个后台进程组，一个进程组包含多个进程。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/sighup.png" /><media:content medium="image" url="https://springcamp.cn/assets/images/sighup.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">用JWT技术为SpringBoot的API增加授权保护</title><link href="https://springcamp.cn/secure-spring-boot-APIs-with-JWT/" rel="alternate" type="text/html" title="用JWT技术为SpringBoot的API增加授权保护" /><published>2017-06-11T13:28:00+00:00</published><updated>2017-06-11T13:28:00+00:00</updated><id>https://springcamp.cn/secure-spring-boot-APIs-with-JWT</id><content type="html" xml:base="https://springcamp.cn/secure-spring-boot-APIs-with-JWT/"><![CDATA[<h3 id="概述">概述</h3>

<p><a href="https://github.com/qihaiyan/jwt-boot-auth">示例</a> https://github.com/qihaiyan/jwt-boot-auth</p>

<p>用spring-boot开发RESTful API非常的方便，在生产环境中，对发布的API增加授权保护是非常必要的。现在我们来看如何利用JWT技术为API增加授权保护，保证只有获得授权的用户才能够访问API。</p>

<h3 id="开发一个简单的api">开发一个简单的API</h3>

<p>spring提供了一个<a href="https://start.spring.io/">网页</a>可以便捷的生成springboot程序。</p>

<p>如图：在Search for dependencies中选择H2、Web、Security、JPA，这几个依赖在我们的示例工程中会用到。</p>

<!-- more -->

<p><img src="/images/spring-boot-starter-jwt.png" alt="spring-boot-starter-jwt" /></p>

<p>点击Generate Project按钮后，下载文件到本地。</p>

<p>在JwtauthApplication.java中增加一个方法：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">"/hello"</span><span class="o">)</span>
<span class="nd">@ResponseBody</span>
<span class="kd">public</span> <span class="nc">String</span> <span class="nf">hello</span><span class="o">(){</span>
  <span class="k">return</span> <span class="s">"hello"</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<p>这样一个简单的RESTful API就开发好了。</p>

<p>现在我们运行一下程序看看效果，打开命令行工具，执行：</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>jwtauth
gradle bootRun
</code></pre></div></div>

<p>等待程序启动完成后，可以简单的通过curl工具进行API的调用：</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl http://localhost:8080/tasks
</code></pre></div></div>

<p>至此，我们的接口就开发完成了。但是这个接口没有任何授权防护，任何人都可以访问，这样是不安全的，下面我们开始加入授权机制。</p>

<h3 id="增加用户注册功能">增加用户注册功能</h3>

<p>首先增加一个实体类MyUser：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">com.example.jwtauth</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">javax.persistence.Entity</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">javax.persistence.GeneratedValue</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">javax.persistence.GenerationType</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">javax.persistence.Id</span><span class="o">;</span>

<span class="nd">@Entity</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">MyUser</span> <span class="o">{</span>
    <span class="nd">@Id</span>
    <span class="nd">@GeneratedValue</span><span class="o">(</span><span class="n">strategy</span> <span class="o">=</span> <span class="nc">GenerationType</span><span class="o">.</span><span class="na">IDENTITY</span><span class="o">)</span>
    <span class="kd">private</span> <span class="kt">long</span> <span class="n">id</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">username</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">password</span><span class="o">;</span>

    <span class="kd">public</span> <span class="kt">long</span> <span class="nf">getId</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">id</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getUsername</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">username</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">setUsername</span><span class="o">(</span><span class="nc">String</span> <span class="n">username</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">username</span> <span class="o">=</span> <span class="n">username</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getPassword</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">password</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">setPassword</span><span class="o">(</span><span class="nc">String</span> <span class="n">password</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">password</span> <span class="o">=</span> <span class="n">password</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>然后增加一个Repository类MyUserRepository，可以读取和保存用户信息：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">com.example.jwtauth</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">org.springframework.data.jpa.repository.JpaRepository</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">MyUserRepository</span> <span class="kd">extends</span> <span class="nc">JpaRepository</span><span class="o">&lt;</span><span class="nc">MyUser</span><span class="o">,</span> <span class="nc">Long</span><span class="o">&gt;</span> <span class="o">{</span>
    <span class="nc">MyUser</span> <span class="nf">findByUsername</span><span class="o">(</span><span class="nc">String</span> <span class="n">username</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>得益于SpringDataJpa，只需要定义一个interface，就让我们拥有了数据的CRUD功能。由于我们在build.gradle中引入了H2，所以我们拥有了一个本地数据库，不需要做任何配置，springboot就会使用这个数据库，不得不说springboot确实极大的减轻了开发工作量。</p>

<p>下面增加一个类UserController，实现用户注册的接口：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">com.example.jwtauth</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.web.bind.annotation.PostMapping</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.web.bind.annotation.RequestBody</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.web.bind.annotation.RequestMapping</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.web.bind.annotation.RestController</span><span class="o">;</span>

<span class="nd">@RestController</span>
<span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">"/users"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserController</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="nc">MyUserRepository</span> <span class="n">applicationUserRepository</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">BCryptPasswordEncoder</span> <span class="n">bCryptPasswordEncoder</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">UserController</span><span class="o">(</span><span class="nc">MyUserRepository</span> <span class="n">myUserRepository</span><span class="o">,</span>
                          <span class="nc">BCryptPasswordEncoder</span> <span class="n">bCryptPasswordEncoder</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">applicationUserRepository</span> <span class="o">=</span> <span class="n">myUserRepository</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">bCryptPasswordEncoder</span> <span class="o">=</span> <span class="n">bCryptPasswordEncoder</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@PostMapping</span><span class="o">(</span><span class="s">"/signup"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">signUp</span><span class="o">(</span><span class="nd">@RequestBody</span> <span class="nc">MyUser</span> <span class="n">user</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">user</span><span class="o">.</span><span class="na">setPassword</span><span class="o">(</span><span class="n">bCryptPasswordEncoder</span><span class="o">.</span><span class="na">encode</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getPassword</span><span class="o">()));</span>
        <span class="n">applicationUserRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">user</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="nd">@PostMapping</span><span class="o">(</span><span class="s">"/signup"</span><span class="o">)</span>
</code></pre></div></div>

<p>这个方法定义了用户注册接口，并且指定了url地址是/users/signup。由于类上加了注解 @RequestMapping(“/users”)，类中的所有方法的url地址都会有/users前缀，所以在方法上只需指定/signup子路径即可。</p>

<p>密码采用了BCryptPasswordEncoder进行加密，我们在Application中增加BCryptPasswordEncoder实例的定义。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@SpringBootApplication</span>
<span class="nd">@RestController</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">JwtauthApplication</span> <span class="o">{</span>
	<span class="nd">@Bean</span>
	<span class="kd">public</span> <span class="nc">BCryptPasswordEncoder</span> <span class="nf">bCryptPasswordEncoder</span><span class="o">()</span> <span class="o">{</span>
		<span class="k">return</span> <span class="k">new</span> <span class="nf">BCryptPasswordEncoder</span><span class="o">();</span>
	<span class="o">}</span>
  <span class="c1">// ...</span>
</code></pre></div></div>

<h3 id="增加jwt认证功能">增加JWT认证功能</h3>

<p>用户填入用户名密码后，与数据库里存储的用户信息进行比对，如果通过，则认证成功。传统的方法是在认证通过后，创建sesstion，并给客户端返回cookie。现在我们采用JWT来处理用户名密码的认证。区别在于，认证通过后，服务器生成一个token，将token返回给客户端，客户端以后的所有请求都需要在http头中指定该token。服务器接收的请求后，会对token的合法性进行验证。验证的内容包括：</p>

<ol>
  <li>
    <p>内容是一个正确的JWT格式</p>
  </li>
  <li>
    <p>检查签名</p>
  </li>
  <li>
    <p>检查claims</p>
  </li>
  <li>
    <p>检查权限</p>
  </li>
</ol>

<h4 id="处理登录">处理登录</h4>

<p>创建一个类JWTLoginFilter，核心功能是在验证用户名密码正确后，生成一个token，并将token返回给客户端：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">com.example.jwtauth</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">com.fasterxml.jackson.databind.ObjectMapper</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">io.jsonwebtoken.Jwts</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">io.jsonwebtoken.SignatureAlgorithm</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.security.authentication.AuthenticationManager</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.security.authentication.UsernamePasswordAuthenticationToken</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.security.core.Authentication</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.security.core.AuthenticationException</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.security.core.userdetails.User</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">javax.servlet.FilterChain</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">javax.servlet.ServletException</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">javax.servlet.http.HttpServletRequest</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">javax.servlet.http.HttpServletResponse</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.io.IOException</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.util.ArrayList</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.util.Date</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">JWTLoginFilter</span> <span class="kd">extends</span> <span class="nc">UsernamePasswordAuthenticationFilter</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="nc">AuthenticationManager</span> <span class="n">authenticationManager</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">JWTLoginFilter</span><span class="o">(</span><span class="nc">AuthenticationManager</span> <span class="n">authenticationManager</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">authenticationManager</span> <span class="o">=</span> <span class="n">authenticationManager</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">Authentication</span> <span class="nf">attemptAuthentication</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">req</span><span class="o">,</span>
                                                <span class="nc">HttpServletResponse</span> <span class="n">res</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">AuthenticationException</span> <span class="o">{</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="nc">MyUser</span> <span class="n">user</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ObjectMapper</span><span class="o">()</span>
                    <span class="o">.</span><span class="na">readValue</span><span class="o">(</span><span class="n">req</span><span class="o">.</span><span class="na">getInputStream</span><span class="o">(),</span> <span class="nc">MyUser</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>

            <span class="k">return</span> <span class="n">authenticationManager</span><span class="o">.</span><span class="na">authenticate</span><span class="o">(</span>
                    <span class="k">new</span> <span class="nf">UsernamePasswordAuthenticationToken</span><span class="o">(</span>
                            <span class="n">user</span><span class="o">.</span><span class="na">getUsername</span><span class="o">(),</span>
                            <span class="n">user</span><span class="o">.</span><span class="na">getPassword</span><span class="o">(),</span>
                            <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;())</span>
            <span class="o">);</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">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">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="nd">@Override</span>
    <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">successfulAuthentication</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">req</span><span class="o">,</span>
                                            <span class="nc">HttpServletResponse</span> <span class="n">res</span><span class="o">,</span>
                                            <span class="nc">FilterChain</span> <span class="n">chain</span><span class="o">,</span>
                                            <span class="nc">Authentication</span> <span class="n">auth</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">IOException</span><span class="o">,</span> <span class="nc">ServletException</span> <span class="o">{</span>

        <span class="nc">String</span> <span class="n">token</span> <span class="o">=</span> <span class="nc">Jwts</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
                <span class="o">.</span><span class="na">setSubject</span><span class="o">(((</span><span class="nc">User</span><span class="o">)</span> <span class="n">auth</span><span class="o">.</span><span class="na">getPrincipal</span><span class="o">()).</span><span class="na">getUsername</span><span class="o">())</span>
                <span class="o">.</span><span class="na">setExpiration</span><span class="o">(</span><span class="k">new</span> <span class="nc">Date</span><span class="o">(</span><span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">()</span> <span class="o">+</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">24</span> <span class="o">*</span> <span class="mi">1000</span><span class="o">))</span>
                <span class="o">.</span><span class="na">signWith</span><span class="o">(</span><span class="nc">SignatureAlgorithm</span><span class="o">.</span><span class="na">HS512</span><span class="o">,</span> <span class="s">"MyJwtSecret"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">compact</span><span class="o">();</span>
        <span class="n">res</span><span class="o">.</span><span class="na">addHeader</span><span class="o">(</span><span class="s">"Authorization"</span><span class="o">,</span> <span class="s">"Bearer "</span> <span class="o">+</span> <span class="n">token</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>该类继承自UsernamePasswordAuthenticationFilter，重写了其中的2个方法：</p>

<p><code class="language-plaintext highlighter-rouge">attemptAuthentication</code> ：接收并解析用户凭证。</p>

<p><code class="language-plaintext highlighter-rouge">successfulAuthentication</code> ：用户成功登录后，这个方法会被调用，我们在这个方法里生成token。</p>

<h4 id="授权验证">授权验证</h4>

<p>用户一旦登录成功后，会拿到token，后续的请求都会带着这个token，服务端会验证token的合法性。</p>

<p>创建JwtAuthenticationFilter类，我们在这个类中实现token的校验功能。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">com.example.jwtauth</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">io.jsonwebtoken.Jwts</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.security.authentication.AuthenticationManager</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.security.authentication.UsernamePasswordAuthenticationToken</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.security.core.context.SecurityContextHolder</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.security.web.authentication.www.BasicAuthenticationFilter</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">javax.servlet.FilterChain</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">javax.servlet.ServletException</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">javax.servlet.http.HttpServletRequest</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">javax.servlet.http.HttpServletResponse</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.io.IOException</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.util.ArrayList</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">JwtAuthenticationFilter</span> <span class="kd">extends</span> <span class="nc">BasicAuthenticationFilter</span> <span class="o">{</span>
    <span class="kd">public</span> <span class="nf">JwtAuthenticationFilter</span><span class="o">(</span><span class="nc">AuthenticationManager</span> <span class="n">authManager</span><span class="o">)</span> <span class="o">{</span>
        <span class="kd">super</span><span class="o">(</span><span class="n">authManager</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">doFilterInternal</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">req</span><span class="o">,</span>
                                    <span class="nc">HttpServletResponse</span> <span class="n">res</span><span class="o">,</span>
                                    <span class="nc">FilterChain</span> <span class="n">chain</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">IOException</span><span class="o">,</span> <span class="nc">ServletException</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">header</span> <span class="o">=</span> <span class="n">req</span><span class="o">.</span><span class="na">getHeader</span><span class="o">(</span><span class="s">"Authorization"</span><span class="o">);</span>

        <span class="k">if</span> <span class="o">(</span><span class="n">header</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">||</span> <span class="o">!</span><span class="n">header</span><span class="o">.</span><span class="na">startsWith</span><span class="o">(</span><span class="s">"Bearer "</span><span class="o">))</span> <span class="o">{</span>
            <span class="n">chain</span><span class="o">.</span><span class="na">doFilter</span><span class="o">(</span><span class="n">req</span><span class="o">,</span> <span class="n">res</span><span class="o">);</span>
            <span class="k">return</span><span class="o">;</span>
        <span class="o">}</span>

        <span class="nc">UsernamePasswordAuthenticationToken</span> <span class="n">authentication</span> <span class="o">=</span> <span class="n">getAuthentication</span><span class="o">(</span><span class="n">req</span><span class="o">);</span>

        <span class="nc">SecurityContextHolder</span><span class="o">.</span><span class="na">getContext</span><span class="o">().</span><span class="na">setAuthentication</span><span class="o">(</span><span class="n">authentication</span><span class="o">);</span>
        <span class="n">chain</span><span class="o">.</span><span class="na">doFilter</span><span class="o">(</span><span class="n">req</span><span class="o">,</span> <span class="n">res</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">UsernamePasswordAuthenticationToken</span> <span class="nf">getAuthentication</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">token</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="s">"Authorization"</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">token</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="c1">// parse the token.</span>
            <span class="nc">String</span> <span class="n">user</span> <span class="o">=</span> <span class="nc">Jwts</span><span class="o">.</span><span class="na">parser</span><span class="o">()</span>
                    <span class="o">.</span><span class="na">setSigningKey</span><span class="o">(</span><span class="s">"MyJwtSecret"</span><span class="o">)</span>
                    <span class="o">.</span><span class="na">parseClaimsJws</span><span class="o">(</span><span class="n">token</span><span class="o">.</span><span class="na">replace</span><span class="o">(</span><span class="s">"Bearer "</span><span class="o">,</span> <span class="s">""</span><span class="o">))</span>
                    <span class="o">.</span><span class="na">getBody</span><span class="o">()</span>
                    <span class="o">.</span><span class="na">getSubject</span><span class="o">();</span>

            <span class="k">if</span> <span class="o">(</span><span class="n">user</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="k">new</span> <span class="nf">UsernamePasswordAuthenticationToken</span><span class="o">(</span><span class="n">user</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;());</span>
            <span class="o">}</span>
            <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
        <span class="o">}</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>该类继承自BasicAuthenticationFilter，在doFilterInternal方法中，从http头的<code class="language-plaintext highlighter-rouge">Authorization</code> 项读取token数据，然后用Jwts包提供的方法校验token的合法性。如果校验通过，就认为这是一个取得授权的合法请求。</p>

<h4 id="springsecurity配置">SpringSecurity配置</h4>

<p>通过SpringSecurity的配置，将上面的方法组合在一起。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">com.example.jwtauth</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">org.springframework.boot.autoconfigure.security.SecurityProperties</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.context.annotation.Configuration</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.core.annotation.Order</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.http.HttpMethod</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.security.config.annotation.web.builders.HttpSecurity</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.security.config.annotation.web.configuration.EnableWebSecurity</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.security.core.userdetails.UserDetailsService</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder</span><span class="o">;</span>

<span class="nd">@Configuration</span>
<span class="nd">@Order</span><span class="o">(</span><span class="nc">SecurityProperties</span><span class="o">.</span><span class="na">ACCESS_OVERRIDE_ORDER</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">MyWebSecurityConfig</span> <span class="kd">extends</span> <span class="nc">WebSecurityConfigurerAdapter</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="nc">UserDetailsService</span> <span class="n">userDetailsService</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">BCryptPasswordEncoder</span> <span class="n">bCryptPasswordEncoder</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">MyWebSecurityConfig</span><span class="o">(</span><span class="nc">UserDetailsService</span> <span class="n">userDetailsService</span><span class="o">,</span> <span class="nc">BCryptPasswordEncoder</span> <span class="n">bCryptPasswordEncoder</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">userDetailsService</span> <span class="o">=</span> <span class="n">userDetailsService</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">bCryptPasswordEncoder</span> <span class="o">=</span> <span class="n">bCryptPasswordEncoder</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">configure</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="n">http</span><span class="o">.</span><span class="na">cors</span><span class="o">().</span><span class="na">and</span><span class="o">().</span><span class="na">csrf</span><span class="o">().</span><span class="na">disable</span><span class="o">().</span><span class="na">authorizeRequests</span><span class="o">()</span>
                <span class="o">.</span><span class="na">antMatchers</span><span class="o">(</span><span class="nc">HttpMethod</span><span class="o">.</span><span class="na">POST</span><span class="o">,</span> <span class="s">"/users/signup"</span><span class="o">).</span><span class="na">permitAll</span><span class="o">()</span>
                <span class="o">.</span><span class="na">anyRequest</span><span class="o">().</span><span class="na">authenticated</span><span class="o">()</span>
                <span class="o">.</span><span class="na">and</span><span class="o">()</span>
                <span class="o">.</span><span class="na">addFilter</span><span class="o">(</span><span class="k">new</span> <span class="nc">JWTLoginFilter</span><span class="o">(</span><span class="n">authenticationManager</span><span class="o">()))</span>
                <span class="o">.</span><span class="na">addFilter</span><span class="o">(</span><span class="k">new</span> <span class="nc">JwtAuthenticationFilter</span><span class="o">(</span><span class="n">authenticationManager</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">configure</span><span class="o">(</span><span class="nc">AuthenticationManagerBuilder</span> <span class="n">auth</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="n">auth</span><span class="o">.</span><span class="na">userDetailsService</span><span class="o">(</span><span class="n">userDetailsService</span><span class="o">).</span><span class="na">passwordEncoder</span><span class="o">(</span><span class="n">bCryptPasswordEncoder</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>这是标准的SpringSecurity配置内容，就不在详细说明。注意其中的</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.addFilter(new JWTLoginFilter(authenticationManager()))
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
</code></pre></div></div>

<p>这两行，将我们定义的JWT方法加入SpringSecurity的处理流程中。</p>

<p>下面对我们的程序进行简单的验证：</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 请求hello接口，会收到403错误</span>
curl http://localhost:8080/hello

<span class="c"># 注册一个新用户</span>
curl <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="nt">-X</span> POST <span class="nt">-d</span> <span class="s1">'{
    "username": "admin",
    "password": "password"
}'</span> http://localhost:8080/users/signup

<span class="c"># 登录，会返回token，在http header中，Authorization: Bearer 后面的部分就是token</span>
curl <span class="nt">-i</span> <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="nt">-X</span> POST <span class="nt">-d</span> <span class="s1">'{
    "username": "admin",
    "password": "password"
}'</span> http://localhost:8080/login

<span class="c"># 用登录成功后拿到的token再次请求hello接口</span>
<span class="c"># 将请求中的XXXXXX替换成拿到的token</span>
<span class="c"># 这次可以成功调用接口了</span>
curl <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="se">\</span>
<span class="nt">-H</span> <span class="s2">"Authorization: Bearer XXXXXX"</span> <span class="se">\</span>
<span class="s2">"http://localhost:8080/hello"</span>

</code></pre></div></div>

<h3 id="总结">总结</h3>

<p>至此，给SpringBoot的接口加上JWT认证的功能就实现了，过程并不复杂，主要是开发两个SpringSecurity的filter，来生成和校验JWT token。</p>

<p>JWT作为一个无状态的认证授权技术，非常适合于分布式系统架构，因为服务端不需要保存用户状态，因此就无需采用redis等技术，在各个服务节点之间共享session数据。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><category term="jwt" /><summary type="html"><![CDATA[概述]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://springcamp.cn/assets/images/jwt.png" /><media:content medium="image" url="https://springcamp.cn/assets/images/jwt.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">OAuth 2.0 教程</title><link href="https://springcamp.cn/oauth2-tutorial/" rel="alternate" type="text/html" title="OAuth 2.0 教程" /><published>2017-06-11T12:22:00+00:00</published><updated>2017-06-11T12:22:00+00:00</updated><id>https://springcamp.cn/oauth2-tutorial</id><content type="html" xml:base="https://springcamp.cn/oauth2-tutorial/"><![CDATA[<p>（原文：/oauth2/index.html）</p>

<p><a href="https://github.com/qihaiyan/ng-boot-oauth">demo: https://github.com/qihaiyan/ng-boot-oauth</a></p>

<h2 id="oauth-20-教程">OAuth 2.0 教程</h2>

<p>OAuth 2.0 是一个开放的标准协议，允许应用程序访问其它应用的用户授权的数据。例如：一个游戏可以获取Facebook中的用户信息，或者是一个地理位置程序可以获取Foursquare的用户信息等。
这儿是一个示例图：</p>

<p><img src="/images/oauth2-intro.png" alt="oauth2 introduce" /></p>

<p>首先用户进入游戏的web应用，该应用要求用户通过Facebook账户登录，并定向到Facebook的登录界面，用户登录Facebook后，会重定向到之前的游戏应用。此时该应用就获取到了用户在Facebook的用户数据以及授权信息。</p>

<h3 id="oauth-20-用例">OAuth 2.0 用例</h3>

<!-- more -->

<p>OAuth 2.0既可以用于在某个应用内访问其它应用的用户信息，又可以提供用户授权服务供其它应用调用。
OAuth 2.0是OAuth 1.0的替代，因为OAuth 1.0太复杂了，比如OAuth 1.0要求使用证书等。OAuth 2.0更加简单，不要求使用证书，仅使用SSL/TLS。</p>

<h3 id="oauth-20-规范">OAuth 2.0 规范</h3>

<p>这个教程的目的是提供一个OAuth 2.0协议的概览以帮助理解，而不是涵盖此协议的所有细节。
如果你计划实现一个OAuth 2.0协议，最好是去阅读规范的详细内容，规范的地址：<a href="http://tools.ietf.org/html/draft-ietf-oauth-v2-23">http://tools.ietf.org/html/draft-ietf-oauth-v2-23</a></p>

<h2 id="oauth-20-概述">OAuth 2.0 概述</h2>

<p>在之前的介绍中我们提到，OAuth 2.0是一个开放标准，其允许应用程序访问其它应用的用户授权的数据。现在我们来介绍这个协议是如何工作的，以及规范中提到的各种概念。
OAuth 2.0	提供了不同的方式去获取权限用语访问资源服务器中的资源。现在介绍其中最安全和最常用的使用方式：一个web应用如何请求访问另一个web应用的访问权限。
下面的示例图描述了整个处理过程：
<img src="/images/oauth2/overview-1.png" alt="Alt text" />
首先用户访问客户端应用，在这个应用中会有一个“通过Facebook登录”的按钮。
第二步，当用户点击这个按钮时，用户被重定向到认证服务器（Facebook）。然后用户开始登录，登录成功后会被询问客户端应用是否可以使用他的用户信息。用户点击确认按钮。
第三步，认证服务器将用户重定向到客户端应用提供的URL。这个重定向URL一般会在认证服务器中进行注册，注册是由客户端应用的所有者进行的。注册完成后，认证服务器会生成一个client id和client password。重定向后的URL会有一个code参数，该参数是此次认证的一个标识。
第四步，重定向完成后，用户会进入重定向后的页面，同时客户端应用会在后台与认证服务器进行通讯，发送client id,client password和上一步获取到的code参数到认证服务器，认证服务器返回access token给客户端应用。
一旦客户端应用拿到了access token，就可以用这个token去访问Facebook提供的相关资源。</p>

<h2 id="oauth-20-应用角色">OAuth 2.0 应用角色</h2>

<p>OAuth 2.0定义了以下应用角色：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Resource Owner （资源所有者）
Resource Server （资源服务器）
Client Application （客户端应用）
Authorization Server （认证服务器） ![Alt text](/images/oauth2/overview-roles.png) Resrouce Owner（资源所有者）是数据的所有者。例如：Facebook或Google上的一个用户就是一个Resrouce Owner。他们所拥有的资源就是用户数据。示例图中的那个用户的图标代表的就是Resrouce Owner。Resrouce Owner也可以是一个应用程序。
</code></pre></div></div>

<p>Resource Server（资源服务器）是存放资源的服务，例如Facebook或Google就是Resource Server。</p>

<p>Client Application（客户端应用）会请求访问存放在资源服务器上的资源，这些资源是属于Resource Owner（资源所有者）的。</p>

<p>Authorization Server（认证服务器）对Client Application（客户端应用）进行授权，授权通过后客户端应用才可以访问资源服务器上的资源。认证服务器和资源服务器可以是同一个应用，也可以分开独立部署。</p>
<h2 id="oauth-20-客户端类型">OAuth 2.0 客户端类型</h2>
<p>OAuth 2.0规范定义了2种客户端类型：</p>

<ul>
  <li>私密型</li>
  <li>公开型</li>
</ul>

<p>私密型客户端会保存client password。认证服务器会给每一个客户端应用生成一个client password，认证服务器通过该client password来识别该客户端应用是一个注册过的应用，而不是其它的欺诈程序。一个web应用可以是私密性客户端，只有系统管理员可以登录这个应用的服务器和查看client password。</p>

<p>公开型客户端不会保存client password。例如移动APP或桌面程序，如果client password被保存在此类应用中，就可以通过破解手段拿到client password，这是非常不安全的。</p>

<h3 id="客户端应用表现形式">客户端应用表现形式</h3>

<ul>
  <li>Web Application （web应用）</li>
  <li>User Agent （富web客户端）</li>
  <li>Native （原生应用）</li>
</ul>

<h4 id="web-applicationweb应用">Web Application（web应用）</h4>

<p>web程序运行在web服务器上。web应用做认证时用到的client password是保存在服务器上的，因此是私密的。下面是一个web应用的示例图：</p>

<p><img src="/images/oauth2/overview-client-types-1.png" alt="Alt text" /></p>
<h4 id="user-agent-application富web客户端">User Agent Application（富web客户端）</h4>
<p>富web客户端应用是指由javascript构建的web应用，浏览器是客户端代理。这类应用的特点是，程序是存放在web服务器上的，但是运行时，浏览器下载javascript程序到本地，直接在浏览器中执行，例如那些用javascript开发的网页版游戏。下面是一个富web客户端的示例图：</p>

<p><img src="/images/oauth2/overview-client-types-2.png" alt="Alt text" /></p>
<h4 id="native原生应用">Native（原生应用）</h4>
<p>（注：这里指没有后端服务器的应用，一次所有的数据和配置只能存放在客户端程序中）
原生应用包括移动APP和桌面程序。原生应用直接安装在用户的设备上（电脑或手机、平板），client password会保存在用户的设备里。下面是一个原生应用的示例图：
<img src="/images/oauth2/overview-client-types-3.png" alt="Alt text" /></p>
<h4 id="hybrid混合应用">Hybrid（混合应用）</h4>
<p>这类应用通常是将原生应用和web应用的开发技术混合在一起，也会有对应的后端服务器。OAuth 2.0规范中并没有提及此类应用，此类应用可以灵活选用上述三种认证类型中的任何一种。</p>

<h2 id="oauth-20-认证">OAuth 2.0 认证</h2>

<ul>
  <li>Client ID, Client Secret and Redirect URI</li>
  <li>Authorization Grant</li>
  <li>Authorization Code</li>
  <li>Implicit</li>
  <li>Resource Owner Password Credentials</li>
  <li>Client Credentials</li>
</ul>

<p>当一个客户端应用要访问资源服务器上的资源时，需要先获取到认证授权。</p>

<h3 id="client-id-client-secret-and-redirect-uri">Client ID, Client Secret and Redirect URI</h3>
<p>客户端应用需要在认证服务器上注册，注册完成后，认证服务器会生成这个应用的client id和client password。client_id和client_password在同一个认证服务器中是唯一的，不会重复。客户端应用可以在多个认证服务器中进行注册（如分别在Facebook和Google中注册），不同的认证服务器会为客户端应用生成不同的client_id和client_password。
当客户端应用需要访问资源服务器上的资源时，首选要通过认证服务器进行认证，认证时要发送对应的client_id和client_password到认证服务器。
客户端应用在认证服务器进行注册时，需要填写一个重定向URL。当资源所有者对客户端应用进行授权成功后，资源所有者（可简单理解为系统用户）会被重定向到重定向URL所指定的页面。</p>
<h4 id="认证授权">认证授权</h4>
<p>资源所有者会给客户端应用认证授权，认证授权时需要认证服务器和资源服务器进行配合。</p>

<p>OAuth 2.0规范列举了4中认证授权方式，每种方式都有不同的安全特点：</p>

<ul>
  <li>Authorization Code（授权码模式）</li>
  <li>Implicit（简化模式）</li>
  <li>Resource Owner Password Credentials（用户密码模式）</li>
  <li>Client Credentials</li>
</ul>

<p>下面来对每一种授权方式进行解释。</p>
<h4 id="authorization-code授权码模式">Authorization Code（授权码模式）</h4>
<p>Authorization Code（授权码）的认证过程如下：</p>

<ol>
  <li>资源所有者（用户）进入客户端应用。</li>
  <li>客户端应用让用户通过认证服务器进行登录。</li>
  <li>登录之前，客户端应用会把用户重定向到认证服务器的登录页面，同时把client id发送到认证服务器，这样认证服务器就知道是哪一个客户端应用在请求认证授权。</li>
  <li>用户在认证服务器上进行登录，登录成功后，会提示用户是否要对客户端应用进行授权，用户选择同意后，会被重定向回客户端应用。</li>
  <li>当重定向回客户端应用时，使用的是客户端应用在认证服务器上注册时填写的重定向URL，同时认证服务器会发送一个代表此次认证过程的一个授权码。</li>
  <li>当成功重定向到客户端应用后，客户端应用会在后台与认证服务器进行交互，将上个步骤中获取到的授权码，连同client id,client password发送给认证服务器。</li>
  <li>认证服务器对收到的数据进行校验，通过后发送access token给客户端应用。</li>
  <li>这时客户端应用就可以用接收到的access token去资源服务器访问相关资源。下面是一个示例图：</li>
</ol>

<p><img src="/images/oauth2/authorization-auth-code.png" alt="Alt text" /></p>
<h4 id="implicit简化模式">Implicit（简化模式）</h4>
<p>Implicit（简化模式）与Authorization Code（授权码模式）类似，区别仅在于当用户成功登录之后，重定向到客户端应用时，access token会直接返回给客户端应用。
这意味着access token在客户端应用中是可见的。而Authorization Code（授权码模式），access token是在web服务器中的，对客户端来说不可见。这是这两种模式的最大区别。
并且，客户端应用只发送client id到认证服务器。如果连同client password一起发送的话，client password需要存储在客户端应用中，这会是一个安全隐患，很容易通过破解手段拿到存放在客户端应用程序中的client password。下面是一个示例图：</p>

<p><img src="/images/oauth2/authorization-implicit.png" alt="Alt text" /></p>
<h4 id="resource-owner-password-credentials用户密码模式">Resource Owner Password Credentials（用户密码模式）</h4>
<p>Resource Owner Password Credentials（密码模式）允许客户端应用直接使用用户的用户名和密码。例如用户可以直接在客户端应用中录入Twitter的用户名和密码。
只有在充分信任客户端应用的情况下，才能使用密码模式。（因为用户名和密码是在客户端应用中录入的，因此客户端应用可以获取并保存用户的用户名和密码）。
密码模式一般在富web客户端应用和原生应用中使用。</p>
<h4 id="client-credentials">Client Credentials</h4>
<p>Client Credentials默认用户访问跟用户无关的资源，因此不需要用户授权。</p>
<h2 id="oauth-20-节点">OAuth 2.0 节点</h2>
<p>OAuth 2.0定义了节点集合。一个节点一般是web服务器上的一个URL。具体包括：</p>

<ul>
  <li>认证节点</li>
  <li>Token节点</li>
  <li>重定向节点</li>
</ul>

<p>认证节点和Token节点在认证服务器上，重定向节点在客户端应用上。示例图如下：</p>

<p><img src="/images/oauth2/endpoints.png" alt="Alt text" /></p>

<p>OAuth 2.0规范并没有对节点的URL做出明确的定义，不同的实现会提供不同的URL。</p>
<h3 id="认证节点">认证节点</h3>
<p>认证节点是用户进行登录操作的地址。</p>
<h3 id="token节点">Token节点</h3>
<p>Token节点是认证服务器提供的，让客户端应用获取access token的地址。</p>
<h3 id="重定向节点">重定向节点</h3>
<p>重定向节点在客户端应用中，用户成功登录后，会被重定向到此地址。</p>
<h2 id="oauth-20-请求和响应">OAuth 2.0 请求和响应</h2>
<p>当客户端应用请求access token时，会发送http请求到认证服务器。不同的认证授权类型会有不同的请求和响应内容。认证授权类型有4种：</p>

<ul>
  <li>Authorization Code（授权码模式）</li>
  <li>Implicit（简化模式）</li>
  <li>Resource Owner Password Credentials（用户密码模式）</li>
  <li>Client Credentials</li>
</ul>

<p>每种类型的请求和响应内容会在后续的内容中详细解释。</p>
<h2 id="oauth-20-authorization-code授权码模式的请求和响应">OAuth 2.0 Authorization Code（授权码模式）的请求和响应</h2>

<p>授权码模式有2个请求和2个响应：</p>

<ul>
  <li>认证请求＋响应</li>
  <li>access token请求＋响应。</li>
</ul>

<h3 id="授权请求">授权请求</h3>
<p>授权请求发送到认证服务器，然后会获取到一个授权码。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>response_type 	必选项，固定值为 "code"
client_id 	必选项, 客户端应用在认证服务器注册时生成的client id.
redirect_uri 	可选项. T客户端应用在认证服务器注册时填写的重定向URL地址.
scope 	可选项. 请求的权限范围.
state 	可选项 (建议提供). 客户端应用的请求URL中的参数，可以是任意值.
</code></pre></div></div>
<h3 id="授权响应">授权响应</h3>
<p>授权响应含有授权码，这个授权码在后续获取access token时需要提供。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>code 	必选项. 认证服务器返回的授权码.
state 	必选项, 如果客户端应用的请求中有这个参数，既为这个参数的值.
</code></pre></div></div>
<h3 id="授权错误响应">授权错误响应</h3>
<p>授权错误的情形有2种。</p>

<p>第一种是客户端应用验证失败，例如授权请求中发送的重定向URL与客户端应用在认证服务器中注册时填写的URL不一致。</p>

<p>第二种是产生了其它错误，此时下面的错误信息会返回给客户端应用：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>error 	Required. Must be one of a set of predefined error codes. See the specification for the codes and their meaning.
error_description 	Optional. A human-readable UTF-8 encoded text describing the error. Intended for a developer, not an end user.
error_uri 	Optional. A URI pointing to a human-readable web page with information about the error.
state 	Required, if present in authorization request. The same value as sent in the state parameter in the request.
</code></pre></div></div>
<h3 id="token请求">Token请求</h3>
<p>客户端应用获取到授权码后，可以用此授权码去获取access token。请求参数如下：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>client_id 	Required. The client application's id.
client_secret 	Required. The client application's client secret .
grant_type 	Required. Must be set to authorization_code .
code 	Required. The authorization code received by the authorization server.
redirect_uri 	Required, if the request URI was included in the authorization request. Must be identical then.
</code></pre></div></div>
<h3 id="token-响应">Token 响应</h3>
<p>access token的响应内容是json格式的：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{ "access_token"  : "...",
  "token_type"    : "...",
  "expires_in"    : "...",
  "refresh_token" : "...",
}
</code></pre></div></div>
<p>access_token  : 访问令牌,
token_type    : 令牌类型，一般是bearer,
expires_in    : 以秒为单位的令牌失效时间,
refresh_token : 当访问令牌失效时，可以用更新令牌获取一个新的访问令牌</p>
<h2 id="oauth-20-简化模式的请求和响应">OAuth 2.0 简化模式的请求和响应</h2>
<p>简化模式只有一个请求和一个响应。</p>
<h3 id="简化模式授权请求">简化模式授权请求</h3>
<p>该请求的参数如下：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>response_type 	Required. Must be set to token .
client_id 	Required. The client identifier as assigned by the authorization server, when the client was registered.
redirect_uri 	Optional. The redirect URI registered by the client.
scope 	Optional. The possible scope of the request.
state 	Optional (recommended). Any client state that needs to be passed on to the client request URI.
</code></pre></div></div>
<h3 id="简化模式授权响应">简化模式授权响应</h3>
<p>响应包含以下参数，注意该响应的格式不是json的。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>access_token 	Required. The access token assigned by the authorization server.
token_type 	Required. The type of the token
expires_in 	Recommended. A number of seconds after which the access token expires.
scope 	Optional. The scope of the access token.
state 	Required, if present in the autorization request. Must be same value as state parameter in request.
</code></pre></div></div>
<h3 id="简化模式错误响应">简化模式错误响应</h3>
<p>有2种情况会导致错误：
第一种是客户端应用验证失败，例如授权请求中发送的重定向URL与客户端应用在认证服务器中注册时填写的URL不一致。</p>

<p>第二种是产生了其它错误，此时下面的错误信息会返回给客户端应用：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>error 	Required. Must be one of a set of predefined error codes. See the specification for the codes and their meaning.
error_description 	Optional. A human-readable UTF-8 encoded text describing the error. Intended for a developer, not an end user.
error_uri 	Optional. A URI pointing to a human-readable web page with information about the error.
state 	Required, if present in authorization request. The same value as sent in the state parameter in the request.
</code></pre></div></div>
<h2 id="用户密码模式的请求和响应">用户密码模式的请求和响应</h2>
<p>用户密码模式只有一个请求和响应。</p>
<h3 id="用户密码模式的请求">用户密码模式的请求</h3>
<p>请求包含以下参数：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>grant_type 	必选项. 固定值 "password"
username 	必选项. UTF-8编码的用户名.
password 	必选项. UTF-8编码的密码.
scope 	可选项. 请求的权限范围.
</code></pre></div></div>
<h3 id="用户密码模式的响应">用户密码模式的响应</h3>
<p>响应的内容是json格式：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{ "access_token"  : "...",
  "token_type"    : "...",
  "expires_in"    : "...",
  "refresh_token" : "...",
}
</code></pre></div></div>
<p>access_token  : 访问令牌,
token_type    : 令牌类型,
expires_in    : 以秒为单位的令牌失效时间,
refresh_token : 当访问令牌失效时，可以用更新令牌获取一个新的访问令牌</p>
<h2 id="客户端模式的请求和响应">客户端模式的请求和响应</h2>
<h3 id="客户端模式的请求">客户端模式的请求</h3>
<p>请求包含以下参数：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>grant_type 	必选项. 固定值 "client_credentials". 
scope 	可选项. 请求的权限范围.
</code></pre></div></div>
<h3 id="客户端模式的响应">客户端模式的响应</h3>
<p>响应包含以下参数：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{ "access_token"  : "...",
  "token_type"    : "...",
  "expires_in"    : "...",
}
</code></pre></div></div>
<p>access_token  : 访问令牌,
token_type    : 令牌类型,
expires_in    : 以秒为单位的令牌失效时间,
注意，此种授权类型没有refresh_token</p>]]></content><author><name>qihaiyan</name></author><category term="oauth2" /><category term="oauth2" /><summary type="html"><![CDATA[（原文：/oauth2/index.html）]]></summary></entry><entry><title type="html">SpringBoot应用基于docker和EFK的日志处理</title><link href="https://springcamp.cn/spring-boot-efk-log/" rel="alternate" type="text/html" title="SpringBoot应用基于docker和EFK的日志处理" /><published>2017-06-02T14:28:00+00:00</published><updated>2017-06-02T14:28:00+00:00</updated><id>https://springcamp.cn/spring-boot-efk-log</id><content type="html" xml:base="https://springcamp.cn/spring-boot-efk-log/"><![CDATA[<p>1.概述
在分布式集群环境下，单个节点的日志内容往往都是存放在自己的节点上，这种独立分散的日志存储方式会有很多问题。我们需要一个统一的日志处理中心，对日志进行收集和集中存储，并进行查看和分析。<a href="https://12factor.net/zh_cn/logs">The Twelve-Factor App</a>中有关于日志处理的建议。</p>

<p>相应的处理技术现在也很成熟，通常会采用Elastic Search + Logstash + Kibana的技术栈（ELK）。在这篇文章中我们会采用一种更便于部署的方式，采用Elastic Search +Fluentd + Kibana的技术栈（EFK），并且通过docker进行部署。</p>

<p>对应的有一个示例项目在github上，地址：<a href="https://github.com/qihaiyan/fluentd-boot">fluentd-boot</a>。</p>

<!-- more -->

<p>2.安装docker</p>

<p>2.1.设置yum镜像</p>

<p>国外的镜像安装速度很慢，采用清华大学 TUNA 镜像源。</p>

<p>用root用户新建 /etc/yum.repos.d/docker.repo 文件，内容如下：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[dockerrepo]
name=Docker Repository
baseurl=https://mirrors.tuna.tsinghua.edu.cn/docker/yum/repo/centos7
enabled=1
gpgcheck=1
gpgkey=https://mirrors.tuna.tsinghua.edu.cn/docker/yum/gpg
</code></pre></div></div>

<p>2.2.安装</p>

<p>执行命令：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo yum makecache
sudo yum install docker-engine
</code></pre></div></div>

<p>2.3.启动docker服务</p>

<p>执行命令：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>systemctl start docker.service
</code></pre></div></div>

<p>2.4.测试docker服务</p>

<p>执行命令：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run hello-world
</code></pre></div></div>

<p>屏幕上如果输出以下类似信息，说明docker安装正常。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
c04b14da8d14: Pull complete 
Digest: sha256:0256e8a36e2070f7bf2d0b0763dbabdd67798512411de4cdcf9431a1feb60fd9
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker Hub account:
 https://hub.docker.com

For more examples and ideas, visit:
 https://docs.docker.com/engine/userguide/
</code></pre></div></div>

<p>2.5.安装docker-compose</p>

<p>执行命令：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo curl -L https://github.com/docker/compose/releases/download/1.8.1/docker-compose-`uname -s`-`uname -m` &gt; /usr/local/bin/docker-compose

chmod +x /usr/local/bin/docker-compose
</code></pre></div></div>

<p>3.启动容器</p>

<p>通过以下命令来下载示例项目，并进入项目目录:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/qihaiyan/fluentd-boot.git;
cd fluentd-boot
</code></pre></div></div>

<p>在项目目录中执行以下命令来启动docker容器：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose up -d
</code></pre></div></div>

<p>容器的配置是在项目的docker-compose.yml文件中，配置内容非常简单：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>es:
  image: elasticsearch
  volumes:
    - ./es:/usr/share/elasticsearch/data
  ports:
    - 9200:9200
    - 9300:9300

kibana:
  image: kibana
  ports:
    - 5601:5601
  links:
    - es:elasticsearch

fluentd:
  build: fluent-es/
  ports:
    - 24224:24224
  links:
- es:es
</code></pre></div></div>

<p>配置文件中启用了3个容器，分别是elasticsearch、kibana、fluentd。其中elasticsearch、kibana直接从仓库中下载，fluentd是自己建立的容器。注意</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- ./es:/usr/share/elasticsearch/data
</code></pre></div></div>

<p>这一行内容，会将elasticsearch的数据持久化保存在docker-compose.yml所在目录的es目录中。可以将./es修改为其它任何路径，但是对应的目录要有读写权限。</p>

<p>fluentd容器的构建文件是项目的fluent-es目录里的Dockerfile，内容如下：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FROM fluent/fluentd:latest

WORKDIR /home/fluent
ENV PATH /home/fluent/.gem/ruby/2.2.0/bin:$PATH
RUN gem install fluent-plugin-elasticsearch

USER root
COPY fluent.conf /fluentd/etc

EXPOSE 24284

USER fluent
VOLUME /fluentd/log
CMD fluentd -c /fluentd/etc/$FLUENTD_CONF -p /fluentd/plugins $FLUENTD_OPT
</code></pre></div></div>

<p>从配置内容中可以看出，我们自建的fluentd容器是在官方的镜像基础上建的，主要的改动有2点：</p>

<ol>
  <li>安装fluent-plugin-elasticsearch这个plugin;</li>
  <li>将配置文件fluent.conf拷贝到容器中;</li>
</ol>

<p>这两个步骤的作用是让fluentd能够将日志内容发送到elasticsearch。</p>

<p>4.配置SpringBoot应用，将日志发送到fluentd</p>

<p>在项目的build.gradle文件中包含这2行内容：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>compile 'org.fluentd:fluent-logger:0.3.2'
compile 'com.sndyuk:logback-more-appenders:1.1.1'
</code></pre></div></div>

<p>项目会用logback-more-appenders将logback的日志转到fluentd。</p>

<p>logback的配置文件为logback.xml：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;configuration&gt;
    &lt;include resource="org/springframework/boot/logging/logback/base.xml"/&gt;
    &lt;property name="FLUENTD_HOST" value="${FLUENTD_HOST:-${DOCKER_HOST:-localhost}}"/&gt;
    &lt;property name="FLUENTD_PORT" value="${FLUENTD_PORT:-24224}"/&gt;
    &lt;appender name="FLUENT" class="ch.qos.logback.more.appenders.DataFluentAppender"&gt;
        &lt;tag&gt;dab&lt;/tag&gt;
        &lt;label&gt;normal&lt;/label&gt;
        &lt;remoteHost&gt;${FLUENTD_HOST}&lt;/remoteHost&gt;
        &lt;port&gt;${FLUENTD_PORT}&lt;/port&gt;
        &lt;maxQueueSize&gt;20&lt;/maxQueueSize&gt;
    &lt;/appender&gt;

    &lt;logger name="fluentd" level="debug" additivity="false"&gt;
        &lt;appender-ref ref="CONSOLE" /&gt;
        &lt;appender-ref ref="FILE" /&gt;
        &lt;appender-ref ref="FLUENT" /&gt;
    &lt;/logger&gt;
&lt;/configuration&gt;
</code></pre></div></div>

<p>配置文件中通过FLUENTD_HOST和FLUENTD_PORT两个环境变量来指定fluentd的地址和端口。如果环境变量中没有这两项配置，会默认发送到本机地址。</p>

<p>5.执行程序，查看效果
进入fluent-es目录，执行 ./gradlew bootRun</p>

<p>这步会启动SpringBoot的应用，该应用会随机的产生日志信息，并将日志发送到Elastic Search。</p>

<p>在浏览器中打开 <code class="language-plaintext highlighter-rouge">http://localhost:5601</code>  可以看到Kibana dashboard的页面。</p>

<p>通过在环境变量中配置FLUENTD_HOST 和 FLUENTD_PORT，可以指定docker容器的地址和端口，如果没有指定，日志会默认发送到localhost，在此种情况下，SpringBoot应用和docker容器应该是运行在同一台机器上。</p>

<p>6.总结</p>

<p>在现代化的系统架构中，越来越强调云计算、微服务、集群部署，日志的集中处理是需要重点考虑的环节。为了便于演示，只是部署了单节点，可以通过kubernetes或是docker自带的docker swarm来实现集群部署。</p>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><summary type="html"><![CDATA[1.概述 在分布式集群环境下，单个节点的日志内容往往都是存放在自己的节点上，这种独立分散的日志存储方式会有很多问题。我们需要一个统一的日志处理中心，对日志进行收集和集中存储，并进行查看和分析。The Twelve-Factor App中有关于日志处理的建议。]]></summary></entry><entry><title type="html">SpringBoot框架中REST接口的异常处理方法</title><link href="https://springcamp.cn/spring-rest-exception/" rel="alternate" type="text/html" title="SpringBoot框架中REST接口的异常处理方法" /><published>2017-05-21T10:28:00+00:00</published><updated>2017-05-21T10:28:00+00:00</updated><id>https://springcamp.cn/spring-rest-exception</id><content type="html" xml:base="https://springcamp.cn/spring-rest-exception/"><![CDATA[<h2 id="一-http状态码">一. HTTP状态码</h2>

<ol>
  <li>100 到199 的状态码代表信息，描述对于请求的处理。</li>
  <li>200 到 299 的状态码表示客户端发来的请求已经被接收并正确处理。</li>
  <li>300 到 399 的状态码表示客户端需要进一步的处理才能完成请求，比如重定向到另一个地址。</li>
  <li>400 到 499 的状态码表示客户端的请求有错误，需要修正。404就是这种情况。</li>
  <li>500 到 599 的状态码表示服务器在处理客户端请求时发生了内部错误。</li>
</ol>

<p>在SpringBoot中，如果接口中有未处理的异常，会返回500，表示内部服务器错误。简单来说，如果后台程序没有对异常做特殊处理，只要有异常抛出，客户端收到的状态码就是500。</p>

<h2 id="二-在异常类中定义状态码">二. 在异常类中定义状态码</h2>

<p>我们可以通过使用@ResponseStatus注解在异常类中定义返回的状态码。</p>

<!-- more -->

<p>例如：</p>

<p>这是一个用户自定义异常类</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@ResponseStatus</span><span class="o">(</span><span class="n">value</span><span class="o">=</span><span class="nc">HttpStatus</span><span class="o">.</span><span class="na">NOT_FOUND</span><span class="o">,</span> <span class="n">reason</span><span class="o">=</span><span class="s">"No such Order"</span><span class="o">)</span>  <span class="c1">// 404</span>
 <span class="kd">public</span> <span class="kd">class</span> <span class="nc">OrderNotFoundException</span> <span class="kd">extends</span> <span class="nc">RuntimeException</span> <span class="o">{</span>
     <span class="c1">// ...</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">@RequestMapping</span><span class="o">(</span><span class="n">value</span><span class="o">=</span><span class="s">"/orders/{id}"</span><span class="o">,</span> <span class="n">method</span><span class="o">=</span><span class="no">GET</span><span class="o">)</span>
 <span class="kd">public</span> <span class="nc">Order</span> <span class="nf">showOrder</span><span class="o">(</span><span class="nd">@PathVariable</span><span class="o">(</span><span class="s">"id"</span><span class="o">)</span> <span class="kt">long</span> <span class="n">id</span><span class="o">,</span> <span class="nc">Model</span> <span class="n">model</span><span class="o">)</span> <span class="o">{</span>
     <span class="nc">Order</span> <span class="n">order</span> <span class="o">=</span> <span class="n">orderRepository</span><span class="o">.</span><span class="na">findOrderById</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">order</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nc">OrderNotFoundException</span><span class="o">(</span><span class="n">id</span><span class="o">);</span>
     <span class="k">return</span> <span class="n">order</span><span class="o">;</span>
 <span class="o">}</span>
</code></pre></div></div>

<p>当这个接口中没有找到指定的order id时，就会返回404。原因是该接口抛出了OrderNotFoundException异常，而这个异常中@ResponseStatus注解指定了返回码是HttpStatus.NOT_FOUND，也就是400。</p>

<h2 id="三-在接口中进行异常处理">三. 在接口中进行异常处理</h2>

<p>可以在接口类中定义带有@ExceptionHandler注解的方法，来处理该类中所有接口的异常。</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">RestServiceError</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="nc">String</span> <span class="n">code</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">message</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getCode</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">code</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">setCode</span><span class="o">(</span><span class="nc">String</span> <span class="n">code</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">code</span> <span class="o">=</span> <span class="n">code</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getMessage</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">message</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">setMessage</span><span class="o">(</span><span class="nc">String</span> <span class="n">message</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">message</span> <span class="o">=</span> <span class="n">message</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">RestServiceError</span> <span class="nf">build</span> <span class="o">(</span><span class="nc">Type</span> <span class="n">errorType</span><span class="o">,</span> <span class="nc">String</span> <span class="n">message</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">RestServiceError</span> <span class="n">error</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">RestServiceError</span><span class="o">();</span>
        <span class="n">error</span><span class="o">.</span><span class="na">code</span> <span class="o">=</span> <span class="n">errorType</span><span class="o">.</span><span class="na">getCode</span><span class="o">();</span>
        <span class="n">error</span><span class="o">.</span><span class="na">message</span> <span class="o">=</span> <span class="n">message</span><span class="o">;</span>
        <span class="k">return</span> <span class="n">error</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">enum</span> <span class="nc">Type</span> <span class="o">{</span>
        <span class="no">BAD_REQUEST_ERROR</span><span class="o">(</span><span class="s">"error.badrequest"</span><span class="o">,</span> <span class="s">"Bad request error"</span><span class="o">),</span>
        <span class="no">INTERNAL_SERVER_ERROR</span><span class="o">(</span><span class="s">"error.internalserver"</span><span class="o">,</span> <span class="s">"Unexpected server error"</span><span class="o">),</span>
        <span class="no">VALIDATION_ERROR</span><span class="o">(</span><span class="s">"error.validation"</span><span class="o">,</span> <span class="s">"Found validation issues"</span><span class="o">);</span>

        <span class="kd">private</span> <span class="nc">String</span> <span class="n">code</span><span class="o">;</span>
        <span class="kd">private</span> <span class="nc">String</span> <span class="n">message</span><span class="o">;</span>

        <span class="nc">Type</span><span class="o">(</span><span class="nc">String</span> <span class="n">code</span><span class="o">,</span> <span class="nc">String</span> <span class="n">message</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">this</span><span class="o">.</span><span class="na">code</span> <span class="o">=</span> <span class="n">code</span><span class="o">;</span>
            <span class="k">this</span><span class="o">.</span><span class="na">message</span> <span class="o">=</span> <span class="n">message</span><span class="o">;</span>
        <span class="o">}</span>

        <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getCode</span><span class="o">()</span> <span class="o">{</span>
            <span class="k">return</span> <span class="n">code</span><span class="o">;</span>
        <span class="o">}</span>

        <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getMessage</span><span class="o">()</span> <span class="o">{</span>
            <span class="k">return</span> <span class="n">message</span><span class="o">;</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>定义一个带有异常处理方法的REST接口类：</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">ControllerWithExceptionHandling</span> <span class="o">{</span>

  <span class="c1">// @RequestMapping 方法</span>
  <span class="o">...</span>
  
  <span class="c1">// 异常处理方法：</span>
  <span class="c1">// 根据特定的异常返回指定的 HTTP 状态码</span>
  <span class="nd">@ResponseStatus</span><span class="o">(</span><span class="n">value</span><span class="o">=</span><span class="nc">HttpStatus</span><span class="o">.</span><span class="na">BAD_REQUEST</span><span class="o">)</span>  <span class="c1">// 400</span>
  <span class="nd">@ExceptionHandler</span><span class="o">(</span><span class="nc">ConstraintViolationException</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
  <span class="nd">@ResponseBody</span>
  <span class="kd">public</span> <span class="nc">RestServiceError</span> <span class="nf">handleValidationException</span><span class="o">(</span><span class="nc">ConstraintViolationException</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Set</span><span class="o">&lt;</span><span class="nc">ConstraintViolation</span><span class="o">&lt;?&gt;&gt;</span> <span class="n">errors</span> <span class="o">=</span> <span class="n">ex</span><span class="o">.</span><span class="na">getConstraintViolations</span><span class="o">();</span>
        <span class="nc">StringBuilder</span> <span class="n">strBuilder</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">StringBuilder</span><span class="o">();</span>
        <span class="k">for</span> <span class="o">(</span><span class="nc">ConstraintViolation</span><span class="o">&lt;?&gt;</span> <span class="n">violation</span> <span class="o">:</span> <span class="n">errors</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">strBuilder</span><span class="o">.</span><span class="na">append</span><span class="o">(</span><span class="n">violation</span><span class="o">.</span><span class="na">getMessage</span><span class="o">()</span> <span class="o">+</span> <span class="s">"\n"</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="nc">RestServiceError</span><span class="o">.</span><span class="na">build</span><span class="o">(</span><span class="nc">RestServiceError</span><span class="o">.</span><span class="na">Type</span><span class="o">.</span><span class="na">VALIDATION_ERROR</span><span class="o">,</span> <span class="n">strBuilder</span><span class="o">.</span><span class="na">toString</span><span class="o">());</span>
  <span class="o">}</span>
  
  <span class="c1">// 通用异常的处理，返回500</span>
  <span class="nd">@ResponseStatus</span><span class="o">(</span><span class="n">value</span><span class="o">=</span><span class="nc">HttpStatus</span><span class="o">.</span><span class="na">INTERNAL_SERVER_ERROR</span><span class="o">)</span>  <span class="c1">// 500</span>
  <span class="nd">@ExceptionHandler</span><span class="o">(</span><span class="nc">Exception</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
  <span class="nd">@ResponseBody</span>
  <span class="kd">public</span> <span class="nc">RestServiceError</span> <span class="nf">handleException</span><span class="o">(</span><span class="nc">Exception</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">RestServiceError</span><span class="o">.</span><span class="na">build</span><span class="o">(</span><span class="nc">RestServiceError</span><span class="o">.</span><span class="na">Type</span><span class="o">.</span><span class="na">INTERNAL_SERVER_ERROR</span><span class="o">,</span> <span class="n">ex</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>这样，该类中的所有接口如果有异常发生，就会返回对应的 HTTP 状态码，并且在返回的报文体中包含错误描述信息。</p>

<p>如果接口抛出的异常是ConstraintViolationException，就是返回handleValidationException方法中指定的状态码和错误信息，否则返回handleException方法中指定的状态码和错误信息。可以通过增加异常处理方法来处理更多特定的异常。</p>

<p>错误信息结构如下：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"code"</span><span class="p">:</span><span class="s2">"error.internalserver"</span><span class="p">,</span><span class="nl">"message"</span><span class="p">:</span><span class="s2">"内部服务器错误"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="四-全局异常处理">四. 全局异常处理</h2>

<p>上述的方法中是处理特定接口类的异常，需要在每个接口类中进行异常处理。现在介绍一种更简单的全局异常处理方法。</p>

<p>使用@ControllerAdvice类来进行全局异常处理。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@ControllerAdvice</span>
<span class="kd">class</span> <span class="nc">GlobalControllerExceptionHandler</span> <span class="o">{</span>

  <span class="c1">// 异常处理方法：</span>
  <span class="c1">// 根据特定的异常返回指定的 HTTP 状态码</span>
  <span class="nd">@ResponseStatus</span><span class="o">(</span><span class="n">value</span><span class="o">=</span><span class="nc">HttpStatus</span><span class="o">.</span><span class="na">BAD_REQUEST</span><span class="o">)</span>  <span class="c1">// 400</span>
  <span class="nd">@ExceptionHandler</span><span class="o">(</span><span class="nc">ConstraintViolationException</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
  <span class="nd">@ResponseBody</span>
  <span class="kd">public</span> <span class="nc">RestServiceError</span> <span class="nf">handleValidationException</span><span class="o">(</span><span class="nc">ConstraintViolationException</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Set</span><span class="o">&lt;</span><span class="nc">ConstraintViolation</span><span class="o">&lt;?&gt;&gt;</span> <span class="n">errors</span> <span class="o">=</span> <span class="n">ex</span><span class="o">.</span><span class="na">getConstraintViolations</span><span class="o">();</span>
        <span class="nc">StringBuilder</span> <span class="n">strBuilder</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">StringBuilder</span><span class="o">();</span>
        <span class="k">for</span> <span class="o">(</span><span class="nc">ConstraintViolation</span><span class="o">&lt;?&gt;</span> <span class="n">violation</span> <span class="o">:</span> <span class="n">errors</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">strBuilder</span><span class="o">.</span><span class="na">append</span><span class="o">(</span><span class="n">violation</span><span class="o">.</span><span class="na">getMessage</span><span class="o">()</span> <span class="o">+</span> <span class="s">"\n"</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="nc">RestServiceError</span><span class="o">.</span><span class="na">build</span><span class="o">(</span><span class="nc">RestServiceError</span><span class="o">.</span><span class="na">Type</span><span class="o">.</span><span class="na">VALIDATION_ERROR</span><span class="o">,</span> <span class="n">strBuilder</span><span class="o">.</span><span class="na">toString</span><span class="o">());</span>
  <span class="o">}</span>
  
  <span class="c1">// 通用异常的处理，返回500</span>
  <span class="nd">@ResponseStatus</span><span class="o">(</span><span class="n">value</span><span class="o">=</span><span class="nc">HttpStatus</span><span class="o">.</span><span class="na">INTERNAL_SERVER_ERROR</span><span class="o">)</span>  <span class="c1">// 500</span>
  <span class="nd">@ExceptionHandler</span><span class="o">(</span><span class="nc">Exception</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
  <span class="nd">@ResponseBody</span>
  <span class="kd">public</span> <span class="nc">RestServiceError</span> <span class="nf">handleException</span><span class="o">(</span><span class="nc">Exception</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">RestServiceError</span><span class="o">.</span><span class="na">build</span><span class="o">(</span><span class="nc">RestServiceError</span><span class="o">.</span><span class="na">Type</span><span class="o">.</span><span class="na">INTERNAL_SERVER_ERROR</span><span class="o">,</span> <span class="n">ex</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>我们可以看到该类的编写方法与上一种处理方式中的编写方法是一致的，相比上一种处理方式的优点是不用每个类去编写异常处理方式，简化了程序的开发，同时可以对所有异常进行集中处理。</p>

<h2 id="五-错误信息的语言本地化处理">五. 错误信息的语言本地化处理</h2>

<p>上述几个方法中错误信息都是写在代码中的，无法支持多语言处理和自定义配置。下面介绍如何从配置文件中根据本地化语言读取相应的错误信息，能够在英语环境中返回英文描述，在中文环境中返回中文描述。</p>

<p>SpringBoot内置了国际化语言的处理机制，只需几个简单的配置就能够使用该功能。</p>

<p>首先在配置文件中指定信息描述的配置文件，以yml配置为例：</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">messages</span><span class="pi">:</span>
    <span class="na">basename</span><span class="pi">:</span> <span class="s">i18n/messages</span>
</code></pre></div></div>

<p>通过这个配置，程序会到resources/i18n目录下读取messages.properties、messages_zh_CN.properties等配置文件中的内容。
配置文件内容很简单：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>error.badrequest <span class="o">=</span> 错误的请求参数
error.internalserver <span class="o">=</span> 内部服务器错误
error.validation <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">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">LocaleMessageUtil</span> <span class="o">{</span>

    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="nc">MessageSource</span> <span class="n">messageSource</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nc">RestServiceError</span> <span class="nf">getLocalErrorMessage</span><span class="o">(</span><span class="nc">RestServiceError</span><span class="o">.</span><span class="na">Type</span> <span class="n">errorCode</span><span class="o">,</span> <span class="nc">String</span> <span class="n">description</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Locale</span> <span class="n">locale</span> <span class="o">=</span> <span class="nc">LocaleContextHolder</span><span class="o">.</span><span class="na">getLocale</span><span class="o">();</span>
        <span class="nc">String</span> <span class="n">errorMessage</span> <span class="o">=</span> <span class="n">messageSource</span><span class="o">.</span><span class="na">getMessage</span><span class="o">(</span><span class="n">errorCode</span><span class="o">.</span><span class="na">getCode</span><span class="o">(),</span> <span class="kc">null</span><span class="o">,</span> <span class="n">locale</span><span class="o">);</span>
        <span class="nc">RestServiceError</span> <span class="n">error</span> <span class="o">=</span> <span class="nc">RestServiceError</span><span class="o">.</span><span class="na">build</span><span class="o">(</span><span class="n">errorCode</span><span class="o">,</span> <span class="n">errorMessage</span><span class="o">,</span> <span class="n">description</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">error</span><span class="o">;</span>
    <span class="o">}</span>

<span class="o">}</span>
</code></pre></div></div>

<p>其中LocaleContextHolder.getLocale()就是读取语言环境的方法。</p>

<p>对上一章节中介绍的全局异常处理方法进行简单的改造，就能支持多语言的异常信息。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@ControllerAdvice</span>
<span class="kd">class</span> <span class="nc">GlobalControllerExceptionHandler</span> <span class="o">{</span>
  <span class="nd">@Autowired</span>
  <span class="nc">LocaleMessageUtil</span> <span class="n">localeMessageUtil</span><span class="o">;</span>
  <span class="c1">// 异常处理方法：</span>
  <span class="c1">// 根据特定的异常返回指定的 HTTP 状态码</span>
  <span class="nd">@ResponseStatus</span><span class="o">(</span><span class="n">value</span><span class="o">=</span><span class="nc">HttpStatus</span><span class="o">.</span><span class="na">BAD_REQUEST</span><span class="o">)</span>  <span class="c1">// 400</span>
  <span class="nd">@ExceptionHandler</span><span class="o">(</span><span class="nc">ConstraintViolationException</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
  <span class="nd">@ResponseBody</span>
  <span class="kd">public</span> <span class="nc">RestServiceError</span> <span class="nf">handleValidationException</span><span class="o">(</span><span class="nc">ConstraintViolationException</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Set</span><span class="o">&lt;</span><span class="nc">ConstraintViolation</span><span class="o">&lt;?&gt;&gt;</span> <span class="n">errors</span> <span class="o">=</span> <span class="n">ex</span><span class="o">.</span><span class="na">getConstraintViolations</span><span class="o">();</span>
        <span class="nc">StringBuilder</span> <span class="n">strBuilder</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">StringBuilder</span><span class="o">();</span>
        <span class="k">for</span> <span class="o">(</span><span class="nc">ConstraintViolation</span><span class="o">&lt;?&gt;</span> <span class="n">violation</span> <span class="o">:</span> <span class="n">errors</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">strBuilder</span><span class="o">.</span><span class="na">append</span><span class="o">(</span><span class="n">violation</span><span class="o">.</span><span class="na">getMessage</span><span class="o">()</span> <span class="o">+</span> <span class="s">"\n"</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="n">localeMessageUtil</span><span class="o">.</span><span class="na">getLocalErrorMessage</span><span class="o">(</span><span class="nc">RestServiceError</span><span class="o">.</span><span class="na">Type</span><span class="o">.</span><span class="na">IVALIDATION_ERROR</span><span class="o">);</span>
  <span class="o">}</span>
  
  <span class="c1">// 通用异常的处理，返回500</span>
  <span class="nd">@ResponseStatus</span><span class="o">(</span><span class="n">value</span><span class="o">=</span><span class="nc">HttpStatus</span><span class="o">.</span><span class="na">INTERNAL_SERVER_ERROR</span><span class="o">)</span>  <span class="c1">// 500</span>
  <span class="nd">@ExceptionHandler</span><span class="o">(</span><span class="nc">Exception</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
  <span class="nd">@ResponseBody</span>
  <span class="kd">public</span> <span class="nc">RestServiceError</span> <span class="nf">handleException</span><span class="o">(</span><span class="nc">Exception</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">localeMessageUtil</span><span class="o">.</span><span class="na">getLocalErrorMessage</span><span class="o">(</span><span class="nc">RestServiceError</span><span class="o">.</span><span class="na">Type</span><span class="o">.</span><span class="na">INTERNAL_SERVER_ERROR</span><span class="o">);</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>改动的地方就是引入了我们上面编写的LocaleMessageUtil类，通过这个类的getLocalErrorMessage方法来生成多语言信息。</p>

<h2 id="六-总结">六. 总结</h2>

<p>接口异常处理的核心内容就是根据具体异常返回指定的错误信息：</p>

<ol>
  <li>HTTP 状态码</li>
  <li>返回报文体中的错误描述</li>
</ol>

<p>异常处理方式常用的有3种，建议用第3种方式进行集中的异常处理：</p>

<ol>
  <li>在异常类中定义状态码和错误信息，适用于指定异常的处理</li>
  <li>在REST接口类中定义状态码和错误信息，适用于指定接口类的处理</li>
  <li>通过@ControllerAdvice注解定义全局状态码和错误信息</li>
  <li>通过SpringBoot的MessageSource来进行多语言支持的处理</li>
</ol>]]></content><author><name>qihaiyan</name></author><category term="spring boot" /><category term="spring" /><summary type="html"><![CDATA[一. HTTP状态码]]></summary></entry></feed>