Jekyll2023-08-14T11:27:18+08:00https://onew.me/feed.xml一水开发笔记java,多线程,jvm,redis,redisdesktopmanager,postgresql
onewadmin@onew.me我是如何把 Tokio Runtime 给卡死的2023-05-22T15:55:12+08:002023-05-22T15:55:12+08:00https://onew.me/rust/2023/05/22/block-on-tokio-runtime<h1 id="一前言">一、前言</h1>
<p>最近在忙着做 <a href="https://github.com/nacos-group/nacos-sdk-rust">nacos-sdk-rust</a> <code class="language-plaintext highlighter-rouge">3.x</code> 版本的重构, 在重构的过程中一切都很顺利, 但是后面的验证过程中, 发现程序每隔 30 分钟就会卡死, 心跳也不发了. 这就很奇怪了. 于是就研究了一下 <code class="language-plaintext highlighter-rouge">tokio</code> 的调度机制.</p>
<h1 id="二给你一段卡死的代码">二、给你一段卡死的代码</h1>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">#[tokio::test(flavor</span> <span class="nd">=</span> <span class="s">"multi_thread"</span><span class="nd">,</span> <span class="nd">worker_threads</span> <span class="nd">=</span> <span class="mi">8</span><span class="nd">)]</span>
<span class="k">pub</span> <span class="k">async</span> <span class="k">fn</span> <span class="nf">test_switch</span><span class="p">()</span> <span class="p">{</span>
<span class="k">let</span> <span class="k">mut</span> <span class="n">handles</span> <span class="o">=</span> <span class="nn">Vec</span><span class="p">::</span><span class="nf">new</span><span class="p">();</span>
<span class="n">handles</span><span class="nf">.push</span><span class="p">(</span><span class="nn">tokio</span><span class="p">::</span><span class="nf">spawn</span><span class="p">({</span>
<span class="k">async</span> <span class="p">{</span>
<span class="k">loop</span> <span class="p">{</span>
<span class="nd">println!</span><span class="p">(</span><span class="s">"{:?}: loop task 1"</span><span class="p">,</span> <span class="nn">thread</span><span class="p">::</span><span class="nf">current</span><span class="p">()</span><span class="nf">.id</span><span class="p">());</span>
<span class="nn">tokio</span><span class="p">::</span><span class="nn">time</span><span class="p">::</span><span class="nf">sleep</span><span class="p">(</span><span class="nn">Duration</span><span class="p">::</span><span class="nf">from_secs</span><span class="p">(</span><span class="mi">10</span><span class="p">))</span><span class="k">.await</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}));</span>
<span class="n">handles</span><span class="nf">.push</span><span class="p">(</span><span class="nn">tokio</span><span class="p">::</span><span class="nf">spawn</span><span class="p">({</span>
<span class="k">async</span> <span class="p">{</span>
<span class="k">loop</span> <span class="p">{</span>
<span class="nd">println!</span><span class="p">(</span><span class="s">"{:?}: loop task 2"</span><span class="p">,</span> <span class="nn">thread</span><span class="p">::</span><span class="nf">current</span><span class="p">()</span><span class="nf">.id</span><span class="p">());</span>
<span class="nn">tokio</span><span class="p">::</span><span class="nn">time</span><span class="p">::</span><span class="nf">sleep</span><span class="p">(</span><span class="nn">Duration</span><span class="p">::</span><span class="nf">from_secs</span><span class="p">(</span><span class="mi">10</span><span class="p">))</span><span class="k">.await</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}));</span>
<span class="n">handles</span><span class="nf">.push</span><span class="p">(</span><span class="nn">tokio</span><span class="p">::</span><span class="nf">spawn</span><span class="p">({</span>
<span class="k">async</span> <span class="p">{</span>
<span class="k">let</span> <span class="k">mut</span> <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="k">loop</span> <span class="p">{</span>
<span class="nd">println!</span><span class="p">(</span><span class="s">"{:?}: loop task 3, task count: {}"</span><span class="p">,</span> <span class="nn">thread</span><span class="p">::</span><span class="nf">current</span><span class="p">()</span><span class="nf">.id</span><span class="p">(),</span> <span class="n">count</span><span class="p">);</span>
<span class="k">if</span> <span class="n">count</span> <span class="o">></span> <span class="mi">3</span> <span class="p">{</span>
<span class="k">let</span> <span class="p">(</span><span class="n">tx</span><span class="p">,</span> <span class="n">rx</span><span class="p">)</span> <span class="o">=</span> <span class="nn">std</span><span class="p">::</span><span class="nn">sync</span><span class="p">::</span><span class="nn">mpsc</span><span class="p">::</span><span class="nn">channel</span><span class="p">::</span><span class="o"><</span><span class="nb">String</span><span class="o">></span><span class="p">();</span>
<span class="nn">tokio</span><span class="p">::</span><span class="nf">spawn</span><span class="p">(</span><span class="k">async</span> <span class="k">move</span><span class="p">{</span>
<span class="k">let</span> <span class="mi">_</span> <span class="o">=</span> <span class="n">tx</span><span class="nf">.send</span><span class="p">(</span><span class="s">"send message"</span><span class="nf">.to_string</span><span class="p">());</span>
<span class="p">});</span>
<span class="k">let</span> <span class="n">ret</span> <span class="o">=</span> <span class="n">rx</span><span class="nf">.recv</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">();</span>
<span class="nd">println!</span><span class="p">(</span><span class="s">"receive message :{} , task count :{}"</span><span class="p">,</span> <span class="n">ret</span><span class="p">,</span> <span class="n">count</span><span class="p">);</span>
<span class="p">}</span>
<span class="nn">tokio</span><span class="p">::</span><span class="nn">time</span><span class="p">::</span><span class="nf">sleep</span><span class="p">(</span><span class="nn">Duration</span><span class="p">::</span><span class="nf">from_secs</span><span class="p">(</span><span class="mi">5</span><span class="p">))</span><span class="k">.await</span><span class="p">;</span>
<span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}));</span>
<span class="k">for</span> <span class="n">handle</span> <span class="n">in</span> <span class="n">handles</span> <span class="p">{</span>
<span class="n">handle</span><span class="k">.await</span><span class="nf">.expect</span><span class="p">(</span><span class="s">"handle await"</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>在遇到定时执行任务的情况, 我相信大多数人的写法都和上面的写法差不多. 看着好像没啥问题对吧. 下面是运行日志</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>running 1 test
ThreadId(10): loop task 1
ThreadId(7): loop task 3, task count: 0
ThreadId(9): loop task 2
ThreadId(10): loop task 3, task count: 1
ThreadId(10): loop task 3, task count: 2
ThreadId(6): loop task 1
ThreadId(10): loop task 2
ThreadId(4): loop task 3, task count: 3
ThreadId(4): loop task 1
ThreadId(10): loop task 2
ThreadId(4): loop task 3, task count: 4
</code></pre></div></div>
<p>在程序跑到 <code class="language-plaintext highlighter-rouge">ThreadId(4): loop task 3, task count: 4</code> 时后面的任务就不执行了, 好像所有的任务都暂停了一样. 是的, 我们成功的把 <code class="language-plaintext highlighter-rouge">tokio </code>的 <code class="language-plaintext highlighter-rouge">wroker</code> 全部卡死了. 从这里引出 2 个问题:</p>
<ul>
<li>为什么 <code class="language-plaintext highlighter-rouge">let ret = rx.recv().unwrap();</code> 这行代码为什么被阻塞, <code class="language-plaintext highlighter-rouge">let _ = tx.send("send message".to_string());</code> 没有被执行吗?</li>
<li>为什么其他定时任务不执行了?</li>
</ul>
<h1 id="三被阻塞的代码">三、被阻塞的代码</h1>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="p">(</span><span class="n">tx</span><span class="p">,</span> <span class="n">rx</span><span class="p">)</span> <span class="o">=</span> <span class="nn">std</span><span class="p">::</span><span class="nn">sync</span><span class="p">::</span><span class="nn">mpsc</span><span class="p">::</span><span class="nn">channel</span><span class="p">::</span><span class="o"><</span><span class="nb">String</span><span class="o">></span><span class="p">();</span>
<span class="nn">tokio</span><span class="p">::</span><span class="nf">spawn</span><span class="p">(</span><span class="k">async</span> <span class="k">move</span><span class="p">{</span>
<span class="k">let</span> <span class="mi">_</span> <span class="o">=</span> <span class="n">tx</span><span class="nf">.send</span><span class="p">(</span><span class="s">"send message"</span><span class="nf">.to_string</span><span class="p">());</span>
<span class="p">});</span>
<span class="k">let</span> <span class="n">ret</span> <span class="o">=</span> <span class="n">rx</span><span class="nf">.recv</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">();</span>
</code></pre></div></div>
<p>这里明明通过 <code class="language-plaintext highlighter-rouge">tokio::spawn</code> 派生了一个任务, 为什么不执行呢? 这里就要看看 <code class="language-plaintext highlighter-rouge">tokio</code> 的任务调度逻辑了</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nd">#[track_caller]</span>
<span class="k">pub</span> <span class="k">fn</span> <span class="n">spawn</span><span class="o"><</span><span class="n">T</span><span class="o">></span><span class="p">(</span><span class="n">future</span><span class="p">:</span> <span class="n">T</span><span class="p">)</span> <span class="k">-></span> <span class="n">JoinHandle</span><span class="o"><</span><span class="nn">T</span><span class="p">::</span><span class="n">Output</span><span class="o">></span>
<span class="k">where</span>
<span class="n">T</span><span class="p">:</span> <span class="n">Future</span> <span class="o">+</span> <span class="nb">Send</span> <span class="o">+</span> <span class="nv">'static</span><span class="p">,</span>
<span class="nn">T</span><span class="p">::</span><span class="n">Output</span><span class="p">:</span> <span class="nb">Send</span> <span class="o">+</span> <span class="nv">'static</span><span class="p">,</span>
<span class="p">{</span>
<span class="c">// preventing stack overflows on debug mode, by quickly sending the</span>
<span class="c">// task to the heap.</span>
<span class="c">// 省略无关的代码 ..</span>
<span class="nf">spawn_inner</span><span class="p">(</span><span class="n">future</span><span class="p">,</span> <span class="nb">None</span><span class="p">)</span>
<span class="p">}</span>
<span class="nd">#[track_caller]</span>
<span class="k">pub</span><span class="p">(</span><span class="n">super</span><span class="p">)</span> <span class="k">fn</span> <span class="n">spawn_inner</span><span class="o"><</span><span class="n">T</span><span class="o">></span><span class="p">(</span><span class="n">future</span><span class="p">:</span> <span class="n">T</span><span class="p">,</span> <span class="n">name</span><span class="p">:</span> <span class="nb">Option</span><span class="o"><&</span><span class="nb">str</span><span class="o">></span><span class="p">)</span> <span class="k">-></span> <span class="n">JoinHandle</span><span class="o"><</span><span class="nn">T</span><span class="p">::</span><span class="n">Output</span><span class="o">></span>
<span class="k">where</span>
<span class="n">T</span><span class="p">:</span> <span class="n">Future</span> <span class="o">+</span> <span class="nb">Send</span> <span class="o">+</span> <span class="nv">'static</span><span class="p">,</span>
<span class="nn">T</span><span class="p">::</span><span class="n">Output</span><span class="p">:</span> <span class="nb">Send</span> <span class="o">+</span> <span class="nv">'static</span><span class="p">,</span>
<span class="p">{</span>
<span class="k">use</span> <span class="nn">crate</span><span class="p">::</span><span class="nn">runtime</span><span class="p">::{</span><span class="n">task</span><span class="p">,</span> <span class="n">context</span><span class="p">};</span>
<span class="c">// 生产 task id</span>
<span class="k">let</span> <span class="n">id</span> <span class="o">=</span> <span class="nn">task</span><span class="p">::</span><span class="nn">Id</span><span class="p">::</span><span class="nf">next</span><span class="p">();</span>
<span class="c">// 获取当前上下文中的 Spawner </span>
<span class="k">let</span> <span class="n">spawn_handle</span> <span class="o">=</span> <span class="nn">context</span><span class="p">::</span><span class="nf">spawn_handle</span><span class="p">()</span><span class="nf">.expect</span><span class="p">(</span><span class="n">CONTEXT_MISSING_ERROR</span><span class="p">);</span>
<span class="c">// 创建 task</span>
<span class="k">let</span> <span class="n">task</span> <span class="o">=</span> <span class="nn">crate</span><span class="p">::</span><span class="nn">util</span><span class="p">::</span><span class="nn">trace</span><span class="p">::</span><span class="nf">task</span><span class="p">(</span><span class="n">future</span><span class="p">,</span> <span class="s">"task"</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="n">id</span><span class="nf">.as_u64</span><span class="p">());</span>
<span class="n">spawn_handle</span><span class="nf">.spawn</span><span class="p">(</span><span class="n">task</span><span class="p">,</span> <span class="n">id</span><span class="p">)</span>
<span class="p">}</span>
<span class="c">// 重点在 spawner 这里</span>
<span class="k">impl</span> <span class="n">Spawner</span> <span class="p">{</span>
<span class="c">/// Spawns a future onto the thread pool</span>
<span class="k">pub</span><span class="p">(</span><span class="n">crate</span><span class="p">)</span> <span class="k">fn</span> <span class="n">spawn</span><span class="o"><</span><span class="n">F</span><span class="o">></span><span class="p">(</span><span class="o">&</span><span class="k">self</span><span class="p">,</span> <span class="n">future</span><span class="p">:</span> <span class="n">F</span><span class="p">,</span> <span class="n">id</span><span class="p">:</span> <span class="nn">task</span><span class="p">::</span><span class="n">Id</span><span class="p">)</span> <span class="k">-></span> <span class="n">JoinHandle</span><span class="o"><</span><span class="nn">F</span><span class="p">::</span><span class="n">Output</span><span class="o">></span>
<span class="k">where</span>
<span class="n">F</span><span class="p">:</span> <span class="nn">crate</span><span class="p">::</span><span class="nn">future</span><span class="p">::</span><span class="n">Future</span> <span class="o">+</span> <span class="nb">Send</span> <span class="o">+</span> <span class="nv">'static</span><span class="p">,</span>
<span class="nn">F</span><span class="p">::</span><span class="n">Output</span><span class="p">:</span> <span class="nb">Send</span> <span class="o">+</span> <span class="nv">'static</span><span class="p">,</span>
<span class="p">{</span>
<span class="c">// 绑定一个 task</span>
<span class="nn">worker</span><span class="p">::</span><span class="nn">Shared</span><span class="p">::</span><span class="nf">bind_new_task</span><span class="p">(</span><span class="o">&</span><span class="k">self</span><span class="py">.shared</span><span class="p">,</span> <span class="n">future</span><span class="p">,</span> <span class="n">id</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">impl</span> <span class="n">Shared</span> <span class="p">{</span>
<span class="k">pub</span><span class="p">(</span><span class="n">super</span><span class="p">)</span> <span class="k">fn</span> <span class="n">bind_new_task</span><span class="o"><</span><span class="n">T</span><span class="o">></span><span class="p">(</span>
<span class="n">me</span><span class="p">:</span> <span class="o">&</span><span class="nb">Arc</span><span class="o"><</span><span class="n">Self</span><span class="o">></span><span class="p">,</span>
<span class="n">future</span><span class="p">:</span> <span class="n">T</span><span class="p">,</span>
<span class="n">id</span><span class="p">:</span> <span class="nn">crate</span><span class="p">::</span><span class="nn">runtime</span><span class="p">::</span><span class="nn">task</span><span class="p">::</span><span class="n">Id</span><span class="p">,</span>
<span class="p">)</span> <span class="k">-></span> <span class="n">JoinHandle</span><span class="o"><</span><span class="nn">T</span><span class="p">::</span><span class="n">Output</span><span class="o">></span>
<span class="k">where</span>
<span class="n">T</span><span class="p">:</span> <span class="n">Future</span> <span class="o">+</span> <span class="nb">Send</span> <span class="o">+</span> <span class="nv">'static</span><span class="p">,</span>
<span class="nn">T</span><span class="p">::</span><span class="n">Output</span><span class="p">:</span> <span class="nb">Send</span> <span class="o">+</span> <span class="nv">'static</span><span class="p">,</span>
<span class="p">{</span>
<span class="c">// 省略无关代码, 任务调度</span>
<span class="n">me</span><span class="nf">.schedule</span><span class="p">(</span><span class="n">notified</span><span class="p">,</span> <span class="k">false</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">pub</span><span class="p">(</span><span class="n">super</span><span class="p">)</span> <span class="k">fn</span> <span class="nf">schedule</span><span class="p">(</span><span class="o">&</span><span class="k">self</span><span class="p">,</span> <span class="n">task</span><span class="p">:</span> <span class="n">Notified</span><span class="p">,</span> <span class="n">is_yield</span><span class="p">:</span> <span class="nb">bool</span><span class="p">)</span> <span class="p">{</span>
<span class="n">CURRENT</span><span class="nf">.with</span><span class="p">(|</span><span class="n">maybe_cx</span><span class="p">|</span> <span class="p">{</span>
<span class="k">if</span> <span class="k">let</span> <span class="nf">Some</span><span class="p">(</span><span class="n">cx</span><span class="p">)</span> <span class="o">=</span> <span class="n">maybe_cx</span> <span class="p">{</span>
<span class="c">// 判断当前 thread 中是否有 runtime 的上下文</span>
<span class="k">if</span> <span class="k">self</span><span class="nf">.ptr_eq</span><span class="p">(</span><span class="o">&</span><span class="n">cx</span><span class="py">.worker.shared</span><span class="p">)</span> <span class="p">{</span>
<span class="c">// 判断上下文中是否有 core 对象</span>
<span class="k">if</span> <span class="k">let</span> <span class="nf">Some</span><span class="p">(</span><span class="n">core</span><span class="p">)</span> <span class="o">=</span> <span class="n">cx</span><span class="py">.core</span><span class="nf">.borrow_mut</span><span class="p">()</span><span class="nf">.as_mut</span><span class="p">()</span> <span class="p">{</span>
<span class="c">// 优先把 task 在此线程的 worker 中进行调度</span>
<span class="k">self</span><span class="nf">.schedule_local</span><span class="p">(</span><span class="n">core</span><span class="p">,</span> <span class="n">task</span><span class="p">,</span> <span class="n">is_yield</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="c">// 否则加入全局任务队列</span>
<span class="k">self</span><span class="py">.inject</span><span class="nf">.push</span><span class="p">(</span><span class="n">task</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">}</span>
<span class="k">fn</span> <span class="nf">schedule_local</span><span class="p">(</span><span class="o">&</span><span class="k">self</span><span class="p">,</span> <span class="n">core</span><span class="p">:</span> <span class="o">&</span><span class="k">mut</span> <span class="n">Core</span><span class="p">,</span> <span class="n">task</span><span class="p">:</span> <span class="n">Notified</span><span class="p">,</span> <span class="n">is_yield</span><span class="p">:</span> <span class="nb">bool</span><span class="p">)</span> <span class="p">{</span>
<span class="c">// 这里的 is_yield 为 false 并且 config.disable_lifo_slot 未禁用 lifo_slot</span>
<span class="k">let</span> <span class="n">should_notify</span> <span class="o">=</span> <span class="k">if</span> <span class="n">is_yield</span> <span class="p">||</span> <span class="k">self</span><span class="py">.config.disable_lifo_slot</span> <span class="p">{</span>
<span class="n">core</span><span class="py">.run_queue</span>
<span class="nf">.push_back</span><span class="p">(</span><span class="n">task</span><span class="p">,</span> <span class="o">&</span><span class="k">self</span><span class="py">.inject</span><span class="p">,</span> <span class="o">&</span><span class="k">mut</span> <span class="n">core</span><span class="py">.metrics</span><span class="p">);</span>
<span class="k">true</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="c">// 获取 lifo_slot 槽中是否有任务</span>
<span class="k">let</span> <span class="n">prev</span> <span class="o">=</span> <span class="n">core</span><span class="py">.lifo_slot</span><span class="nf">.take</span><span class="p">();</span>
<span class="k">let</span> <span class="n">ret</span> <span class="o">=</span> <span class="n">prev</span><span class="nf">.is_some</span><span class="p">();</span>
<span class="c">// 如果有任务 把上一个任务推到 本地任务队列中</span>
<span class="c">// 并把当前任务放入到 lifo_slot 槽中</span>
<span class="k">if</span> <span class="k">let</span> <span class="nf">Some</span><span class="p">(</span><span class="n">prev</span><span class="p">)</span> <span class="o">=</span> <span class="n">prev</span> <span class="p">{</span>
<span class="n">core</span><span class="py">.run_queue</span>
<span class="nf">.push_back</span><span class="p">(</span><span class="n">prev</span><span class="p">,</span> <span class="o">&</span><span class="k">self</span><span class="py">.inject</span><span class="p">,</span> <span class="o">&</span><span class="k">mut</span> <span class="n">core</span><span class="py">.metrics</span><span class="p">);</span>
<span class="p">}</span>
<span class="c">// 放入到 lifo_slot 槽中</span>
<span class="n">core</span><span class="py">.lifo_slot</span> <span class="o">=</span> <span class="nf">Some</span><span class="p">(</span><span class="n">task</span><span class="p">);</span>
<span class="n">ret</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>我们在代码里进行了 <code class="language-plaintext highlighter-rouge">tokio::spawn</code> 操作, <code class="language-plaintext highlighter-rouge">tokio</code> 会把该任务放入到当前 <code class="language-plaintext highlighter-rouge">worker</code> 的 <code class="language-plaintext highlighter-rouge">FILO</code> 槽中, 而 <code class="language-plaintext highlighter-rouge">FILO</code> 槽中的任务是无法被其他 <code class="language-plaintext highlighter-rouge">worker</code> 所窃取的, 所以此任务要被执行要等当前执行的线程让出执行执行权然后 <code class="language-plaintext highlighter-rouge">worker</code> 重新轮训任务才会得到执行. 而我们这里直接一个 <code class="language-plaintext highlighter-rouge">tx.receive()</code> 这里的 <code class="language-plaintext highlighter-rouge">receive</code> 非 <code class="language-plaintext highlighter-rouge">tokio</code> 库中的 <code class="language-plaintext highlighter-rouge">receive</code> 而是标准库中的, 所以这里无法让 <code class="language-plaintext highlighter-rouge">worker</code> 回到 <code class="language-plaintext highlighter-rouge">tokio::rutnime</code> 中去重新执行任务, 只能在这里苦苦等待 <code class="language-plaintext highlighter-rouge">receive</code> 结果. 这个等待是不会有结果的, 因为与 <code class="language-plaintext highlighter-rouge">receive</code> 对应的 <code class="language-plaintext highlighter-rouge">send</code> 方法是永远不会被执行的.</p>
<h1 id="四runtime-被卡死">四、Runtime 被卡死</h1>
<p><code class="language-plaintext highlighter-rouge">runtime</code> 被卡死的问题还是得回到 <code class="language-plaintext highlighter-rouge">tokio</code> 的任务调度中来, 所以还是来看看 <code class="language-plaintext highlighter-rouge">tokio</code> 任务调度的代码吧.</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// 每个 worker 对应一个 Context</span>
<span class="k">impl</span> <span class="n">Context</span> <span class="p">{</span>
<span class="k">fn</span> <span class="nf">run</span><span class="p">(</span><span class="o">&</span><span class="k">self</span><span class="p">,</span> <span class="k">mut</span> <span class="n">core</span><span class="p">:</span> <span class="nb">Box</span><span class="o"><</span><span class="n">Core</span><span class="o">></span><span class="p">)</span> <span class="k">-></span> <span class="n">RunResult</span> <span class="p">{</span>
<span class="c">// Core 是 worker 的核心数据结构 包含很多任务信息</span>
<span class="c">// Core 里包含了 lifo_slot(用于减少任务调度延迟的 lifo_slot 优先执行 lifo_slot 中的任务) 和 run_queue (worker 专属的任务队列)</span>
<span class="k">while</span> <span class="o">!</span><span class="n">core</span><span class="py">.is_shutdown</span> <span class="p">{</span>
<span class="c">// Increment the tick</span>
<span class="c">// 每循环一次增加一次 tick</span>
<span class="c">// 改 tick 是用于判断是否需要强制 park 等待 readiness 事件</span>
<span class="n">core</span><span class="nf">.tick</span><span class="p">();</span>
<span class="c">// 当 tick 达到 31 的整数倍 则强制 park 等待 readiness 事件</span>
<span class="n">core</span> <span class="o">=</span> <span class="k">self</span><span class="nf">.maintenance</span><span class="p">(</span><span class="n">core</span><span class="p">);</span>
<span class="c">// 获取任务的优先级如下:</span>
<span class="c">// lifo_slot -> run_queue -> inject_queue</span>
<span class="c">// 如果 tick 达到 61 的整数倍 优先级如下</span>
<span class="c">// inject_queue -> lifo_slot -> run_queue</span>
<span class="k">if</span> <span class="k">let</span> <span class="nf">Some</span><span class="p">(</span><span class="n">task</span><span class="p">)</span> <span class="o">=</span> <span class="n">core</span><span class="nf">.next_task</span><span class="p">(</span><span class="o">&</span><span class="k">self</span><span class="py">.worker</span><span class="p">)</span> <span class="p">{</span>
<span class="n">core</span> <span class="o">=</span> <span class="k">self</span><span class="nf">.run_task</span><span class="p">(</span><span class="n">task</span><span class="p">,</span> <span class="n">core</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>
<span class="k">continue</span><span class="p">;</span>
<span class="p">}</span>
<span class="c">// 若 worker 处于空闲状态则 窃取 其他 worker 的任务</span>
<span class="k">if</span> <span class="k">let</span> <span class="nf">Some</span><span class="p">(</span><span class="n">task</span><span class="p">)</span> <span class="o">=</span> <span class="n">core</span><span class="nf">.steal_work</span><span class="p">(</span><span class="o">&</span><span class="k">self</span><span class="py">.worker</span><span class="p">)</span> <span class="p">{</span>
<span class="n">core</span> <span class="o">=</span> <span class="k">self</span><span class="nf">.run_task</span><span class="p">(</span><span class="n">task</span><span class="p">,</span> <span class="n">core</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="c">// 若未能 窃取到任务 则进行 park 等待 readiness 事件</span>
<span class="n">core</span> <span class="o">=</span> <span class="k">if</span> <span class="nf">did_defer_tasks</span><span class="p">()</span> <span class="p">{</span>
<span class="k">self</span><span class="nf">.park_timeout</span><span class="p">(</span><span class="n">core</span><span class="p">,</span> <span class="nf">Some</span><span class="p">(</span><span class="nn">Duration</span><span class="p">::</span><span class="nf">from_millis</span><span class="p">(</span><span class="mi">0</span><span class="p">)))</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">self</span><span class="nf">.park</span><span class="p">(</span><span class="n">core</span><span class="p">)</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="n">core</span><span class="nf">.pre_shutdown</span><span class="p">(</span><span class="o">&</span><span class="k">self</span><span class="py">.worker</span><span class="p">);</span>
<span class="c">// Signal shutdown</span>
<span class="k">self</span><span class="py">.worker.handle</span><span class="nf">.shutdown_core</span><span class="p">(</span><span class="n">core</span><span class="p">);</span>
<span class="nf">Err</span><span class="p">(())</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>以上代码就是 <code class="language-plaintext highlighter-rouge">tokio</code> 执行任务的核心逻辑, 回到 <code class="language-plaintext highlighter-rouge">Runtime</code> 被卡死的问题上, 这里导致问题的是 <code class="language-plaintext highlighter-rouge">park</code> 操作, 因为我们是使用 <code class="language-plaintext highlighter-rouge">tokio::sleep</code> , 在还没到达唤醒时间时, <code class="language-plaintext highlighter-rouge">worker</code> 处于 <code class="language-plaintext highlighter-rouge">park </code> 状态.</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">impl</span> <span class="n">Inner</span> <span class="p">{</span>
<span class="k">fn</span> <span class="nf">park</span><span class="p">(</span><span class="o">&</span><span class="k">self</span><span class="p">,</span> <span class="n">handle</span><span class="p">:</span> <span class="o">&</span><span class="nn">driver</span><span class="p">::</span><span class="n">Handle</span><span class="p">)</span> <span class="p">{</span>
<span class="c">// 省略代码</span>
<span class="k">if</span> <span class="k">let</span> <span class="nf">Some</span><span class="p">(</span><span class="k">mut</span> <span class="n">driver</span><span class="p">)</span> <span class="o">=</span> <span class="k">self</span><span class="py">.shared.driver</span><span class="nf">.try_lock</span><span class="p">()</span> <span class="p">{</span>
<span class="k">self</span><span class="nf">.park_driver</span><span class="p">(</span><span class="o">&</span><span class="k">mut</span> <span class="n">driver</span><span class="p">,</span> <span class="n">handle</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">self</span><span class="nf">.park_condvar</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>上面的代码表示, 只能有一个 <code class="language-plaintext highlighter-rouge">worker</code> 能获得 <code class="language-plaintext highlighter-rouge">IO Driver</code> 资源, 其余的 <code class="language-plaintext highlighter-rouge">worker</code> 只能被 <code class="language-plaintext highlighter-rouge">park_condvar</code>, 而这 2 种 <code class="language-plaintext highlighter-rouge">park</code> 的区别在于, <code class="language-plaintext highlighter-rouge">park_driver</code> 能靠底层 <code class="language-plaintext highlighter-rouge">epoll</code> 或者 <code class="language-plaintext highlighter-rouge">kqueue</code> 机制被唤醒, 而 <code class="language-plaintext highlighter-rouge">park_condvar</code> 只能被其他的 ` worker` 唤醒. 所以被卡死的流程如下:</p>
<ul>
<li>所有 <code class="language-plaintext highlighter-rouge">worker</code> 因为 <code class="language-plaintext highlighter-rouge">sleep</code> 被 <code class="language-plaintext highlighter-rouge">park</code> 住处于休眠状态, 其中一个 <code class="language-plaintext highlighter-rouge">worker</code> 持有 <code class="language-plaintext highlighter-rouge">IO Driver</code> 资源</li>
<li>当等待时间达到唤醒时间时, 持有 <code class="language-plaintext highlighter-rouge">IO Driver</code> 的 <code class="language-plaintext highlighter-rouge">worker</code> 会被 <code class="language-plaintext highlighter-rouge">epoll</code>或者<code class="language-plaintext highlighter-rouge">kqueue</code> 事件机制给唤醒</li>
<li><code class="language-plaintext highlighter-rouge">worker</code> 唤醒之后执行任务队列中的任务, 并唤醒其他的 <code class="language-plaintext highlighter-rouge">worker</code> , 如此往复直到 <code class="language-plaintext highlighter-rouge">loop task 3</code> 执行第四次时情况开始发生变化</li>
<li>因为 <code class="language-plaintext highlighter-rouge">loop task 3</code> 的 <code class="language-plaintext highlighter-rouge">sleep </code> 时间短, 先于其他几个 <code class="language-plaintext highlighter-rouge">worker</code> 被唤醒, 唤醒之后由于没有其他的任务执行, 也没有去唤醒其他 <code class="language-plaintext highlighter-rouge">worker</code>, 然后执行自己的 <code class="language-plaintext highlighter-rouge">task</code>, 这里执行 <code class="language-plaintext highlighter-rouge">task</code> 就会被 <code class="language-plaintext highlighter-rouge">rx.recive</code> 给永远阻塞住(具体原因往上看)</li>
<li>由于这个唯一活着的 <code class="language-plaintext highlighter-rouge">worker</code> 被永远阻塞住了, 所以就算其余的 <code class="language-plaintext highlighter-rouge">task</code> 唤醒条件达到了也无法被唤醒然后继续执行, 那么我们的 <code class="language-plaintext highlighter-rouge">Runtime</code> 卡死目标达成</li>
</ul>
<p>这里的情况与这个 ISSUE 类似: https://github.com/tokio-rs/tokio/issues/4730</p>
<h1 id="五总结">五、总结</h1>
<p>在 <code class="language-plaintext highlighter-rouge">tokio</code> 中尽量使用 <code class="language-plaintext highlighter-rouge">tokio</code> 中提供的 <code class="language-plaintext highlighter-rouge">channel</code> 和各种锁, 避免出现这种情况, 主要是这种情况并不好排查(本人能力有限), 如果有爱好 rust 的朋友, 欢迎一起来共建 <a href="https://github.com/nacos-group/nacos-sdk-rust">nacos-sdk-rust</a> 来练手</p>{"nick"=>"onew", "link"=>"https://onew.me"}一、前言 最近在忙着做 nacos-sdk-rust 3.x 版本的重构, 在重构的过程中一切都很顺利, 但是后面的验证过程中, 发现程序每隔 30 分钟就会卡死, 心跳也不发了. 这就很奇怪了. 于是就研究了一下 tokio 的调度机制. 二、给你一段卡死的代码 #[tokio::test(flavor = "multi_thread", worker_threads = 8)] pub async fn test_switch() { let mut handles = Vec::new(); handles.push(tokio::spawn({ async { loop { println!("{:?}: loop task 1", thread::current().id()); tokio::time::sleep(Duration::from_secs(10)).await; } } })); handles.push(tokio::spawn({ async { loop { println!("{:?}: loop task 2", thread::current().id()); tokio::time::sleep(Duration::from_secs(10)).await; } } })); handles.push(tokio::spawn({ async { let mut count = 0; loop { println!("{:?}: loop task 3, task count: {}", thread::current().id(), count); if count > 3 { let (tx, rx) = std::sync::mpsc::channel::<String>(); tokio::spawn(async move{ let _ = tx.send("send message".to_string()); }); let ret = rx.recv().unwrap(); println!("receive message :{} , task count :{}", ret, count); } tokio::time::sleep(Duration::from_secs(5)).await; count += 1; } } })); for handle in handles { handle.await.expect("handle await"); } } 在遇到定时执行任务的情况, 我相信大多数人的写法都和上面的写法差不多. 看着好像没啥问题对吧. 下面是运行日志 running 1 test ThreadId(10): loop task 1 ThreadId(7): loop task 3, task count: 0 ThreadId(9): loop task 2 ThreadId(10): loop task 3, task count: 1 ThreadId(10): loop task 3, task count: 2 ThreadId(6): loop task 1 ThreadId(10): loop task 2 ThreadId(4): loop task 3, task count: 3 ThreadId(4): loop task 1 ThreadId(10): loop task 2 ThreadId(4): loop task 3, task count: 4 在程序跑到 ThreadId(4): loop task 3, task count: 4 时后面的任务就不执行了, 好像所有的任务都暂停了一样. 是的, 我们成功的把 tokio 的 wroker 全部卡死了. 从这里引出 2 个问题: 为什么 let ret = rx.recv().unwrap(); 这行代码为什么被阻塞, let _ = tx.send("send message".to_string()); 没有被执行吗? 为什么其他定时任务不执行了? 三、被阻塞的代码 let (tx, rx) = std::sync::mpsc::channel::<String>(); tokio::spawn(async move{ let _ = tx.send("send message".to_string()); }); let ret = rx.recv().unwrap(); 这里明明通过 tokio::spawn 派生了一个任务, 为什么不执行呢? 这里就要看看 tokio 的任务调度逻辑了 #[track_caller] pub fn spawn<T>(future: T) -> JoinHandle<T::Output> where T: Future + Send + 'static, T::Output: Send + 'static, { // preventing stack overflows on debug mode, by quickly sending the // task to the heap. // 省略无关的代码 .. spawn_inner(future, None) } #[track_caller] pub(super) fn spawn_inner<T>(future: T, name: Option<&str>) -> JoinHandle<T::Output> where T: Future + Send + 'static, T::Output: Send + 'static, { use crate::runtime::{task, context}; // 生产 task id let id = task::Id::next(); // 获取当前上下文中的 Spawner let spawn_handle = context::spawn_handle().expect(CONTEXT_MISSING_ERROR); // 创建 task let task = crate::util::trace::task(future, "task", name, id.as_u64()); spawn_handle.spawn(task, id) } // 重点在 spawner 这里 impl Spawner { /// Spawns a future onto the thread pool pub(crate) fn spawn<F>(&self, future: F, id: task::Id) -> JoinHandle<F::Output> where F: crate::future::Future + Send + 'static, F::Output: Send + 'static, { // 绑定一个 task worker::Shared::bind_new_task(&self.shared, future, id) } } impl Shared { pub(super) fn bind_new_task<T>( me: &Arc<Self>, future: T, id: crate::runtime::task::Id, ) -> JoinHandle<T::Output> where T: Future + Send + 'static, T::Output: Send + 'static, { // 省略无关代码, 任务调度 me.schedule(notified, false); } pub(super) fn schedule(&self, task: Notified, is_yield: bool) { CURRENT.with(|maybe_cx| { if let Some(cx) = maybe_cx { // 判断当前 thread 中是否有 runtime 的上下文 if self.ptr_eq(&cx.worker.shared) { // 判断上下文中是否有 core 对象 if let Some(core) = cx.core.borrow_mut().as_mut() { // 优先把 task 在此线程的 worker 中进行调度 self.schedule_local(core, task, is_yield); return; } } } // 否则加入全局任务队列 self.inject.push(task); }) } fn schedule_local(&self, core: &mut Core, task: Notified, is_yield: bool) { // 这里的 is_yield 为 false 并且 config.disable_lifo_slot 未禁用 lifo_slot let should_notify = if is_yield || self.config.disable_lifo_slot { core.run_queue .push_back(task, &self.inject, &mut core.metrics); true } else { // 获取 lifo_slot 槽中是否有任务 let prev = core.lifo_slot.take(); let ret = prev.is_some(); // 如果有任务 把上一个任务推到 本地任务队列中 // 并把当前任务放入到 lifo_slot 槽中 if let Some(prev) = prev { core.run_queue .push_back(prev, &self.inject, &mut core.metrics); } // 放入到 lifo_slot 槽中 core.lifo_slot = Some(task); ret }; } } 我们在代码里进行了 tokio::spawn 操作, tokio 会把该任务放入到当前 worker 的 FILO 槽中, 而 FILO 槽中的任务是无法被其他 worker 所窃取的, 所以此任务要被执行要等当前执行的线程让出执行执行权然后 worker 重新轮训任务才会得到执行. 而我们这里直接一个 tx.receive() 这里的 receive 非 tokio 库中的 receive 而是标准库中的, 所以这里无法让 worker 回到 tokio::rutnime 中去重新执行任务, 只能在这里苦苦等待 receive 结果. 这个等待是不会有结果的, 因为与 receive 对应的 send 方法是永远不会被执行的. 四、Runtime 被卡死 runtime 被卡死的问题还是得回到 tokio 的任务调度中来, 所以还是来看看 tokio 任务调度的代码吧. // 每个 worker 对应一个 Context impl Context { fn run(&self, mut core: Box<Core>) -> RunResult { // Core 是 worker 的核心数据结构 包含很多任务信息 // Core 里包含了 lifo_slot(用于减少任务调度延迟的 lifo_slot 优先执行 lifo_slot 中的任务) 和 run_queue (worker 专属的任务队列) while !core.is_shutdown { // Increment the tick // 每循环一次增加一次 tick // 改 tick 是用于判断是否需要强制 park 等待 readiness 事件 core.tick(); // 当 tick 达到 31 的整数倍 则强制 park 等待 readiness 事件 core = self.maintenance(core); // 获取任务的优先级如下: // lifo_slot -> run_queue -> inject_queue // 如果 tick 达到 61 的整数倍 优先级如下 // inject_queue -> lifo_slot -> run_queue if let Some(task) = core.next_task(&self.worker) { core = self.run_task(task, core)?; continue; } // 若 worker 处于空闲状态则 窃取 其他 worker 的任务 if let Some(task) = core.steal_work(&self.worker) { core = self.run_task(task, core)?; } else { // 若未能 窃取到任务 则进行 park 等待 readiness 事件 core = if did_defer_tasks() { self.park_timeout(core, Some(Duration::from_millis(0))) } else { self.park(core) }; } } core.pre_shutdown(&self.worker); // Signal shutdown self.worker.handle.shutdown_core(core); Err(()) } } 以上代码就是 tokio 执行任务的核心逻辑, 回到 Runtime 被卡死的问题上, 这里导致问题的是 park 操作, 因为我们是使用 tokio::sleep , 在还没到达唤醒时间时, worker 处于 park 状态. impl Inner { fn park(&self, handle: &driver::Handle) { // 省略代码 if let Some(mut driver) = self.shared.driver.try_lock() { self.park_driver(&mut driver, handle); } else { self.park_condvar(); } } } 上面的代码表示, 只能有一个 worker 能获得 IO Driver 资源, 其余的 worker 只能被 park_condvar, 而这 2 种 park 的区别在于, park_driver 能靠底层 epoll 或者 kqueue 机制被唤醒, 而 park_condvar 只能被其他的 ` worker` 唤醒. 所以被卡死的流程如下: 所有 worker 因为 sleep 被 park 住处于休眠状态, 其中一个 worker 持有 IO Driver 资源 当等待时间达到唤醒时间时, 持有 IO Driver 的 worker 会被 epoll或者kqueue 事件机制给唤醒 worker 唤醒之后执行任务队列中的任务, 并唤醒其他的 worker , 如此往复直到 loop task 3 执行第四次时情况开始发生变化 因为 loop task 3 的 sleep 时间短, 先于其他几个 worker 被唤醒, 唤醒之后由于没有其他的任务执行, 也没有去唤醒其他 worker, 然后执行自己的 task, 这里执行 task 就会被 rx.recive 给永远阻塞住(具体原因往上看) 由于这个唯一活着的 worker 被永远阻塞住了, 所以就算其余的 task 唤醒条件达到了也无法被唤醒然后继续执行, 那么我们的 Runtime 卡死目标达成 这里的情况与这个 ISSUE 类似: https://github.com/tokio-rs/tokio/issues/4730 五、总结 在 tokio 中尽量使用 tokio 中提供的 channel 和各种锁, 避免出现这种情况, 主要是这种情况并不好排查(本人能力有限), 如果有爱好 rust 的朋友, 欢迎一起来共建 nacos-sdk-rust 来练手nacos 客户端 bug 导致服务批量下线2021-10-19T09:29:12+08:002021-10-19T09:29:12+08:00https://onew.me/java/2021/10/19/nacos-net-bug<h1 id="一前言">一、前言</h1>
<p> 在某个凌晨,本来打算睡觉的时候.发现工作群里边的变得热闹了起来.”服务挂了”,“一个机房的服务全部挂了”,“网络问题吧.”好不热闹.心想反正不关我事,睡觉吧,猝死了可不好.</p>
<p> 好巧不巧,第二天上班的时候,这个问题居然安排我去调查.倒霉了.</p>
<p> 生产环境:</p>
<table>
<thead>
<tr>
<th>Nacos-server</th>
<th>Nacos-client</th>
<th> </th>
</tr>
</thead>
<tbody>
<tr>
<td>1.3.1</td>
<td>1.4.1</td>
<td> </td>
</tr>
</tbody>
</table>
<p> 去案发现场看了一下,服务并没有挂,但在 nacos 控制里看服务却下线了.下线原因是没有发送心跳,超过15秒后,server 端就把这个服务给踢了.那么排查的方向就确定了,查一查 nacos 客户端为啥没有发送心跳.</p>
<p> 简单的看了一下日志,发现没有心跳相关的错误.看来这个问题还得看看客户端的源码才能解决.</p>
<h1 id="二源码分析-发送心跳">二、源码分析-发送心跳</h1>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/uaB3AZ.png" alt="uaB3AZ" /></p>
<p> 发送心跳的核心代码就在 <code class="language-plaintext highlighter-rouge">BeatReactor</code> 类里,使用了一个 <code class="language-plaintext highlighter-rouge">ScheduledThreadPoolExecutor</code> 来定时发送心跳.如果没有发送心跳,也就意味着 <code class="language-plaintext highlighter-rouge">ScheduledThreadPoolExecutor</code> 没有任务运行.</p>
<p> <img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/wbNy8c.png" alt="wbNy8c" /></p>
<p> 这块代码是发送心跳的核心代码,如果说不继续发送心跳,那么只能是出现了未捕获的异常(非<code class="language-plaintext highlighter-rouge">NacosException</code>),导致没有走到 <code class="language-plaintext highlighter-rouge">executorService.schedule</code> 这句代码来.</p>
<p> 翻来覆去找了一下,发现可能是解析 ip 地址判断是否是 ipv4 的时候报错.这个错误,nacos 是没有捕获到的,满足上面的推测.</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/7ZyS3Z.png" alt="7ZyS3Z" /></p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/AxjD4j.png" alt="AxjD4j" /></p>
<h1 id="三问题复现">三、问题复现</h1>
先来快速的搭建个环境吧. Nacos 官方提供了 nacos-server 构建 docker 镜像的教程. <code class="language-plaintext highlighter-rouge">https://nacos.io/zh-cn/docs/quick-start-docker.html</code> 根据官方的指南构建一个 1.3.1 的 nacos-server 镜像.
<p> 构建 nacos 1.4.1 和 1.42 版本的客户端,得整个 java 程序.</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><?xml version="1.0" encoding="UTF-8"?></span>
<span class="nt"><project</span> <span class="na">xmlns=</span><span class="s">"http://maven.apache.org/POM/4.0.0"</span>
<span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span>
<span class="na">xsi:schemaLocation=</span><span class="s">"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"</span><span class="nt">></span>
<span class="nt"><modelVersion></span>4.0.0<span class="nt"></modelVersion></span>
<span class="nt"><groupId></span>org.example<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>nacos-naming-test<span class="nt"></artifactId></span>
<span class="nt"><version></span>1.0-SNAPSHOT<span class="nt"></version></span>
<span class="nt"><properties></span>
<span class="nt"><maven.compiler.source></span>8<span class="nt"></maven.compiler.source></span>
<span class="nt"><maven.compiler.target></span>8<span class="nt"></maven.compiler.target></span>
<span class="nt"></properties></span>
<span class="nt"><parent></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-starter-parent<span class="nt"></artifactId></span>
<span class="nt"><version></span>2.2.5.RELEASE<span class="nt"></version></span>
<span class="nt"><relativePath/></span>
<span class="nt"></parent></span>
<span class="nt"><dependencyManagement></span>
<span class="nt"><dependencies></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>com.alibaba.cloud<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-cloud-alibaba-dependencies<span class="nt"></artifactId></span>
<span class="c"><!-- 2.2.5使用的是 1.4.1 --></span>
<span class="c"><!-- 2.2.6使用的是 1.4.2 --></span>
<span class="nt"><version></span>2.2.6.RELEASE<span class="nt"></version></span>
<span class="nt"><type></span>pom<span class="nt"></type></span>
<span class="nt"><scope></span>import<span class="nt"></scope></span>
<span class="nt"></dependency></span>
<span class="nt"></dependencies></span>
<span class="nt"></dependencyManagement></span>
<span class="nt"><dependencies></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>com.alibaba.cloud<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-cloud-starter-alibaba-nacos-discovery<span class="nt"></artifactId></span>
<span class="nt"></dependency></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-starter-web<span class="nt"></artifactId></span>
<span class="nt"></dependency></span>
<span class="nt"></dependencies></span>
<span class="nt"><build></span>
<span class="nt"><plugins></span>
<span class="nt"><plugin></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-maven-plugin<span class="nt"></artifactId></span>
<span class="nt"></plugin></span>
<span class="nt"></plugins></span>
<span class="nt"></build></span>
<span class="nt"></project></span>
</code></pre></div></div>
<p> java 代码</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">org.example</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.boot.SpringApplication</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.boot.autoconfigure.SpringBootApplication</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.cloud.client.discovery.EnableDiscoveryClient</span><span class="o">;</span>
<span class="nd">@SpringBootApplication</span>
<span class="nd">@EnableDiscoveryClient</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="n">application</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">SpringApplication</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">application</span><span class="o">.</span><span class="na">run</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> Dockerfile</p>
<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> openjdk:8u302-jdk</span>
<span class="k">ADD</span><span class="s"> nacos-naming-testjar .</span>
<span class="k">CMD</span><span class="s"> ["java","-jar","nacos-naming-test.jar"]</span>
</code></pre></div></div>
<p> 构建 2 个版本,一个版本使用的是 1.4.1 另一个版本是 1.4.2</p>
<p> 编写 docker-compose.yaml</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3.3'</span>
<span class="na">services</span><span class="pi">:</span>
<span class="na">nacos-server</span><span class="pi">:</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">nacos/nacos:${NACOS_SERVER_VERSION}</span>
<span class="na">container_name</span><span class="pi">:</span> <span class="s">nacos</span>
<span class="na">env_file</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">./env/nacos-standlone-mysql.env</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s2">"</span><span class="s">8848:8848"</span>
<span class="pi">-</span> <span class="s2">"</span><span class="s">9848:9848"</span>
<span class="pi">-</span> <span class="s2">"</span><span class="s">9555:9555"</span>
<span class="na">depends_on</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">mysql</span>
<span class="na">healthcheck</span><span class="pi">:</span>
<span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">CMD"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">curl"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">-f"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">http://localhost:8848/nacos/"</span><span class="pi">]</span>
<span class="na">interval</span><span class="pi">:</span> <span class="s">30s</span>
<span class="na">timeout</span><span class="pi">:</span> <span class="s">10s</span>
<span class="na">retries</span><span class="pi">:</span> <span class="m">3</span>
<span class="na">start_period</span><span class="pi">:</span> <span class="s">40s</span>
<span class="na">mysql</span><span class="pi">:</span>
<span class="na">container_name</span><span class="pi">:</span> <span class="s">mysql</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">nacos/nacos-mysql:5.7</span>
<span class="na">env_file</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">./env/mysql.env</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s2">"</span><span class="s">3306:3306"</span>
<span class="na">nacos-naming-test</span><span class="pi">:</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">nacos-naming-test:v1.1.0</span>
<span class="na">deploy</span><span class="pi">:</span>
<span class="na">mode</span><span class="pi">:</span> <span class="s">replicated</span>
<span class="na">replicas</span><span class="pi">:</span> <span class="m">3</span>
<span class="na">depends_on</span><span class="pi">:</span>
<span class="na">nacos-server</span><span class="pi">:</span>
<span class="na">condition</span><span class="pi">:</span> <span class="s">service_healthy</span>
</code></pre></div></div>
<p> 模拟网络的波动,可以通过启停 nacos-server 来达到预期的效果.通过验证 1.4.2 版本的客户端是没有问题的,在 nacos-server 重启之后能够重新注册上服务,但 1.4.1 版本的客户端却不可以.</p>{"nick"=>"onew", "link"=>"https://onew.me"}一、前言 在某个凌晨,本来打算睡觉的时候.发现工作群里边的变得热闹了起来.”服务挂了”,“一个机房的服务全部挂了”,“网络问题吧.”好不热闹.心想反正不关我事,睡觉吧,猝死了可不好. 好巧不巧,第二天上班的时候,这个问题居然安排我去调查.倒霉了. 生产环境: Nacos-server Nacos-client 1.3.1 1.4.1 去案发现场看了一下,服务并没有挂,但在 nacos 控制里看服务却下线了.下线原因是没有发送心跳,超过15秒后,server 端就把这个服务给踢了.那么排查的方向就确定了,查一查 nacos 客户端为啥没有发送心跳. 简单的看了一下日志,发现没有心跳相关的错误.看来这个问题还得看看客户端的源码才能解决. 二、源码分析-发送心跳 发送心跳的核心代码就在 BeatReactor 类里,使用了一个 ScheduledThreadPoolExecutor 来定时发送心跳.如果没有发送心跳,也就意味着 ScheduledThreadPoolExecutor 没有任务运行. 这块代码是发送心跳的核心代码,如果说不继续发送心跳,那么只能是出现了未捕获的异常(非NacosException),导致没有走到 executorService.schedule 这句代码来. 翻来覆去找了一下,发现可能是解析 ip 地址判断是否是 ipv4 的时候报错.这个错误,nacos 是没有捕获到的,满足上面的推测. 三、问题复现 先来快速的搭建个环境吧. Nacos 官方提供了 nacos-server 构建 docker 镜像的教程. https://nacos.io/zh-cn/docs/quick-start-docker.html 根据官方的指南构建一个 1.3.1 的 nacos-server 镜像. 构建 nacos 1.4.1 和 1.42 版本的客户端,得整个 java 程序. <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>nacos-naming-test</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> <relativePath/> </parent> <dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <!-- 2.2.5使用的是 1.4.1 --> <!-- 2.2.6使用的是 1.4.2 --> <version>2.2.6.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> java 代码 package org.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication @EnableDiscoveryClient public class Application { public static void main(String[] args) { SpringApplication application = new SpringApplication(Application.class); application.run(args); } } Dockerfile FROM openjdk:8u302-jdk ADD nacos-naming-testjar . CMD ["java","-jar","nacos-naming-test.jar"] 构建 2 个版本,一个版本使用的是 1.4.1 另一个版本是 1.4.2 编写 docker-compose.yaml version: '3.3' services: nacos-server: image: nacos/nacos:${NACOS_SERVER_VERSION} container_name: nacos env_file: - ./env/nacos-standlone-mysql.env ports: - "8848:8848" - "9848:9848" - "9555:9555" depends_on: - mysql healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8848/nacos/"] interval: 30s timeout: 10s retries: 3 start_period: 40s mysql: container_name: mysql image: nacos/nacos-mysql:5.7 env_file: - ./env/mysql.env ports: - "3306:3306" nacos-naming-test: image: nacos-naming-test:v1.1.0 deploy: mode: replicated replicas: 3 depends_on: nacos-server: condition: service_healthy 模拟网络的波动,可以通过启停 nacos-server 来达到预期的效果.通过验证 1.4.2 版本的客户端是没有问题的,在 nacos-server 重启之后能够重新注册上服务,但 1.4.1 版本的客户端却不可以.docker 模拟双机房多网段通信2021-09-06T07:53:25+08:002021-09-06T07:53:25+08:00https://onew.me/docker/2021/09/06/docker-network-IDC<h1 id="一前言">一、前言</h1>
<p> 最近在搞 redis 双活,在折腾 redis 双活的模拟环境,考虑到生产环境是不同的机房,就萌生了使用 docker 来模拟多机房不同网段的理想环境.</p>
<p> docker 网络是采用桥接完成,会有一张 docker0 的网卡,每个容器都是在 docker0 这张网卡下.我记得网上有张描述 docker 网络通信的图,这里就不放出来,随便百度一下就有的东西.总之,明白网络通信的原理是很重要的.</p>
<h1 id="二干活">二、干活</h1>
<p> 由于是在 mac 系统下干活,想要模拟还得使用 docker-machine 来搞.以下会涉及到 docker-machine 命令,但核心部分还是在设置网络上,设置网络的命令是没有任何差异的.</p>
<h2 id="21-使用-docker-machine-创建一个跑-docker-的虚拟机">2.1 使用 docker-machine 创建一个跑 docker 的虚拟机</h2>
<p>创建一个名称为: network-lab 的虚拟机</p>
<p><code class="language-plaintext highlighter-rouge">docker-machine create -d=vmware network-lab</code></p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/qGaruh.png" alt="qGaruh" /></p>
<p>创建成功.</p>
<h2 id="22-创建-2-个不同的网段的网络">2.2 创建 2 个不同的网段的网络</h2>
<p>创建一个名称为北京的网络: bj-net</p>
<p><code class="language-plaintext highlighter-rouge">docker network create -d bridge --subnet 192.168.9.0/24 --gateway 192.168.9.1 bj-net</code></p>
<p>创建一个名称为上海的网络: sh-net</p>
<p><code class="language-plaintext highlighter-rouge">docker network create -d bridge --subnet 192.168.10.0/24 --gateway 192.168.10.1 sh-net</code></p>
<p>查看网络是否创建成功:</p>
<p><code class="language-plaintext highlighter-rouge">docker network ls</code></p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/dE9bo5.png" alt="dE9bo5" /></p>
<p>创建2个容器测试网络</p>
<p>使用北京网络运行容器: <code class="language-plaintext highlighter-rouge">docker run --rm -it --network bj-net busybox:latest</code></p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/Vv0oGQ.png" alt="Vv0oGQ" /></p>
<p>使用北京网络的容器网络情况:</p>
<p>网关: 192.168.9.1</p>
<p>ip地址: 192.168.9.2</p>
<p>使用上海网络运行容器: <code class="language-plaintext highlighter-rouge">docker run --rm -it --network bj-net busybox:latest</code></p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/X08BC9.png" alt="X08BC9" /></p>
<p>使用上海网络的容器网络情况:</p>
<p>网关: 192.168.10.1</p>
<p>ip地址: 192.168.10.2</p>
<p>现在这种网络情况是无法 ping 通的</p>
<p>北京 ping 上海</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/SUQLzz.png" alt="SUQLzz" /></p>
<p>上海 ping 北京</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/UXh6Wx.png" alt="UXh6Wx" /></p>
<p> 那么这种情况怎么解决呢?按照 docker 网络通信的原理,宿主机上必定是有 2 张网卡的,所以只要开启转发,就能把不同网段发过来的数据包转到对应的网卡,这样就能实现互通了.</p>
<h2 id="23-互通有无">2.3 互通有无</h2>
<p> 在宿主机上使用 ifconfig 可以看到北京和上海的2张网卡</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/4CvTmh.png" alt="4CvTmh" /></p>
<p>先查看一下 route 信息,看看有没有进行默认的配置</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/V3zYqu.png" alt="V3zYqu" /></p>
<p>通过 route 命令可以看到 9段和10段的包会到9段和10段的网卡上去,这里路由的配置是没有问题.</p>
<p>查看一下主机是否开启转发,默认情况下应该是开启了的,这里确认一下</p>
<p><code class="language-plaintext highlighter-rouge">cat /proc/sys/net/ipv4/ip_forward</code></p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/EFcpDh.png" alt="EFcpDh" /></p>
<p>删除 docker 针对于我们自定义的2个网卡的默认的 iptables 规则.</p>
<p>查看 iptables 中 filter 表中的所有规则: <code class="language-plaintext highlighter-rouge">sudo iptables -t filter -nvL</code></p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/vMqoI4.png" alt="vMqoI4" /></p>
<p>可以看到在 INPUT链 和 OUTPUT链中是没有规则配置的,那么这里只需要更改 FORWARD 链的规则即可,由于虚拟的原因,这里截图不能截全.</p>
<p>通过查看 docker 的自定义链,发现是在 DOCKER-ISOLATION-STAGE-2 里定义了 北京和上海 2张网卡的规则.</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/Wt3MQP.png" alt="Wt3MQP" /></p>
<p>这里只要删除这2条规则,不同网段的通信就通了.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo iptables -t filter -D DOCKER-ISOLATION-STAGE-2 -o br-7703ed4f8946 -j DROP
sudo iptables -t filter -D DOCKER-ISOLATION-STAGE-2 -o br-109ff1ea9a3d -j DROP
</code></pre></div></div>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/mLMQ2n.png" alt="mLMQ2n" /></p>
<p>ok~已经删干净了,试一试看看能不能成功.</p>
<p>上海 ping 北京</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/Ryycc2.png" alt="Ryycc2" /></p>
<p>北京 ping 上海</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/zB60DX.png" alt="zB60DX" /></p>
<p>搞定</p>
<h1 id="四总结">四、总结</h1>
<p> 总体来说不是很复杂,只要了解 docker 网络通信原理和 iptables 的使用,问题不大.吃饭了~</p>
<p></p>{"nick"=>"onew", "link"=>"https://onew.me"}一、前言 最近在搞 redis 双活,在折腾 redis 双活的模拟环境,考虑到生产环境是不同的机房,就萌生了使用 docker 来模拟多机房不同网段的理想环境. docker 网络是采用桥接完成,会有一张 docker0 的网卡,每个容器都是在 docker0 这张网卡下.我记得网上有张描述 docker 网络通信的图,这里就不放出来,随便百度一下就有的东西.总之,明白网络通信的原理是很重要的. 二、干活 由于是在 mac 系统下干活,想要模拟还得使用 docker-machine 来搞.以下会涉及到 docker-machine 命令,但核心部分还是在设置网络上,设置网络的命令是没有任何差异的. 2.1 使用 docker-machine 创建一个跑 docker 的虚拟机 创建一个名称为: network-lab 的虚拟机 docker-machine create -d=vmware network-lab 创建成功. 2.2 创建 2 个不同的网段的网络 创建一个名称为北京的网络: bj-net docker network create -d bridge --subnet 192.168.9.0/24 --gateway 192.168.9.1 bj-net 创建一个名称为上海的网络: sh-net docker network create -d bridge --subnet 192.168.10.0/24 --gateway 192.168.10.1 sh-net 查看网络是否创建成功: docker network ls 创建2个容器测试网络 使用北京网络运行容器: docker run --rm -it --network bj-net busybox:latest 使用北京网络的容器网络情况: 网关: 192.168.9.1 ip地址: 192.168.9.2 使用上海网络运行容器: docker run --rm -it --network bj-net busybox:latest 使用上海网络的容器网络情况: 网关: 192.168.10.1 ip地址: 192.168.10.2 现在这种网络情况是无法 ping 通的 北京 ping 上海 上海 ping 北京 那么这种情况怎么解决呢?按照 docker 网络通信的原理,宿主机上必定是有 2 张网卡的,所以只要开启转发,就能把不同网段发过来的数据包转到对应的网卡,这样就能实现互通了. 2.3 互通有无 在宿主机上使用 ifconfig 可以看到北京和上海的2张网卡 先查看一下 route 信息,看看有没有进行默认的配置 通过 route 命令可以看到 9段和10段的包会到9段和10段的网卡上去,这里路由的配置是没有问题. 查看一下主机是否开启转发,默认情况下应该是开启了的,这里确认一下 cat /proc/sys/net/ipv4/ip_forward 删除 docker 针对于我们自定义的2个网卡的默认的 iptables 规则. 查看 iptables 中 filter 表中的所有规则: sudo iptables -t filter -nvL 可以看到在 INPUT链 和 OUTPUT链中是没有规则配置的,那么这里只需要更改 FORWARD 链的规则即可,由于虚拟的原因,这里截图不能截全. 通过查看 docker 的自定义链,发现是在 DOCKER-ISOLATION-STAGE-2 里定义了 北京和上海 2张网卡的规则. 这里只要删除这2条规则,不同网段的通信就通了. sudo iptables -t filter -D DOCKER-ISOLATION-STAGE-2 -o br-7703ed4f8946 -j DROP sudo iptables -t filter -D DOCKER-ISOLATION-STAGE-2 -o br-109ff1ea9a3d -j DROP ok~已经删干净了,试一试看看能不能成功. 上海 ping 北京 北京 ping 上海 搞定 四、总结 总体来说不是很复杂,只要了解 docker 网络通信原理和 iptables 的使用,问题不大.吃饭了~ Docker Machine 在 Big Sur下的各种问题2021-09-03T17:29:12+08:002021-09-03T17:29:12+08:00https://onew.me/docker/2021/09/03/docker-machine-bigsur-QA<h1 id="一前言">一、前言</h1>
<p> 最近需要模拟 2 套环境来做 redis 双活测试,由于涉及到不同的网段通信,但受制于 docker 在 macOS 系统下的实现方式,没办法做呀.就想到了用虚拟机来搞,既然都用到了虚拟机为何不用 Docker Machine 来搞呢? 反正都是跑在虚拟机上的,在怎么比在虚拟机上装个 Ubuntu 来的快吧.</p>
<p> 于是,…我还是低估了难度,噩梦开始了.</p>
<h1 id="二docker-machine-vs-virtualbox">二、Docker Machine VS virtualbox</h1>
<p> Docker Machine 这个从官方的文档来看还是挺简单的,并且命令也很简单.官方推荐用的 driver 是 virtualbox,按照官方的文档一步一步的做来下,发现使用 virtualbox 无法成功的创建 machine,就算创建了,后面的操作也会报错.</p>
<h2 id="21-virtualbox">2.1 <strong>virtualbox</strong></h2>
<p>创建 名称为 t 的machine</p>
<p><code class="language-plaintext highlighter-rouge">docker-machine create -d=virtualbox t</code></p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/j0b2bK.png" alt="j0b2bK" /></p>
<p>这里会卡住,卡 60s 左右</p>
<p>那么这个虚拟机到底有没有创建成功呢? 可以使用 <code class="language-plaintext highlighter-rouge">docker-machine ls</code> 命令查看</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/XIxj9d.png" alt="XIxj9d" /></p>
<p>可以看到这个名称为 t 的 machine 是创建成功了,但是这个状态却是 stop.</p>
<p>打开 virtualbox 查看虚拟机</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/J6cnpi.png" alt="J6cnpi" /></p>
<p>这个虚拟机居然退出了,异常退出也不知道是啥原因.没关系,说不定重启就好了.</p>
<p>使用命令 <code class="language-plaintext highlighter-rouge">docker-machine start t</code> 重启虚拟机</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/gBUz7W.png" alt="gBUz7W" /></p>
<p> 在控制台可以看到,这个虚拟机是启动成功了.可是命令 <code class="language-plaintext highlighter-rouge">docker-machine start t</code> 却没有反应,好想被卡住了一样.
<img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/Zy13CG.png" alt="Zy13CG" /></p>
<p> 在控制台里面的虚拟机都启动成功了,这个命令居然还没有反应,有点奇怪了.大概等了 60s ,命令报错了.</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/HwU5E6.png" alt="HwU5E6" /></p>
<p> 进入到虚拟机,好像是在报错.</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/aLgysF.png" alt="aLgysF" /></p>
<p> 并且虚拟机中的 docker 也没有启动.</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/StIBL8.png" alt="StIBL8" /></p>
<p> 只能从错误信息来分析了.看到一个 <code class="language-plaintext highlighter-rouge">Segmentation fault</code> 错误,这个错误搞 c 的估计很眼熟吧,在仔细分析了一下错误日志的上下文,发现是生成证书报的错.这个错误因为是系统启动的时候,调用 shell 脚本的时候报的.找了一下系统的可能存放开机启动的脚步的地方结果没找到.一下就失去了方向了.😭.</p>
<p> 但仔细一想,这个虚拟机的系统镜像叫做 <code class="language-plaintext highlighter-rouge">boot2docker</code>, 干脆去 google 一下,发现这个玩意儿是放在 github 上的,那这个事情就简单了,在 github 上翻了一下启动脚本,找到了以下这段代码.</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/ZNiE9M.png" alt="ZNiE9M" /></p>
<p> 于是就猜测是 openssl 这个命令报错的,带着疑惑到虚拟机里面执行一下看下,果然是这个问题.</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/wlzi6w.png" alt="wlzi6w" /></p>
<p> 这下错误信息就对应上了.那么解决这个问题的思路也就清晰了,按道理,只要把这个 openssl 错误解决,那么之前的卡住命令的情况也就迎刃而解了.</p>
<p> 可惜,并没有在网上找到相关讨论.不是吧,这个问题就这样了?不如最后挣扎一下,装个 ubuntu 试一试.</p>
<p> 装 ubuntu 的过程就不描述了,结论是 virtualbox 装不上 😂😂😂😂😂😂😂😂.怀疑人生了,瞬间感觉是自己太菜了不配使用 docker machine.</p>
<p> emmm,会不会是 virtualbox 的问题??肯定不是我人的问题.下载一个 <code class="language-plaintext highlighter-rouge">vmware</code>试一试,看看是不是我人的问题.</p>
<h2 id="22-vmware-fusion">2.2 VMware fusion</h2>
<p> 在官网上下载一个最新的版本安装上,安装 ubuntu.这个过程就写了.结论是安装上了,但是还得验证 openssl 是否能够正常运行.</p>
<p> 验证 openssl</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/FQ8lQj.png" alt="FQ8lQj" /></p>
<p> 运行成功了,是不是就代表 docker machine 没问题了?</p>
<p> 测试一把,<code class="language-plaintext highlighter-rouge">docker-machine create --driver=vmwarefusion test</code></p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/6fQgvX.png" alt="6fQgvX" /></p>
<p>还是报错了…</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/lzXlTe.png" alt="lzXlTe" /></p>
<p>错误信息大意是没有返回ip,但是虚拟机是运行正常的,也没有报错.</p>
<p>继续 google 大法,发现是 macOS 里 dhcp 分配ip 的问题,并且在 github 上找到了一个 pr https://github.com/machine-drivers/docker-machine-driver-vmware/pull/34</p>
<p>按照 pr 里面的提示的操作一波.</p>
<p>先安装 <code class="language-plaintext highlighter-rouge">docker-machine-driver-vmware</code>,使用 brew 命令安装 <code class="language-plaintext highlighter-rouge">brew install docker-machine-driver-vmware</code></p>
<p>再使用 <code class="language-plaintext highlighter-rouge">docker-machine-driver-vmware</code> 驱动创建一个 machine. <code class="language-plaintext highlighter-rouge">docker-machine create -d=vmware vm-test</code>.</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/yn8sVJ.png" alt="yn8sVJ" /></p>
<p>搞定~</p>
<h1 id="三总结">三、总结</h1>
<p> 要不被逼无赖,估计我也不会去折腾这个玩意儿,希望后面搞 redis 双活能够顺利点吧</p>{"nick"=>"onew", "link"=>"https://onew.me"}一、前言 最近需要模拟 2 套环境来做 redis 双活测试,由于涉及到不同的网段通信,但受制于 docker 在 macOS 系统下的实现方式,没办法做呀.就想到了用虚拟机来搞,既然都用到了虚拟机为何不用 Docker Machine 来搞呢? 反正都是跑在虚拟机上的,在怎么比在虚拟机上装个 Ubuntu 来的快吧. 于是,…我还是低估了难度,噩梦开始了. 二、Docker Machine VS virtualbox Docker Machine 这个从官方的文档来看还是挺简单的,并且命令也很简单.官方推荐用的 driver 是 virtualbox,按照官方的文档一步一步的做来下,发现使用 virtualbox 无法成功的创建 machine,就算创建了,后面的操作也会报错. 2.1 virtualbox 创建 名称为 t 的machine docker-machine create -d=virtualbox t 这里会卡住,卡 60s 左右 那么这个虚拟机到底有没有创建成功呢? 可以使用 docker-machine ls 命令查看 可以看到这个名称为 t 的 machine 是创建成功了,但是这个状态却是 stop. 打开 virtualbox 查看虚拟机 这个虚拟机居然退出了,异常退出也不知道是啥原因.没关系,说不定重启就好了. 使用命令 docker-machine start t 重启虚拟机 在控制台可以看到,这个虚拟机是启动成功了.可是命令 docker-machine start t 却没有反应,好想被卡住了一样. 在控制台里面的虚拟机都启动成功了,这个命令居然还没有反应,有点奇怪了.大概等了 60s ,命令报错了. 进入到虚拟机,好像是在报错. 并且虚拟机中的 docker 也没有启动. 只能从错误信息来分析了.看到一个 Segmentation fault 错误,这个错误搞 c 的估计很眼熟吧,在仔细分析了一下错误日志的上下文,发现是生成证书报的错.这个错误因为是系统启动的时候,调用 shell 脚本的时候报的.找了一下系统的可能存放开机启动的脚步的地方结果没找到.一下就失去了方向了.😭. 但仔细一想,这个虚拟机的系统镜像叫做 boot2docker, 干脆去 google 一下,发现这个玩意儿是放在 github 上的,那这个事情就简单了,在 github 上翻了一下启动脚本,找到了以下这段代码. 于是就猜测是 openssl 这个命令报错的,带着疑惑到虚拟机里面执行一下看下,果然是这个问题. 这下错误信息就对应上了.那么解决这个问题的思路也就清晰了,按道理,只要把这个 openssl 错误解决,那么之前的卡住命令的情况也就迎刃而解了. 可惜,并没有在网上找到相关讨论.不是吧,这个问题就这样了?不如最后挣扎一下,装个 ubuntu 试一试. 装 ubuntu 的过程就不描述了,结论是 virtualbox 装不上 😂😂😂😂😂😂😂😂.怀疑人生了,瞬间感觉是自己太菜了不配使用 docker machine. emmm,会不会是 virtualbox 的问题??肯定不是我人的问题.下载一个 vmware试一试,看看是不是我人的问题. 2.2 VMware fusion 在官网上下载一个最新的版本安装上,安装 ubuntu.这个过程就写了.结论是安装上了,但是还得验证 openssl 是否能够正常运行. 验证 openssl 运行成功了,是不是就代表 docker machine 没问题了? 测试一把,docker-machine create --driver=vmwarefusion test 还是报错了… 错误信息大意是没有返回ip,但是虚拟机是运行正常的,也没有报错. 继续 google 大法,发现是 macOS 里 dhcp 分配ip 的问题,并且在 github 上找到了一个 pr https://github.com/machine-drivers/docker-machine-driver-vmware/pull/34 按照 pr 里面的提示的操作一波. 先安装 docker-machine-driver-vmware,使用 brew 命令安装 brew install docker-machine-driver-vmware 再使用 docker-machine-driver-vmware 驱动创建一个 machine. docker-machine create -d=vmware vm-test. 搞定~ 三、总结 要不被逼无赖,估计我也不会去折腾这个玩意儿,希望后面搞 redis 双活能够顺利点吧druid连接池配置刷新导致数据库被锁2021-08-27T03:29:12+08:002021-08-27T03:29:12+08:00https://onew.me/java/2021/08/27/druid-refresh-config<h1 id="一前言">一、前言</h1>
<p> 说到 druid 这个连接池,个人感觉还是行的.但 github 2k 多的 issue ,还是让人喜欢不起来呀.在自己的项目里一般都不会选择 druid 这个连接池,毕竟不需要 druid 的监控功能,用 spring 自带的连接池就足够了.</p>
<p> 但,其他的项目非要用这个连接池,挡都挡不住.没办法~毕竟我说了不算.后面问题就来了,项目还没上线跑着跑着数据库就被锁了,关键是不止一次的被锁.这个问题还是有点意思的,当时就去把日志取了下来分析了一下.好玩!</p>
<p> 先说一下他们用 druid 连接池干了什么事情, druid 连接池有功能是可以配置数据库密码为密文的,在初始化连接池的时候由 druid 连接池的 filter 进行解密.这样做的好处是防止数据库的密码泄露,这挺好的没啥可说的,毕竟安全的问题大于天对吧~</p>
<h1 id="二问题分析">二、问题分析</h1>
<p> 通过日志分析,发现项目跑着的时候,打印了一句 <code class="language-plaintext highlighter-rouge">password changed</code>.这个可是一个线头,直觉告诉我应该是有啥东西刷新了数据库的配置.</p>
<p> 不过呢,首先的追踪一下<code class="language-plaintext highlighter-rouge">password changed</code>这个日志是在哪里打印的,翻了一下 druid 的源码.</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/uquynm.png" alt="uquynm" /></p>
<p>可以看到<code class="language-plaintext highlighter-rouge">password changed</code>这句话,是在连接池初始化完毕之后才会打印.因为有个 if 判断嘛~.那么就进一步的证实了我的猜想: 数据库的配置被刷新了.</p>
<p> 项目里用的是 <code class="language-plaintext highlighter-rouge">nacos</code> 作为配置中心,所有的配置都是从 <code class="language-plaintext highlighter-rouge">nacos</code> 里获取的,当配置有变化的时候会刷新配置.实验一把,改一下 <code class="language-plaintext highlighter-rouge">nacos</code> 上的数据库的连接数的配置.</p>
<p> 果不其然,<code class="language-plaintext highlighter-rouge">password changed</code>这句话被打印出来了.和相关人员核对了一下,说是没有更改数据库的相关配置,这点我从历史记录里面也确认了一下,确实没改过.那这个就奇怪了.</p>
<p> 难道改其他的配置文件也会引起配置刷新?实验一把,改了一下 <code class="language-plaintext highlighter-rouge">nacos</code> 的 redis 配置,😯.<code class="language-plaintext highlighter-rouge">password changed</code>这句话还是被打印了出来.</p>
<p> 那么密码被刷新了,刷新成什么了呢?通过 debug 发现是一个没有解密的密文.也就是说这个问题的根本原因是:</p>
<ol>
<li>druid 配置了数据库密码加密,并在初始化数据源的时候把密文解密成明文,并设置 <code class="language-plaintext highlighter-rouge">password</code> 这个字段为明文,以提供下一次创建连接到时候使用.</li>
<li>应用启动完成后,修改了nacos 上的配置,导致 druid 的密码被刷新为密文,并设置到 <code class="language-plaintext highlighter-rouge">password</code>这个字段,这就是<code class="language-plaintext highlighter-rouge">password changed</code>日志的出处.</li>
<li>当有流量进入应用到时候,连接池的默认连接不够使用了,连接池会拿着错误密码去创建连接,错误的密码创建连接当然会创建失败,当创建失败的时候 druid 会不断的重试,重试次数多了,数据库就把这个账户给锁定了.</li>
</ol>
<p></p>
<h1 id="三问题重现">三、问题重现</h1>
<p> 通过分析得出的结论,还是属于猜测状态,必须得深入源码来剖析.那么起点就是 <code class="language-plaintext highlighter-rouge">nacos</code>了.</p>
<h2 id="31-nacos-的配置刷新机制">3.1 nacos 的配置刷新机制</h2>
<p> <img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/CTcddI.png" alt="CTcddI" /></p>
<p> nacos 在<code class="language-plaintext highlighter-rouge">NacosContextRefresher</code>类里,会给每个配置文件加上一个监听器,当配置发生改变的时候,会通过 spring 上下文发送一个 refresh 的事件.</p>
<p> spring 有一个<code class="language-plaintext highlighter-rouge">RefreshEventListener</code>监听这个事件.</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/未命名.png" alt="未命名" /></p>
<p>当 spring 接收到这个事件的时候,会使用 <code class="language-plaintext highlighter-rouge">ContextRefresher</code> 类进行刷新.</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/refresher.png" alt="refresher" /></p>
<p>这里的刷新逻辑就比较复杂了.</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/TkfTPP.png" alt="TkfTPP" /></p>
<p>核心逻辑就是在这个方法里面.</p>
<ol>
<li>提取出当前上下文中的环境配置</li>
<li>添加配置文件到 <code class="language-plaintext highlighter-rouge">env</code>中</li>
<li>获取变化的 <code class="language-plaintext highlighter-rouge">key</code>集合</li>
<li>发送 <code class="language-plaintext highlighter-rouge">envChange</code>的事件</li>
</ol>
<p>这个 <code class="language-plaintext highlighter-rouge">envChange</code> 的事件会在<code class="language-plaintext highlighter-rouge">ConfigurationPropertiesRebinder</code>中进行处理.</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/TPCE9B.png" alt="TPCE9B" /></p>
<p>这里的核心逻辑是把所有带有<code class="language-plaintext highlighter-rouge">@ConfigurationProperties</code>注解类,进行重新绑定.</p>
<p>druid 的配置类刚好也使用了这个注解,所以不可避免的密码被重新刷新了.</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/WjpmxR.png" alt="WjpmxR" /></p>
<h2 id="32-解决方案">3.2 解决方案</h2>
<p> 当然这种刷新的情况 spring 也是注意到了的,所以专门留了个配置,用于指定的类不进行重新绑定.</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/zZj4Cl.png" alt="zZj4Cl" /></p>
<p>看到这里,感觉 spring 真的偏心,默认给 HK 数据源配置了不刷新.</p>
<h1 id="四总结">四、总结</h1>
<p> 这个问题在官方的 github 上也是有的,https://github.com/alibaba/druid/issues/2312. 有时间来提个PR过去.不知道官方有人管没得.</p>{"nick"=>"onew", "link"=>"https://onew.me"}一、前言 说到 druid 这个连接池,个人感觉还是行的.但 github 2k 多的 issue ,还是让人喜欢不起来呀.在自己的项目里一般都不会选择 druid 这个连接池,毕竟不需要 druid 的监控功能,用 spring 自带的连接池就足够了. 但,其他的项目非要用这个连接池,挡都挡不住.没办法~毕竟我说了不算.后面问题就来了,项目还没上线跑着跑着数据库就被锁了,关键是不止一次的被锁.这个问题还是有点意思的,当时就去把日志取了下来分析了一下.好玩! 先说一下他们用 druid 连接池干了什么事情, druid 连接池有功能是可以配置数据库密码为密文的,在初始化连接池的时候由 druid 连接池的 filter 进行解密.这样做的好处是防止数据库的密码泄露,这挺好的没啥可说的,毕竟安全的问题大于天对吧~ 二、问题分析 通过日志分析,发现项目跑着的时候,打印了一句 password changed.这个可是一个线头,直觉告诉我应该是有啥东西刷新了数据库的配置. 不过呢,首先的追踪一下password changed这个日志是在哪里打印的,翻了一下 druid 的源码. 可以看到password changed这句话,是在连接池初始化完毕之后才会打印.因为有个 if 判断嘛~.那么就进一步的证实了我的猜想: 数据库的配置被刷新了. 项目里用的是 nacos 作为配置中心,所有的配置都是从 nacos 里获取的,当配置有变化的时候会刷新配置.实验一把,改一下 nacos 上的数据库的连接数的配置. 果不其然,password changed这句话被打印出来了.和相关人员核对了一下,说是没有更改数据库的相关配置,这点我从历史记录里面也确认了一下,确实没改过.那这个就奇怪了. 难道改其他的配置文件也会引起配置刷新?实验一把,改了一下 nacos 的 redis 配置,😯.password changed这句话还是被打印了出来. 那么密码被刷新了,刷新成什么了呢?通过 debug 发现是一个没有解密的密文.也就是说这个问题的根本原因是: druid 配置了数据库密码加密,并在初始化数据源的时候把密文解密成明文,并设置 password 这个字段为明文,以提供下一次创建连接到时候使用. 应用启动完成后,修改了nacos 上的配置,导致 druid 的密码被刷新为密文,并设置到 password这个字段,这就是password changed日志的出处. 当有流量进入应用到时候,连接池的默认连接不够使用了,连接池会拿着错误密码去创建连接,错误的密码创建连接当然会创建失败,当创建失败的时候 druid 会不断的重试,重试次数多了,数据库就把这个账户给锁定了. 三、问题重现 通过分析得出的结论,还是属于猜测状态,必须得深入源码来剖析.那么起点就是 nacos了. 3.1 nacos 的配置刷新机制 nacos 在NacosContextRefresher类里,会给每个配置文件加上一个监听器,当配置发生改变的时候,会通过 spring 上下文发送一个 refresh 的事件. spring 有一个RefreshEventListener监听这个事件. 当 spring 接收到这个事件的时候,会使用 ContextRefresher 类进行刷新. 这里的刷新逻辑就比较复杂了. 核心逻辑就是在这个方法里面. 提取出当前上下文中的环境配置 添加配置文件到 env中 获取变化的 key集合 发送 envChange的事件 这个 envChange 的事件会在ConfigurationPropertiesRebinder中进行处理. 这里的核心逻辑是把所有带有@ConfigurationProperties注解类,进行重新绑定. druid 的配置类刚好也使用了这个注解,所以不可避免的密码被重新刷新了. 3.2 解决方案 当然这种刷新的情况 spring 也是注意到了的,所以专门留了个配置,用于指定的类不进行重新绑定. 看到这里,感觉 spring 真的偏心,默认给 HK 数据源配置了不刷新. 四、总结 这个问题在官方的 github 上也是有的,https://github.com/alibaba/druid/issues/2312. 有时间来提个PR过去.不知道官方有人管没得.asus-b85-pro-g+i5-4450黑苹果2021-08-26T09:29:12+08:002021-08-26T09:29:12+08:00https://onew.me/hackintosh/2021/08/26/hacktion-b85<h1 id="一前言">一、前言</h1>
<p> 最近一段时间可谓是DIY玩家的噩梦,各种硬件疯涨,特别是在虚拟货币的加持下显卡都上天了.本来还想去淘一张rx580没想到这三朝元老都涨价到了1800左右.</p>
<p> 迫于无奈(穷),在加上最近有点时间可以折腾一下.就想着折腾一下黑苹果.看了看家里的电子垃圾,心里暗想,还是让他们发光发热一下吧(😭).</p>
<p> 本教程大多数都是从 opencore install guide 官网上摘抄过来的,如果有啥问题那就是我理解的问题的.</p>
<h1 id="二配置">二、配置</h1>
<p> </p>
<table>
<thead>
<tr>
<th> </th>
<th> </th>
</tr>
</thead>
<tbody>
<tr>
<td>cpu</td>
<td>I5-4450</td>
</tr>
<tr>
<td>主板</td>
<td>Asus-b85-pro-g</td>
</tr>
<tr>
<td>内存</td>
<td>海盗船DDR3 8G * 2</td>
</tr>
<tr>
<td>硬盘</td>
<td>铠侠RC10</td>
</tr>
<tr>
<td>网卡</td>
<td>bcm943602cs</td>
</tr>
<tr>
<td>显卡</td>
<td>HD4600</td>
</tr>
</tbody>
</table>
<p> 由于没有独显只能拿核显顶一下,纯粹办公还是能顶得住的.</p>
<h1 id="三安装">三、安装</h1>
<h3 id="31-硬件选择">3.1 硬件选择</h3>
<p> 现在安装黑苹果基本都是基于 opencore 进行安装.所以第一步就需要在 <a href="https://dortania.github.io/OpenCore-Install-Guide/macos-limits.html">opencore install guid</a> 的指南网站上去查看硬件配置是否兼容.</p>
<p> 如果觉得英文看得难受也可以参考<a href="https://blog.daliansky.net/Mojave-Hardware-Support-List.html">黑果小兵的博客</a>,显卡和网卡尽量选择免驱的,减少折腾的时间(一般情况下固态是不会有兼容性问题的,除了:三星 970 EVO,这些限制都能在 opencore 的网站上找到)</p>
<p> 这片文章撰写时,opencore的版本为0.7.2</p>
<h3 id="32-准备工作">3.2 准备工作</h3>
<p> 硬件选择好了之后,建议先安装windows系统,在windows上面做准备工作.也要准备一个大于16G的U盘用于引导和安装黑苹果.</p>
<p> 辅助软件下载:</p>
<p></p>
<table>
<thead>
<tr>
<th>软件</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>python3</td>
<td>作为辅助软件的基础运行环境</td>
</tr>
<tr>
<td><a href="https://github.com/corpnewt/ProperTree">ProperTree</a></td>
<td>一个plist编辑器,opencore官方推荐工具</td>
</tr>
<tr>
<td><a href="https://github.com/corpnewt/SSDTTime">SSDTTime</a></td>
<td>用于导出SSDT信息和生成通用的SSDT-EC、SSDT-PLUG、SSDT-HPET布丁,在ACPI章节会需要</td>
</tr>
<tr>
<td><a href="https://github.com/corpnewt/GenSMBIOS">GenSMBIOS</a></td>
<td>生成随机的smbios信息</td>
</tr>
<tr>
<td><a href="https://github.com/headkaze/Hackintool">Hackintool</a></td>
<td>在macos安装好之后可以使用该工具进行打补丁或者做usb定制</td>
</tr>
<tr>
<td>OpenCore Configurator</td>
<td>一个图形化配置工具,在macos安装好之后可以使用该工具进行配置</td>
</tr>
<tr>
<td>IORegistryExplorer2.0</td>
<td>在macos安装好之后使用该工具查看各个硬件是否驱动</td>
</tr>
<tr>
<td>AIDA64</td>
<td>用于在windows平台上获取硬件信息</td>
</tr>
</tbody>
</table>
<p></p>
<h2 id="33-获取硬件信息">3.3 获取硬件信息</h2>
<p><a href="https://dortania.github.io/OpenCore-Install-Guide/find-hardware.html#finding-hardware-using-windows">opencore 原文</a></p>
<p>获取CPU型号:</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/AAbQPZ.png" alt="cpu型号" /></p>
<p>获取显卡型号:</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/bqxHzH.png" alt="bqxHzH" /></p>
<p>获取芯片组信息:</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/8e1wDW.png" alt="8e1wDW" /></p>
<p>获取声卡信息:</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/ztmq5h.png" alt="ztmq5h" /></p>
<p>获取网卡信息:</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/YGAfvj.png" alt="YGAfvj" /></p>
<p>opencore 指南里面还获取了 SMBUS、USB、I2C、Keyboard, Trackpad、 Touchscreen Connection Type这些信息,我个人认为,用台式机的话是用不着以上信息的,当然设备不能太老.</p>
<h2 id="34-创建macos启动盘">3.4 创建MacOS启动盘</h2>
<p> 创建启动盘需要准备一个U盘和<a href="https://rufus.ie/zh/">rufus</a>、<a href="https://github.com/acidanthera/OpenCorePkg/releases">OpenCorePkg</a>工具.OpenCorePkg需要提前解压好.下载OpenCorePkg建议下载debug版本,输出的信息更多,方便前期调试.</p>
<h3 id="341-下载macos">3.4.1 下载MacOS</h3>
<p>进入到Utilities/macrecovery/,复制目录路径</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/4I9Qlc.jpg" alt="4I9Qlc" /></p>
<p>打开命令行工具,切换目录到macrecovery</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/UO3IQX.jpg" alt="UO3IQX" /></p>
<p>运行命令</p>
<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Lion(10.7):
</span><span class="err">python</span> <span class="err">macrecovery.py</span> <span class="err">-b</span> <span class="err">Mac-2E6FAB96566FE58C</span> <span class="err">-m</span> <span class="err">00000000000F25Y00</span> <span class="err">download</span>
<span class="err">python</span> <span class="err">macrecovery.py</span> <span class="err">-b</span> <span class="err">Mac-C3EC7CD22292981F</span> <span class="err">-m</span> <span class="err">00000000000F0HM00</span> <span class="err">download</span>
<span class="c"># Mountain Lion(10.8):
</span><span class="err">python</span> <span class="err">macrecovery.py</span> <span class="err">-b</span> <span class="err">Mac-7DF2A3B5E5D671ED</span> <span class="err">-m</span> <span class="err">00000000000F65100</span> <span class="err">download</span>
<span class="c"># Mavericks(10.9):
</span><span class="err">python</span> <span class="err">macrecovery.py</span> <span class="err">-b</span> <span class="err">Mac-F60DEB81FF30ACF6</span> <span class="err">-m</span> <span class="err">00000000000FNN100</span> <span class="err">download</span>
<span class="c"># Yosemite(10.10):
</span><span class="err">python</span> <span class="err">macrecovery.py</span> <span class="err">-b</span> <span class="err">Mac-E43C1C25D4880AD6</span> <span class="err">-m</span> <span class="err">00000000000GDVW00</span> <span class="err">download</span>
<span class="c"># El Capitan(10.11):
</span><span class="err">python</span> <span class="err">macrecovery.py</span> <span class="err">-b</span> <span class="err">Mac-FFE5EF870D7BA81A</span> <span class="err">-m</span> <span class="err">00000000000GQRX00</span> <span class="err">download</span>
<span class="c"># Sierra(10.12):
</span><span class="err">python</span> <span class="err">macrecovery.py</span> <span class="err">-b</span> <span class="err">Mac-77F17D7DA9285301</span> <span class="err">-m</span> <span class="err">00000000000J0DX00</span> <span class="err">download</span>
<span class="c"># High Sierra(10.13)
</span><span class="err">python</span> <span class="err">macrecovery.py</span> <span class="err">-b</span> <span class="err">Mac-7BA5B2D9E42DDD94</span> <span class="err">-m</span> <span class="err">00000000000J80300</span> <span class="err">download</span>
<span class="err">python</span> <span class="err">macrecovery.py</span> <span class="err">-b</span> <span class="err">Mac-BE088AF8C5EB4FA2</span> <span class="err">-m</span> <span class="err">00000000000J80300</span> <span class="err">download</span>
<span class="c"># Mojave(10.14)
</span><span class="err">python</span> <span class="err">macrecovery.py</span> <span class="err">-b</span> <span class="err">Mac-7BA5B2DFE22DDD8C</span> <span class="err">-m</span> <span class="err">00000000000KXPG00</span> <span class="err">download</span>
<span class="c"># Catalina(10.15)
</span><span class="err">python</span> <span class="err">macrecovery.py</span> <span class="err">-b</span> <span class="err">Mac-00BE6ED71E35EB86</span> <span class="err">-m</span> <span class="err">00000000000000000</span> <span class="err">download</span>
<span class="c"># Latest version
# ie. Big Sur(11)
</span><span class="err">python</span> <span class="err">macrecovery.py</span> <span class="err">-b</span> <span class="err">Mac-E43C1C25D4880AD6</span> <span class="err">-m</span> <span class="err">00000000000000000</span> <span class="err">download</span>
</code></pre></div></div>
<p>注意: Mojave 往后的版本会有15个usb端口的限制,需要定制usb端口.建议安装 Mojave,在 Mojave 上定制USB端口后再进行升级到最新版本.</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/rwE1f5.jpg" alt="rwE1f5" /></p>
<p>下载完成后会产生以下2个文件</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/r6H0ID.jpg" alt="r6H0ID" /></p>
<h3 id="342-制作引导盘">3.4.2 制作引导盘</h3>
<p>运行Rufus</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/3Sd2lp.jpg" alt="3Sd2lp" /></p>
<ol>
<li>选择 boot selection 为 non bootable</li>
<li>选择 file system 为 large FAT32</li>
<li>点击 start</li>
<li>Rufus 运行结束后,打开U盘,创建目录名为:<code class="language-plaintext highlighter-rouge">com.apple.recovery.boot</code>,然后把之前下载好的文件复制到U盘中去.</li>
</ol>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/6jRXg1.jpg" alt="6jRXg1" /></p>
<ol>
<li>
<p>打开之前下载的OpenCorePkg,选择对应的模板,一般来讲是用x64的模板</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/6wShje.jpg" alt="6wShje" /></p>
</li>
<li>
<p>复制x64文件夹中的EFI到U盘的根目录</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/xK19eo.jpg" alt="xK19eo" /></p>
</li>
</ol>
<h3 id="343-精简模板">3.4.3 精简模板</h3>
<p> 安装黑苹果的准则之一就是东西越少越好,这样方便排查错误.在opencore模板里面带了很多不必要的东西,需要删除掉.</p>
<p> openCore的目录结构如下:</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/N0Wnug.jpg" alt="N0Wnug" /></p>
<p> 在<code class="language-plaintext highlighter-rouge">BOOT</code>目录里面只有一个efi文件,一般情况下不会去动它.调整的比较多的目录是<code class="language-plaintext highlighter-rouge">OC</code>这个目录.</p>
<table>
<thead>
<tr>
<th>目录</th>
<th>文件名</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>OC/Drivers</td>
<td>OpenRuntime.efi</td>
<td>必要</td>
</tr>
<tr>
<td>OC/Tools</td>
<td>OpenShell.efi</td>
<td>非必要,推荐使用方便调试</td>
</tr>
<tr>
<td>BOOT</td>
<td>BOOTx64.efi</td>
<td>必要</td>
</tr>
<tr>
<td>OC</td>
<td>OpenCore.efi</td>
<td>必要</td>
</tr>
</tbody>
</table>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/5uHmI2.jpg" alt="5uHmI2" /></p>
<h3 id="344-完善模板">3.4.4 完善模板</h3>
<p>以下的文件,按照自己硬件情况添加,这里我只记录我自己的添加的文件.详情请参考 <a href="https://dortania.github.io/OpenCore-Install-Guide/ktext.html#universal">openCore install guide</a></p>
<h4 id="kexts">kexts</h4>
<table>
<thead>
<tr>
<th>目录</th>
<th>文件</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>OC/Drivers</td>
<td><a href="https://github.com/acidanthera/OcBinaryData/blob/master/Drivers/HfsPlus.efi">HfsPlus.efi</a></td>
<td>必要,文件系统</td>
</tr>
<tr>
<td>OC/Kexts</td>
<td><a href="https://github.com/acidanthera/VirtualSMC/releases">VirtualSMC.kext</a></td>
<td>必要,模拟mac</td>
</tr>
<tr>
<td>OC/Kexts</td>
<td>SMCProcessor.kext</td>
<td>非必要,监控处理器温度</td>
</tr>
<tr>
<td>OC/Kexts</td>
<td>SMCSuperIO.kext</td>
<td>非必要.监控风扇速度</td>
</tr>
<tr>
<td>OC/Kexts</td>
<td><a href="https://github.com/acidanthera/Lilu/releases">Lilu.kext</a></td>
<td>必要,底层运行环境</td>
</tr>
<tr>
<td>OC/Kexts</td>
<td><a href="https://github.com/acidanthera/WhateverGreen/releases">WhateverGreen.kext</a></td>
<td>显卡驱动</td>
</tr>
<tr>
<td>OC/Kexts</td>
<td><a href="https://github.com/acidanthera/AppleALC/releases">AppleALC.kext</a></td>
<td>声卡驱动</td>
</tr>
<tr>
<td>OC/Kexts</td>
<td><a href="https://github.com/acidanthera/IntelMausi/releases">IntelMausi.kext</a></td>
<td>网卡驱动,这个需要根据硬件情况</td>
</tr>
<tr>
<td>OC/Kexts</td>
<td><a href="https://bitbucket.org/RehabMan/os-x-usb-inject-all/downloads/">USBInjectAll.kext</a></td>
<td>usb驱动</td>
</tr>
</tbody>
</table>
<p>由于使用的是免驱的网卡和核显,就不需要额外的驱动了.</p>
<h4 id="ssdts">SSDTs:</h4>
<p> ssdt补丁需要根据自己的平台来选择对应的补丁.例如我这边是 4代 i5 Haswell 平台,就需要2个补丁,分别是:<a href="https://dortania.github.io/Getting-Started-With-ACPI/Universal/plug.html">SSDT-PLUG</a>、<a href="https://dortania.github.io/Getting-Started-With-ACPI/Universal/ec-fix.html">SSDT-EC</a>.</p>
<p> 以上2个补丁都可以通过SSDTTime这个工具生成.</p>
<p>SSDT-PLUG:</p>
<ol>
<li>运行 SSDTTime</li>
<li>执行 dump SSDT</li>
<li>执行 PluginType</li>
</ol>
<p>SSDT-EC:</p>
<ol>
<li>执行 SSDTTime</li>
<li>执行 dump SSDT</li>
<li>执行 Fake EC</li>
</ol>
<p>以上2步执行完毕后会在 result 目录中生产 aml 文件,复制 SSDT-PLUG.aml 和 SSDT-EC.aml 到u盘的 ACPI 目录中.</p>
<h4 id="configplist">config.plist</h4>
<p> 添加完文件之后,目录结构大概是这样:</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/IHlIuZ.jpg" alt="IHlIuZ" /></p>
<p> 回到 OpenCorePkg 目录中,进入到 Docs 目录,复制 <code class="language-plaintext highlighter-rouge">Sample.plist</code> 文件到 U盘 <code class="language-plaintext highlighter-rouge">EFI/OC/</code>目录中,并重命名为<code class="language-plaintext highlighter-rouge">config.plist</code>.</p>
<p> <img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/VBHZNW.jpg" alt="VBHZNW" /></p>
<p> 运行 <code class="language-plaintext highlighter-rouge">ProperTree</code>,用<strong>Ctrl + O</strong>打开U盘的 <code class="language-plaintext highlighter-rouge">config.plist</code>文件.再用<strong>Ctrl + Shift + R</strong>快捷键刷新<code class="language-plaintext highlighter-rouge">config.plist</code>.</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/8oAZps.jpg" alt="8oAZps" /></p>
<p> 后面的过程就是根据自己的平台来调整配置.这个配置会调整很多次,不存在一次性调整好的(除非运气好).调整的内容太多了,不打算在这里写了.</p>
<h2 id="四hd4600-hdmi音频的坑">四、HD4600 HDMI音频的坑</h2>
<p> 自己调整很久的,hdmi音频一直没有弄出来,再一次爬贴过程中发现把缓存帧中flags,也就是把Flag_10取消勾选,声音取消勾选,声音就出来了.</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/3GJvXU.png" alt="3GJvXU" /></p>
<h2 id="五总结">五、总结</h2>
<p> 黑苹果这块涉及的东西太多了,光是打缓冲帧就可以水一章,反正是些来给自己看的,等哪天心情好的时候再来完善完善.</p>{"nick"=>"onew", "link"=>"https://onew.me"}一、前言 最近一段时间可谓是DIY玩家的噩梦,各种硬件疯涨,特别是在虚拟货币的加持下显卡都上天了.本来还想去淘一张rx580没想到这三朝元老都涨价到了1800左右. 迫于无奈(穷),在加上最近有点时间可以折腾一下.就想着折腾一下黑苹果.看了看家里的电子垃圾,心里暗想,还是让他们发光发热一下吧(😭). 本教程大多数都是从 opencore install guide 官网上摘抄过来的,如果有啥问题那就是我理解的问题的. 二、配置 cpu I5-4450 主板 Asus-b85-pro-g 内存 海盗船DDR3 8G * 2 硬盘 铠侠RC10 网卡 bcm943602cs 显卡 HD4600 由于没有独显只能拿核显顶一下,纯粹办公还是能顶得住的. 三、安装 3.1 硬件选择 现在安装黑苹果基本都是基于 opencore 进行安装.所以第一步就需要在 opencore install guid 的指南网站上去查看硬件配置是否兼容. 如果觉得英文看得难受也可以参考黑果小兵的博客,显卡和网卡尽量选择免驱的,减少折腾的时间(一般情况下固态是不会有兼容性问题的,除了:三星 970 EVO,这些限制都能在 opencore 的网站上找到) 这片文章撰写时,opencore的版本为0.7.2 3.2 准备工作 硬件选择好了之后,建议先安装windows系统,在windows上面做准备工作.也要准备一个大于16G的U盘用于引导和安装黑苹果. 辅助软件下载: 软件 说明 python3 作为辅助软件的基础运行环境 ProperTree 一个plist编辑器,opencore官方推荐工具 SSDTTime 用于导出SSDT信息和生成通用的SSDT-EC、SSDT-PLUG、SSDT-HPET布丁,在ACPI章节会需要 GenSMBIOS 生成随机的smbios信息 Hackintool 在macos安装好之后可以使用该工具进行打补丁或者做usb定制 OpenCore Configurator 一个图形化配置工具,在macos安装好之后可以使用该工具进行配置 IORegistryExplorer2.0 在macos安装好之后使用该工具查看各个硬件是否驱动 AIDA64 用于在windows平台上获取硬件信息 3.3 获取硬件信息 opencore 原文 获取CPU型号: 获取显卡型号: 获取芯片组信息: 获取声卡信息: 获取网卡信息: opencore 指南里面还获取了 SMBUS、USB、I2C、Keyboard, Trackpad、 Touchscreen Connection Type这些信息,我个人认为,用台式机的话是用不着以上信息的,当然设备不能太老. 3.4 创建MacOS启动盘 创建启动盘需要准备一个U盘和rufus、OpenCorePkg工具.OpenCorePkg需要提前解压好.下载OpenCorePkg建议下载debug版本,输出的信息更多,方便前期调试. 3.4.1 下载MacOS 进入到Utilities/macrecovery/,复制目录路径 打开命令行工具,切换目录到macrecovery 运行命令 # Lion(10.7): python macrecovery.py -b Mac-2E6FAB96566FE58C -m 00000000000F25Y00 download python macrecovery.py -b Mac-C3EC7CD22292981F -m 00000000000F0HM00 download # Mountain Lion(10.8): python macrecovery.py -b Mac-7DF2A3B5E5D671ED -m 00000000000F65100 download # Mavericks(10.9): python macrecovery.py -b Mac-F60DEB81FF30ACF6 -m 00000000000FNN100 download # Yosemite(10.10): python macrecovery.py -b Mac-E43C1C25D4880AD6 -m 00000000000GDVW00 download # El Capitan(10.11): python macrecovery.py -b Mac-FFE5EF870D7BA81A -m 00000000000GQRX00 download # Sierra(10.12): python macrecovery.py -b Mac-77F17D7DA9285301 -m 00000000000J0DX00 download # High Sierra(10.13) python macrecovery.py -b Mac-7BA5B2D9E42DDD94 -m 00000000000J80300 download python macrecovery.py -b Mac-BE088AF8C5EB4FA2 -m 00000000000J80300 download # Mojave(10.14) python macrecovery.py -b Mac-7BA5B2DFE22DDD8C -m 00000000000KXPG00 download # Catalina(10.15) python macrecovery.py -b Mac-00BE6ED71E35EB86 -m 00000000000000000 download # Latest version # ie. Big Sur(11) python macrecovery.py -b Mac-E43C1C25D4880AD6 -m 00000000000000000 download 注意: Mojave 往后的版本会有15个usb端口的限制,需要定制usb端口.建议安装 Mojave,在 Mojave 上定制USB端口后再进行升级到最新版本. 下载完成后会产生以下2个文件 3.4.2 制作引导盘 运行Rufus 选择 boot selection 为 non bootable 选择 file system 为 large FAT32 点击 start Rufus 运行结束后,打开U盘,创建目录名为:com.apple.recovery.boot,然后把之前下载好的文件复制到U盘中去. 打开之前下载的OpenCorePkg,选择对应的模板,一般来讲是用x64的模板 复制x64文件夹中的EFI到U盘的根目录 3.4.3 精简模板 安装黑苹果的准则之一就是东西越少越好,这样方便排查错误.在opencore模板里面带了很多不必要的东西,需要删除掉. openCore的目录结构如下: 在BOOT目录里面只有一个efi文件,一般情况下不会去动它.调整的比较多的目录是OC这个目录. 目录 文件名 说明 OC/Drivers OpenRuntime.efi 必要 OC/Tools OpenShell.efi 非必要,推荐使用方便调试 BOOT BOOTx64.efi 必要 OC OpenCore.efi 必要 3.4.4 完善模板 以下的文件,按照自己硬件情况添加,这里我只记录我自己的添加的文件.详情请参考 openCore install guide kexts 目录 文件 说明 OC/Drivers HfsPlus.efi 必要,文件系统 OC/Kexts VirtualSMC.kext 必要,模拟mac OC/Kexts SMCProcessor.kext 非必要,监控处理器温度 OC/Kexts SMCSuperIO.kext 非必要.监控风扇速度 OC/Kexts Lilu.kext 必要,底层运行环境 OC/Kexts WhateverGreen.kext 显卡驱动 OC/Kexts AppleALC.kext 声卡驱动 OC/Kexts IntelMausi.kext 网卡驱动,这个需要根据硬件情况 OC/Kexts USBInjectAll.kext usb驱动 由于使用的是免驱的网卡和核显,就不需要额外的驱动了. SSDTs: ssdt补丁需要根据自己的平台来选择对应的补丁.例如我这边是 4代 i5 Haswell 平台,就需要2个补丁,分别是:SSDT-PLUG、SSDT-EC. 以上2个补丁都可以通过SSDTTime这个工具生成. SSDT-PLUG: 运行 SSDTTime 执行 dump SSDT 执行 PluginType SSDT-EC: 执行 SSDTTime 执行 dump SSDT 执行 Fake EC 以上2步执行完毕后会在 result 目录中生产 aml 文件,复制 SSDT-PLUG.aml 和 SSDT-EC.aml 到u盘的 ACPI 目录中. config.plist 添加完文件之后,目录结构大概是这样: 回到 OpenCorePkg 目录中,进入到 Docs 目录,复制 Sample.plist 文件到 U盘 EFI/OC/目录中,并重命名为config.plist. 运行 ProperTree,用Ctrl + O打开U盘的 config.plist文件.再用Ctrl + Shift + R快捷键刷新config.plist. 后面的过程就是根据自己的平台来调整配置.这个配置会调整很多次,不存在一次性调整好的(除非运气好).调整的内容太多了,不打算在这里写了. 四、HD4600 HDMI音频的坑 自己调整很久的,hdmi音频一直没有弄出来,再一次爬贴过程中发现把缓存帧中flags,也就是把Flag_10取消勾选,声音取消勾选,声音就出来了. 五、总结 黑苹果这块涉及的东西太多了,光是打缓冲帧就可以水一章,反正是些来给自己看的,等哪天心情好的时候再来完善完善.WebLogic 第三弹,代码都去哪了,绕过https证书验证失效?2021-04-25T10:01:25+08:002021-04-25T10:01:25+08:00https://onew.me/java/2021/04/25/WebLogic-url-connection<h1 id="weblogic-第三弹代码都去哪了">WebLogic 第三弹,代码都去哪了?</h1>
<h1 id="一前言">一、前言</h1>
<p> 在开发过程中,总能遇到奇奇怪怪的问题。这次这个问题比较好玩,故记录下来。整个事情是这样的,前段时间上线了一个新的需求,结果发现调用三方接口报错了。报错的信息是对方 <code class="language-plaintext highlighter-rouge">https</code> 的证书有问题,不能通过证书校验。</p>
<p> 这个问题其实很好解决,让接口方检查一下证书就好了。奈何这里比较弱势,做了甲方却是乙方的地位。于是开发的同学就提出,可以绕过https的证书的校验,由于不是我负责的需求,我也不能插话不是。</p>
<p> 很快啊,代码就改造完了(增加了跳过的代码)。我简单的贴一下代码。</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/TpUADz.png" alt="TpUADz" /></p>
<p> 乍一看没啥问题,开发环境里也运行的很好,测试环境里也运行的很好。嗯。冲吧,上线吧!</p>
<p> 不幸的是,上线后,这块儿跳过的代码并没有生效,依旧的报错了。开发的小伙伴一看懵了,发来报错日志的图片来找我求助。</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/3zBYfh.jpg" alt="3zBYfh" /></p>
<p> 很想吐槽,为啥不截图给我,而且也该换手机了吧。就这图能看出啥??正想骂人的时候,等等!我破案了。对,我知道为啥线上为啥不能跳过 <code class="language-plaintext highlighter-rouge">https</code> 证书校验了。</p>
<h1 id="二spring中的http请求工具resttemplate">二、Spring中的http请求工具,RestTemplate。</h1>
<p> 相信很多小伙伴都用过这个工具,今天的主题是绕过 <code class="language-plaintext highlighter-rouge">https</code> 的证书验证。恰好就是用 <code class="language-plaintext highlighter-rouge">RestTemplate</code> 发送的请求。那就来抽丝剥茧的看看,<code class="language-plaintext highlighter-rouge">RestTemplate</code> 这个工具是怎么发送的请求。</p>
<p> 先来看看是怎么发送 <code class="language-plaintext highlighter-rouge">GET</code> 请求的:</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/ZysRbe.png" alt="ZysRbe" /></p>
<p> 这个方法的核心逻辑是调用了 <code class="language-plaintext highlighter-rouge">execute</code> 方法。接着往下看。</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/Qa5o7W.png" alt="Qa5o7W" /></p>
<p> 这里的逻辑是,解析请求的 <code class="language-plaintext highlighter-rouge">url</code> 拼装为<code class="language-plaintext highlighter-rouge">URI</code> 对象。接着往下看</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/截屏2021-04-25 上午10.14.57.png" alt="截屏2021-04-25上午10.14.57" /></p>
<p> 这里有两个核心的逻辑,一个创建请求,一个是执行请求。这里就只看创建请求。</p>
<ul>
<li>
<p>创建请求</p>
<ul>
<li>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/gbn5AK.png" alt="gbn5AK" /></p>
<p> 这里使用了一个工厂进行创建,如果没有设置工厂的话,默认是 <code class="language-plaintext highlighter-rouge">SimpleClientHttpRequestFactory</code> 为了能跳过 <code class="language-plaintext highlighter-rouge">https</code> 证书校验,需要创建一个自定义的工厂对象,并设置到了 restTemplate 里,主要是为了重写工厂对象中的 <code class="language-plaintext highlighter-rouge">prepareConnection</code> 方法。</p>
<p> <code class="language-plaintext highlighter-rouge">prepareConnection</code> 能够在连接创建完毕之后进行预处理,这里的预处理就包括跳过 <code class="language-plaintext highlighter-rouge">https</code> 证书校验等.</p>
</li>
<li>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/nzPfGT.png" alt="nzPfGT" /></p>
<p> 从 <code class="language-plaintext highlighter-rouge">createRequest</code> 方法可以看出,连接在使用前必须要进行预处理。</p>
</li>
<li>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/Bn31aj.png" alt="Bn31aj" /></p>
<p> 打开连接这里就简单直白了,只是就是用 <code class="language-plaintext highlighter-rouge">jdk</code> 自带的方法创建的连接。既然都看到这里了,索性再看一下 <code class="language-plaintext highlighter-rouge">url</code> 是怎样创建连接的吧。</p>
</li>
<li>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/gyfFYK.png" alt="gyfFYK" /></p>
<p> ??就这?其实这里的一句话,还是要依赖前面的初始化,毕竟<code class="language-plaintext highlighter-rouge">handler</code> 一开始是为空,需要进行初始化。当创建 <code class="language-plaintext highlighter-rouge">URL</code> 被创建的时候,<code class="language-plaintext highlighter-rouge">handler</code> 就会在构造函数被初始化。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nf">URL</span><span class="o">(</span><span class="nc">String</span> <span class="n">protocol</span><span class="o">,</span> <span class="nc">String</span> <span class="n">host</span><span class="o">,</span> <span class="kt">int</span> <span class="n">port</span><span class="o">,</span> <span class="nc">String</span> <span class="n">file</span><span class="o">,</span>
<span class="nc">URLStreamHandler</span> <span class="n">handler</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">MalformedURLException</span> <span class="o">{</span>
<span class="c1">// 省略很多无关的代码</span>
<span class="c1">// 当 handler 为空的时候,初始化一个 handler 并复制给当前对象</span>
<span class="k">if</span> <span class="o">(</span><span class="n">handler</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">&&</span>
<span class="o">(</span><span class="n">handler</span> <span class="o">=</span> <span class="n">getURLStreamHandler</span><span class="o">(</span><span class="n">protocol</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">throw</span> <span class="k">new</span> <span class="nf">MalformedURLException</span><span class="o">(</span><span class="s">"unknown protocol: "</span> <span class="o">+</span> <span class="n">protocol</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">this</span><span class="o">.</span><span class="na">handler</span> <span class="o">=</span> <span class="n">handler</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div> </div>
<p><code class="language-plaintext highlighter-rouge">getURLStreamHandler</code> 这个方法才是核心。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">static</span> <span class="nc">URLStreamHandler</span> <span class="nf">getURLStreamHandler</span><span class="o">(</span><span class="nc">String</span> <span class="n">protocol</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// 先从缓存中根据协议获取 handler 如果缓存命中则直接返回</span>
<span class="nc">URLStreamHandler</span> <span class="n">handler</span> <span class="o">=</span> <span class="n">handlers</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">protocol</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">handler</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">handler</span><span class="o">;</span>
<span class="o">}</span>
<span class="nc">URLStreamHandlerFactory</span> <span class="n">fac</span><span class="o">;</span>
<span class="kt">boolean</span> <span class="n">checkedWithFactory</span> <span class="o">=</span> <span class="kc">false</span><span class="o">;</span>
<span class="kt">boolean</span> <span class="n">overrideableProtocol</span> <span class="o">=</span> <span class="n">isOverrideable</span><span class="o">(</span><span class="n">protocol</span><span class="o">);</span>
<span class="c1">// 判断是否是 jat 和 file 协议 并且 JVM 已经启动完成</span>
<span class="k">if</span> <span class="o">(</span><span class="n">overrideableProtocol</span> <span class="o">&&</span> <span class="no">VM</span><span class="o">.</span><span class="na">isBooted</span><span class="o">())</span> <span class="o">{</span>
<span class="c1">// Use the factory (if any). Volatile read makes</span>
<span class="c1">// URLStreamHandlerFactory appear fully initialized to current thread.</span>
<span class="n">fac</span> <span class="o">=</span> <span class="n">factory</span><span class="o">;</span>
<span class="k">if</span> <span class="o">(</span><span class="n">fac</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="n">handler</span> <span class="o">=</span> <span class="n">fac</span><span class="o">.</span><span class="na">createURLStreamHandler</span><span class="o">(</span><span class="n">protocol</span><span class="o">);</span>
<span class="n">checkedWithFactory</span> <span class="o">=</span> <span class="kc">true</span><span class="o">;</span>
<span class="o">}</span>
<span class="c1">// factory 为空 使用lookupViaProviders创建handler</span>
<span class="k">if</span> <span class="o">(</span><span class="n">handler</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">&&</span> <span class="o">!</span><span class="n">protocol</span><span class="o">.</span><span class="na">equalsIgnoreCase</span><span class="o">(</span><span class="s">"jar"</span><span class="o">))</span> <span class="o">{</span>
<span class="c1">// 使用 SPI 创建 handler</span>
<span class="n">handler</span> <span class="o">=</span> <span class="n">lookupViaProviders</span><span class="o">(</span><span class="n">protocol</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">if</span> <span class="o">(</span><span class="n">handler</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// 通过环境变量创建 handler</span>
<span class="n">handler</span> <span class="o">=</span> <span class="n">lookupViaProperty</span><span class="o">(</span><span class="n">protocol</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="c1">// 如果还没创建好 handler 则使用默认的 factory 进行创建</span>
<span class="k">if</span> <span class="o">(</span><span class="n">handler</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// Try the built-in protocol handler</span>
<span class="n">handler</span> <span class="o">=</span> <span class="n">defaultFactory</span><span class="o">.</span><span class="na">createURLStreamHandler</span><span class="o">(</span><span class="n">protocol</span><span class="o">);</span>
<span class="o">}</span>
<span class="kd">synchronized</span> <span class="o">(</span><span class="n">streamHandlerLock</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">URLStreamHandler</span> <span class="n">handler2</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
<span class="c1">// Check again with hashtable just in case another</span>
<span class="c1">// thread created a handler since we last checked</span>
<span class="n">handler2</span> <span class="o">=</span> <span class="n">handlers</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">protocol</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">handler2</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">handler2</span><span class="o">;</span>
<span class="o">}</span>
<span class="c1">// Check with factory if another thread set a</span>
<span class="c1">// factory since our last check</span>
<span class="k">if</span> <span class="o">(</span><span class="n">overrideableProtocol</span> <span class="o">&&</span> <span class="o">!</span><span class="n">checkedWithFactory</span> <span class="o">&&</span>
<span class="o">(</span><span class="n">fac</span> <span class="o">=</span> <span class="n">factory</span><span class="o">)</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="n">handler2</span> <span class="o">=</span> <span class="n">fac</span><span class="o">.</span><span class="na">createURLStreamHandler</span><span class="o">(</span><span class="n">protocol</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">if</span> <span class="o">(</span><span class="n">handler2</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// The handler from the factory must be given more</span>
<span class="c1">// importance. Discard the default handler that</span>
<span class="c1">// this thread created.</span>
<span class="n">handler</span> <span class="o">=</span> <span class="n">handler2</span><span class="o">;</span>
<span class="o">}</span>
<span class="c1">// Insert this handler into the hashtable</span>
<span class="k">if</span> <span class="o">(</span><span class="n">handler</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// 放入缓存</span>
<span class="n">handlers</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="n">protocol</span><span class="o">,</span> <span class="n">handler</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="k">return</span> <span class="n">handler</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div> </div>
<p> 通过<code class="language-plaintext highlighter-rouge">SPI</code> 创建的过程就不多说了,主要看看通过环境变量创建的环节.</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="nc">URLStreamHandler</span> <span class="nf">lookupViaProperty</span><span class="o">(</span><span class="nc">String</span> <span class="n">protocol</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">//private static final String protocolPathProp = "java.protocol.handler.pkgs";</span>
<span class="c1">// 获取环境变量为 java.protocol.handler.pkgs 的值</span>
<span class="nc">String</span> <span class="n">packagePrefixList</span> <span class="o">=</span>
<span class="nc">GetPropertyAction</span><span class="o">.</span><span class="na">privilegedGetProperty</span><span class="o">(</span><span class="n">protocolPathProp</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">packagePrefixList</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// not set</span>
<span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
<span class="o">}</span>
<span class="c1">// 按照 | 切割</span>
<span class="nc">String</span><span class="o">[]</span> <span class="n">packagePrefixes</span> <span class="o">=</span> <span class="n">packagePrefixList</span><span class="o">.</span><span class="na">split</span><span class="o">(</span><span class="s">"\\|"</span><span class="o">);</span>
<span class="nc">URLStreamHandler</span> <span class="n">handler</span> <span class="o">=</span> <span class="kc">null</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">handler</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">&&</span> <span class="n">i</span><span class="o"><</span><span class="n">packagePrefixes</span><span class="o">.</span><span class="na">length</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
<span class="nc">String</span> <span class="n">packagePrefix</span> <span class="o">=</span> <span class="n">packagePrefixes</span><span class="o">[</span><span class="n">i</span><span class="o">].</span><span class="na">trim</span><span class="o">();</span>
<span class="k">try</span> <span class="o">{</span>
<span class="c1">// 拼接 Handler 的名称</span>
<span class="nc">String</span> <span class="n">clsName</span> <span class="o">=</span> <span class="n">packagePrefix</span> <span class="o">+</span> <span class="s">"."</span> <span class="o">+</span> <span class="n">protocol</span> <span class="o">+</span> <span class="s">".Handler"</span><span class="o">;</span>
<span class="nc">Class</span><span class="o"><?></span> <span class="n">cls</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="c1">// 加载类</span>
<span class="n">cls</span> <span class="o">=</span> <span class="nc">Class</span><span class="o">.</span><span class="na">forName</span><span class="o">(</span><span class="n">clsName</span><span class="o">);</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">ClassNotFoundException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">ClassLoader</span> <span class="n">cl</span> <span class="o">=</span> <span class="nc">ClassLoader</span><span class="o">.</span><span class="na">getSystemClassLoader</span><span class="o">();</span>
<span class="k">if</span> <span class="o">(</span><span class="n">cl</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="n">cls</span> <span class="o">=</span> <span class="n">cl</span><span class="o">.</span><span class="na">loadClass</span><span class="o">(</span><span class="n">clsName</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="k">if</span> <span class="o">(</span><span class="n">cls</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="nd">@SuppressWarnings</span><span class="o">(</span><span class="s">"deprecation"</span><span class="o">)</span>
<span class="nc">Object</span> <span class="n">tmp</span> <span class="o">=</span> <span class="n">cls</span><span class="o">.</span><span class="na">newInstance</span><span class="o">();</span>
<span class="n">handler</span> <span class="o">=</span> <span class="o">(</span><span class="nc">URLStreamHandler</span><span class="o">)</span><span class="n">tmp</span><span class="o">;</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="c1">// any number of exceptions can get thrown here</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="k">return</span> <span class="n">handler</span><span class="o">;</span>
<span class="o">}</span><span class="err">`</span>
</code></pre></div> </div>
</li>
</ul>
</li>
</ul>
<h1 id="三破案">三、破案</h1>
<p> 说了这么多,还是没看出为啥代码没生效,有啥用?静下心来,你就会发现是有用的,如果是 <code class="language-plaintext highlighter-rouge">https</code> 协议,这里的默认 <code class="language-plaintext highlighter-rouge">handler</code> 是啥呢?如果没有人为的指定应该是<code class="language-plaintext highlighter-rouge">sun.net.www.protocol.https.Handler</code>,也就是说,通过 <code class="language-plaintext highlighter-rouge">sun</code> 包下面的 <code class="language-plaintext highlighter-rouge">handler</code> 创建出来的 <code class="language-plaintext highlighter-rouge">connection</code> 也会是 <code class="language-plaintext highlighter-rouge">sun</code> 包下的。</p>
<p> 那么,破案了。通过异常的日志可以看出,报错的栈信息是来自 <code class="language-plaintext highlighter-rouge">weblogic</code>的,特别是<code class="language-plaintext highlighter-rouge">weblogic.net.http.HttpsUrlConnection</code>这句话。这就说明了,使用的 <code class="language-plaintext highlighter-rouge">handler</code> 是来 <code class="language-plaintext highlighter-rouge">weblogic</code> 包里面的而不是 <code class="language-plaintext highlighter-rouge">sun</code>包里面的。</p>
<p> 回到跳过<code class="language-plaintext highlighter-rouge">https</code>证书的这块代码里:</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/截屏2021-04-25 上午11.02.57.png" alt="截屏2021-04-25上午11.02.57" /></p>
<p> 这里就是罪魁祸首,为了证明这点,我特地找到了 <code class="language-plaintext highlighter-rouge">weblogic</code>下的jar里面去看了一下。</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/NhpFHk.png" alt="NhpFHk" /></p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/f78XP3.png" alt="f78XP3" /></p>
<h1 id="四总结">四、总结</h1>
<p> 遇到不能复现的问题,不要慌张,慌张是解决不了问题的。话说这个问题解决也好解决,要么改代码,要么加上启动参数<code class="language-plaintext highlighter-rouge">-DUseSunHttpHandler=true</code></p>{"nick"=>"onew", "link"=>"https://onew.me"}WebLogic 第三弹,代码都去哪了? 一、前言 在开发过程中,总能遇到奇奇怪怪的问题。这次这个问题比较好玩,故记录下来。整个事情是这样的,前段时间上线了一个新的需求,结果发现调用三方接口报错了。报错的信息是对方 https 的证书有问题,不能通过证书校验。 这个问题其实很好解决,让接口方检查一下证书就好了。奈何这里比较弱势,做了甲方却是乙方的地位。于是开发的同学就提出,可以绕过https的证书的校验,由于不是我负责的需求,我也不能插话不是。 很快啊,代码就改造完了(增加了跳过的代码)。我简单的贴一下代码。 乍一看没啥问题,开发环境里也运行的很好,测试环境里也运行的很好。嗯。冲吧,上线吧! 不幸的是,上线后,这块儿跳过的代码并没有生效,依旧的报错了。开发的小伙伴一看懵了,发来报错日志的图片来找我求助。 很想吐槽,为啥不截图给我,而且也该换手机了吧。就这图能看出啥??正想骂人的时候,等等!我破案了。对,我知道为啥线上为啥不能跳过 https 证书校验了。 二、Spring中的http请求工具,RestTemplate。 相信很多小伙伴都用过这个工具,今天的主题是绕过 https 的证书验证。恰好就是用 RestTemplate 发送的请求。那就来抽丝剥茧的看看,RestTemplate 这个工具是怎么发送的请求。 先来看看是怎么发送 GET 请求的: 这个方法的核心逻辑是调用了 execute 方法。接着往下看。 这里的逻辑是,解析请求的 url 拼装为URI 对象。接着往下看 这里有两个核心的逻辑,一个创建请求,一个是执行请求。这里就只看创建请求。 创建请求 这里使用了一个工厂进行创建,如果没有设置工厂的话,默认是 SimpleClientHttpRequestFactory 为了能跳过 https 证书校验,需要创建一个自定义的工厂对象,并设置到了 restTemplate 里,主要是为了重写工厂对象中的 prepareConnection 方法。 prepareConnection 能够在连接创建完毕之后进行预处理,这里的预处理就包括跳过 https 证书校验等. 从 createRequest 方法可以看出,连接在使用前必须要进行预处理。 打开连接这里就简单直白了,只是就是用 jdk 自带的方法创建的连接。既然都看到这里了,索性再看一下 url 是怎样创建连接的吧。 ??就这?其实这里的一句话,还是要依赖前面的初始化,毕竟handler 一开始是为空,需要进行初始化。当创建 URL 被创建的时候,handler 就会在构造函数被初始化。 public URL(String protocol, String host, int port, String file, URLStreamHandler handler) throws MalformedURLException { // 省略很多无关的代码 // 当 handler 为空的时候,初始化一个 handler 并复制给当前对象 if (handler == null && (handler = getURLStreamHandler(protocol)) == null) { throw new MalformedURLException("unknown protocol: " + protocol); } this.handler = handler; } getURLStreamHandler 这个方法才是核心。 static URLStreamHandler getURLStreamHandler(String protocol) { // 先从缓存中根据协议获取 handler 如果缓存命中则直接返回 URLStreamHandler handler = handlers.get(protocol); if (handler != null) { return handler; } URLStreamHandlerFactory fac; boolean checkedWithFactory = false; boolean overrideableProtocol = isOverrideable(protocol); // 判断是否是 jat 和 file 协议 并且 JVM 已经启动完成 if (overrideableProtocol && VM.isBooted()) { // Use the factory (if any). Volatile read makes // URLStreamHandlerFactory appear fully initialized to current thread. fac = factory; if (fac != null) { handler = fac.createURLStreamHandler(protocol); checkedWithFactory = true; } // factory 为空 使用lookupViaProviders创建handler if (handler == null && !protocol.equalsIgnoreCase("jar")) { // 使用 SPI 创建 handler handler = lookupViaProviders(protocol); } if (handler == null) { // 通过环境变量创建 handler handler = lookupViaProperty(protocol); } } // 如果还没创建好 handler 则使用默认的 factory 进行创建 if (handler == null) { // Try the built-in protocol handler handler = defaultFactory.createURLStreamHandler(protocol); } synchronized (streamHandlerLock) { URLStreamHandler handler2 = null; // Check again with hashtable just in case another // thread created a handler since we last checked handler2 = handlers.get(protocol); if (handler2 != null) { return handler2; } // Check with factory if another thread set a // factory since our last check if (overrideableProtocol && !checkedWithFactory && (fac = factory) != null) { handler2 = fac.createURLStreamHandler(protocol); } if (handler2 != null) { // The handler from the factory must be given more // importance. Discard the default handler that // this thread created. handler = handler2; } // Insert this handler into the hashtable if (handler != null) { // 放入缓存 handlers.put(protocol, handler); } } return handler; } 通过SPI 创建的过程就不多说了,主要看看通过环境变量创建的环节. private static URLStreamHandler lookupViaProperty(String protocol) { //private static final String protocolPathProp = "java.protocol.handler.pkgs"; // 获取环境变量为 java.protocol.handler.pkgs 的值 String packagePrefixList = GetPropertyAction.privilegedGetProperty(protocolPathProp); if (packagePrefixList == null) { // not set return null; } // 按照 | 切割 String[] packagePrefixes = packagePrefixList.split("\\|"); URLStreamHandler handler = null; for (int i=0; handler == null && i<packagePrefixes.length; i++) { String packagePrefix = packagePrefixes[i].trim(); try { // 拼接 Handler 的名称 String clsName = packagePrefix + "." + protocol + ".Handler"; Class<?> cls = null; try { // 加载类 cls = Class.forName(clsName); } catch (ClassNotFoundException e) { ClassLoader cl = ClassLoader.getSystemClassLoader(); if (cl != null) { cls = cl.loadClass(clsName); } } if (cls != null) { @SuppressWarnings("deprecation") Object tmp = cls.newInstance(); handler = (URLStreamHandler)tmp; } } catch (Exception e) { // any number of exceptions can get thrown here } } return handler; }` 三、破案 说了这么多,还是没看出为啥代码没生效,有啥用?静下心来,你就会发现是有用的,如果是 https 协议,这里的默认 handler 是啥呢?如果没有人为的指定应该是sun.net.www.protocol.https.Handler,也就是说,通过 sun 包下面的 handler 创建出来的 connection 也会是 sun 包下的。 那么,破案了。通过异常的日志可以看出,报错的栈信息是来自 weblogic的,特别是weblogic.net.http.HttpsUrlConnection这句话。这就说明了,使用的 handler 是来 weblogic 包里面的而不是 sun包里面的。 回到跳过https证书的这块代码里: 这里就是罪魁祸首,为了证明这点,我特地找到了 weblogic下的jar里面去看了一下。 四、总结 遇到不能复现的问题,不要慌张,慌张是解决不了问题的。话说这个问题解决也好解决,要么改代码,要么加上启动参数-DUseSunHttpHandler=trueWebLogic Metaspace OOM 解决案例(后续之SkyWalking)2021-03-22T17:01:25+08:002021-03-22T17:01:25+08:00https://onew.me/java/2021/03/22/Weblogic-Metastapce-skywalking<h1 id="weblogic-metaspace-oom-解决案例后续之skykwalking">WebLogic Metaspace OOM 解决案例(后续之SkykWalking)</h1>
<h2 id="一前言">一、前言</h2>
<p> 之前解决了因为 <code class="language-plaintext highlighter-rouge">nacos</code> 未能关闭线程,导致 weblogic 中的 ChangeAwareClassloader 被 nacos 的线程长期持有的问题。虽然是解决了,但是还是大意了。由于当时复现的环境跟线上的环境并不是完全一致,所以还是没能根本性的解决。没办法只能把复现环境尽量调整到跟线上一致,再来分析一波。</p>
<p> 先预告一下,这次的罪魁祸首是 SkyWalking 。emmm,标题已经剧透了,😅。</p>
<h2 id="二skywalking">二、SkyWalking</h2>
<p> SkyWalking 是业内流行度很高的 apm ,目前在 apache 旗下。skyWalking 在 java 端可以使用 agent 的方式来进行监控,由于是无侵入性的,所以在初期选型的时候直接就采用了 agent 的方式。但世事难料呀,由于 skywalking 并没有宣布支持 weblogic ,加上调研不仔细,就直接莽了上去。</p>
<p> 用,是能用的,只不过会有一些小问题,前期的小毛病都已经解决了,只是这次的问题比较严重而已。来,直接分析一波 heap 看看是什么东西导致了 classLoader 没又被回收掉。有了上一次的经验,基本可以确定是 classLoader 没有被释放。</p>
<h2 id="三heap-分析">三、Heap 分析</h2>
<p> 这次分析的主角还是 Mat 。先在 weblogic 中启动项目,然后停止项目,并删除项目。这样的目的是模拟项目更新的操作,然后我们再使用 <code class="language-plaintext highlighter-rouge">jamp</code> 命令 <code class="language-plaintext highlighter-rouge">dump</code> 一份儿内存看看。命令: <code class="language-plaintext highlighter-rouge">jmap -dump:file=/tmp/PID.dump PID</code>。</p>
<p> 使用 Mat 加载刚才 dump 出来的文件。</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/aRRVhQ.png" alt="aRRVhQ" /></p>
<p>可以看到,果然还是 ChangeAwareClassLoader 没有被释放掉的问题,点开详情看看,到底是谁那么讨厌,拿着 classLoader 不放。</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/XLlfmo.png" alt="XLlfmo" /></p>
<p>从这个图可以看出,ChangeAwareClassLoader 没有被释放掉是被 skywalking 中的一个Map 给持有了。这个 Map 到底有啥用,这点需要去源码看一看。</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/未命名.jpg" alt="未命名" /></p>
<p>根据 ClassLoader 卸载原则,要清空 <code class="language-plaintext highlighter-rouge">INSTANCE_CACHE</code> 和 <code class="language-plaintext highlighter-rouge">EXTEND_PLUGIN_CLASSLOADERS</code> 这两个 map。 由于这两个都是私有变量不能直接访问,这里需要反射一波。</p>
<h2 id="四如何释放-skywalking-缓存">四、如何释放 SkyWalking 缓存?</h2>
<p> 由于 weblogic 的特殊性,这里需要考虑到以下几点:</p>
<ul>
<li>要准确清理应用的 classLoader,不能出现应用部署多次,只清理一个的情况。</li>
<li>只能清理当前应用的 classLoader,不能出现别的应用不需要清理的情况下,误清理。</li>
</ul>
<p> 基于以上2点,有点不好操作,因为这个 ChangeAwareClassLoader 的生命周期和 ServletContext 的生命周期是不一致的。在整个应用的生命周期中,ChangeAwareClassLoader 只会创建一次(除非重新部署)。但 ServletContext 则会创建多次,应用启动一次创建一次。</p>
<p> 如果跟着 ServletContext 的生命周期走,在应用重复启动多次情况下,会把本不应该清理的 ClassLoader 给清理掉。因为我们需要在应用卸载的时候卸载 ClassLoader 而不是在应用停止的时候清理。</p>
<p> 看了一下 weblogic 的官网文档,得知有个 <code class="language-plaintext highlighter-rouge">ApplicationLifecycleListener</code>。但这个东西是 weblogic 独有的,不是属于j2e规范。要使用这个东西就必须把 war 改成 ear。这就有点尴尬了。</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/gdXIkU.png" alt="gdXIkU" /></p>
<p> 既然要解决这个问题,本就是逆天改命之举。那也怪不得我使用奇淫巧技了。</p>
<h2 id="五skywalking-里的花招">五、SkyWalking 里的花招</h2>
<p> 虽然不能直接使用<code class="language-plaintext highlighter-rouge">ApplicationLifecycleListener</code>,那么能不能换个方式使用呢?了解过 skyWalking 的人都知道,skyWalking 可以通过 agent 的方式实现无侵入式的增强。幸好 skyWalking 留了一个口子,让我们自行扩展。我深信 skyWalking 留个口子不是拿来给我搞骚操作的。但没办法,还是要利用一下。那么呼之欲出的插件就来了。</p>
<p> skyWalking 是有一个插件功能的,这个插件可以理解为一个拦截器。至于插件要怎么写,这里就不详细介绍了,可以去看看 skyWalking 的官方文档。 可以简单看看官方项目自带的tomcat插件。</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/interceptor.jpg" alt="interceptor" /></p>
<p>这里分为3个部分:</p>
<ul>
<li>拦截器的定义</li>
<li>拦截器的具体逻辑</li>
<li>描述信息</li>
</ul>
<p>下面代码为定义代码</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// org.apache.skywalking.apm.plugin.tomcat78x.define.ApplicationDispatcherInstrumentation</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ApplicationDispatcherInstrumentation</span> <span class="kd">extends</span> <span class="nc">ClassInstanceMethodsEnhancePluginDefine</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">ENHANCE_CLASS</span> <span class="o">=</span> <span class="s">"org.apache.catalina.core.ApplicationDispatcher"</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">ENHANCE_METHOD</span> <span class="o">=</span> <span class="s">"forward"</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">INTERCEPTOR_CLASS</span> <span class="o">=</span> <span class="s">"org.apache.skywalking.apm.plugin.tomcat78x.ForwardInterceptor"</span><span class="o">;</span>
<span class="cm">/***
* 构造器拦截器
* */</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">ConstructorInterceptPoint</span><span class="o">[]</span> <span class="nf">getConstructorsInterceptPoints</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nc">ConstructorInterceptPoint</span><span class="o">[]</span> <span class="o">{</span>
<span class="k">new</span> <span class="nf">ConstructorInterceptPoint</span><span class="o">()</span> <span class="o">{</span>
<span class="cm">/***
* 描述如何匹配构造器
* */</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">ElementMatcher</span><span class="o"><</span><span class="nc">MethodDescription</span><span class="o">></span> <span class="nf">getConstructorMatcher</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="nf">any</span><span class="o">();</span>
<span class="o">}</span>
<span class="cm">/***
* 使用哪个拦截器
* */</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">String</span> <span class="nf">getConstructorInterceptor</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="no">INTERCEPTOR_CLASS</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">};</span>
<span class="o">}</span>
<span class="cm">/***
* 方法拦截器
* **/</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">InstanceMethodsInterceptPoint</span><span class="o">[]</span> <span class="nf">getInstanceMethodsInterceptPoints</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nc">InstanceMethodsInterceptPoint</span><span class="o">[]</span> <span class="o">{</span>
<span class="k">new</span> <span class="nf">InstanceMethodsInterceptPoint</span><span class="o">()</span> <span class="o">{</span>
<span class="cm">/***
* 描述如何匹配方法
* */</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">ElementMatcher</span><span class="o"><</span><span class="nc">MethodDescription</span><span class="o">></span> <span class="nf">getMethodsMatcher</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="nf">named</span><span class="o">(</span><span class="no">ENHANCE_METHOD</span><span class="o">);</span>
<span class="o">}</span>
<span class="cm">/***
* 使用哪个拦截器
* */</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">String</span> <span class="nf">getMethodsInterceptor</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="no">INTERCEPTOR_CLASS</span><span class="o">;</span>
<span class="o">}</span>
<span class="cm">/***
* 是否覆盖参数
* */</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">isOverrideArgs</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>
<span class="o">};</span>
<span class="o">}</span>
<span class="nd">@Override</span>
<span class="kd">protected</span> <span class="nc">ClassMatch</span> <span class="nf">enhanceClass</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="nf">byName</span><span class="o">(</span><span class="no">ENHANCE_CLASS</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>下面代码为拦截器代码</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// org.apache.skywalking.apm.plugin.tomcat78x.ForwardInterceptor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ForwardInterceptor</span> <span class="kd">implements</span> <span class="nc">InstanceMethodsAroundInterceptor</span><span class="o">,</span> <span class="nc">InstanceConstructorInterceptor</span> <span class="o">{</span>
<span class="cm">/***
* 目标方法执行前
* @param objInst 执行方法的目标对象
* @param method 目标方法
* @param allArguments 方法参数
* @param argumentsTypes 参数类型
* @param result 返回值
* */</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">beforeMethod</span><span class="o">(</span><span class="nc">EnhancedInstance</span> <span class="n">objInst</span><span class="o">,</span> <span class="nc">Method</span> <span class="n">method</span><span class="o">,</span> <span class="nc">Object</span><span class="o">[]</span> <span class="n">allArguments</span><span class="o">,</span> <span class="nc">Class</span><span class="o"><?>[]</span> <span class="n">argumentsTypes</span><span class="o">,</span>
<span class="nc">MethodInterceptResult</span> <span class="n">result</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Throwable</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="nc">ContextManager</span><span class="o">.</span><span class="na">isActive</span><span class="o">())</span> <span class="o">{</span>
<span class="nc">AbstractSpan</span> <span class="n">abstractTracingSpan</span> <span class="o">=</span> <span class="nc">ContextManager</span><span class="o">.</span><span class="na">activeSpan</span><span class="o">();</span>
<span class="nc">Map</span><span class="o"><</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">></span> <span class="n">eventMap</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HashMap</span><span class="o"><</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">>();</span>
<span class="n">eventMap</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"forward-url"</span><span class="o">,</span> <span class="n">objInst</span><span class="o">.</span><span class="na">getSkyWalkingDynamicField</span><span class="o">()</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">?</span> <span class="s">""</span> <span class="o">:</span> <span class="nc">String</span><span class="o">.</span><span class="na">valueOf</span><span class="o">(</span><span class="n">objInst</span><span class="o">.</span><span class="na">getSkyWalkingDynamicField</span><span class="o">()));</span>
<span class="n">abstractTracingSpan</span><span class="o">.</span><span class="na">log</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">eventMap</span><span class="o">);</span>
<span class="nc">ContextManager</span><span class="o">.</span><span class="na">getRuntimeContext</span><span class="o">().</span><span class="na">put</span><span class="o">(</span><span class="nc">Constants</span><span class="o">.</span><span class="na">FORWARD_REQUEST_FLAG</span><span class="o">,</span> <span class="kc">true</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="cm">/**
* 目标方法执行后
* @param objInst 执行方法的目标对象
* @param method 目标方法
* @param allArguments 方法参数
* @param argumentsTypes 参数类型
* @param ret 返回值
* **/</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">Object</span> <span class="nf">afterMethod</span><span class="o">(</span><span class="nc">EnhancedInstance</span> <span class="n">objInst</span><span class="o">,</span> <span class="nc">Method</span> <span class="n">method</span><span class="o">,</span> <span class="nc">Object</span><span class="o">[]</span> <span class="n">allArguments</span><span class="o">,</span> <span class="nc">Class</span><span class="o"><?>[]</span> <span class="n">argumentsTypes</span><span class="o">,</span>
<span class="nc">Object</span> <span class="n">ret</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Throwable</span> <span class="o">{</span>
<span class="nc">ContextManager</span><span class="o">.</span><span class="na">getRuntimeContext</span><span class="o">().</span><span class="na">remove</span><span class="o">(</span><span class="nc">Constants</span><span class="o">.</span><span class="na">FORWARD_REQUEST_FLAG</span><span class="o">);</span>
<span class="k">return</span> <span class="n">ret</span><span class="o">;</span>
<span class="o">}</span>
<span class="cm">/***
* 处理异常
* @param objInst 执行方法的目标对象
* @param method 目标方法
* @param allArguments 方法参数
* @param argumentsTypes 参数类型
* @param t 异常
* */</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">handleMethodException</span><span class="o">(</span><span class="nc">EnhancedInstance</span> <span class="n">objInst</span><span class="o">,</span> <span class="nc">Method</span> <span class="n">method</span><span class="o">,</span> <span class="nc">Object</span><span class="o">[]</span> <span class="n">allArguments</span><span class="o">,</span>
<span class="nc">Class</span><span class="o"><?>[]</span> <span class="n">argumentsTypes</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="cm">/**
* 构造方法执行后
* @param objInst 目标对象
* @param allArguments 构造器参数
* */</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">onConstruct</span><span class="o">(</span><span class="nc">EnhancedInstance</span> <span class="n">objInst</span><span class="o">,</span> <span class="nc">Object</span><span class="o">[]</span> <span class="n">allArguments</span><span class="o">)</span> <span class="o">{</span>
<span class="n">objInst</span><span class="o">.</span><span class="na">setSkyWalkingDynamicField</span><span class="o">(</span><span class="n">allArguments</span><span class="o">[</span><span class="mi">1</span><span class="o">]);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>下面代码是描述</p>
<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">tomcat-7.x/</span><span class="py">8.x</span><span class="p">=</span><span class="s">org.apache.skywalking.apm.plugin.tomcat78x.define.TomcatInstrumentation</span>
<span class="err">tomcat-7.x/</span><span class="py">8.x</span><span class="p">=</span><span class="s">org.apache.skywalking.apm.plugin.tomcat78x.define.ApplicationDispatcherInstrumentation</span>
</code></pre></div></div>
<p>好了,现在你已经能够熟练的编写一个插件了。</p>
<h2 id="六清理-classloader-的插件">六、清理 ClassLoader 的插件</h2>
<p> 根据 webLogic 的特性,需要增强 WebAppModule 这个类,这个类安装应用只会创建一次。这是个很好的人选。那么生命周期监听器在那里添加呢?</p>
<p> WebAppModule 这个类提供了获取 WebApplicationContext 对象的方法,只需要在 WebAppModule 初始化方法 <code class="language-plaintext highlighter-rouge">init</code> 调用完毕之后,就直接把 listener 添加到 WebApplicationContext 中去。</p>
<p> listener 的具体逻辑是,在 postStart 方法里持有 ChangeAwareClassLoader 引用,然后在 postStop 方法里进行清理。清理不用说了,反射直接莽。</p>
<p>定义:</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/c7pfyg.png" alt="c7pfyg" /></p>
<p>插件:</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/UaqMVw.png" alt="UaqMVw" /></p>
<p>描述:</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/UCAu2D.png" alt="UCAu2D" /></p>
<p>一切准备就绪,只需要打成 jar 包,丢到 skywalking 到 plugin 目录即可。</p>
<h2 id="七事成之后">七、事成之后</h2>
<p> 加入插件之后,可以用 jdk 自带的调试工具来欣赏一下期待已久的 Metaspace 内存使用图。</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/SCOQzK.png" alt="SCOQzK" /></p>
<p> weblogic 还是太坑了,主要不是 weblogic 的问题,是使用新技术与不敢去老技术栈的矛盾问题。其实呢,全部用老技术,遵循 weblogic 这一套,也不会出幺蛾子。但现在要推新技术,老的技术栈不去,只能天天填坑。</p>{"nick"=>"onew", "link"=>"https://onew.me"}WebLogic Metaspace OOM 解决案例(后续之SkykWalking) 一、前言 之前解决了因为 nacos 未能关闭线程,导致 weblogic 中的 ChangeAwareClassloader 被 nacos 的线程长期持有的问题。虽然是解决了,但是还是大意了。由于当时复现的环境跟线上的环境并不是完全一致,所以还是没能根本性的解决。没办法只能把复现环境尽量调整到跟线上一致,再来分析一波。 先预告一下,这次的罪魁祸首是 SkyWalking 。emmm,标题已经剧透了,😅。 二、SkyWalking SkyWalking 是业内流行度很高的 apm ,目前在 apache 旗下。skyWalking 在 java 端可以使用 agent 的方式来进行监控,由于是无侵入性的,所以在初期选型的时候直接就采用了 agent 的方式。但世事难料呀,由于 skywalking 并没有宣布支持 weblogic ,加上调研不仔细,就直接莽了上去。 用,是能用的,只不过会有一些小问题,前期的小毛病都已经解决了,只是这次的问题比较严重而已。来,直接分析一波 heap 看看是什么东西导致了 classLoader 没又被回收掉。有了上一次的经验,基本可以确定是 classLoader 没有被释放。 三、Heap 分析 这次分析的主角还是 Mat 。先在 weblogic 中启动项目,然后停止项目,并删除项目。这样的目的是模拟项目更新的操作,然后我们再使用 jamp 命令 dump 一份儿内存看看。命令: jmap -dump:file=/tmp/PID.dump PID。 使用 Mat 加载刚才 dump 出来的文件。 可以看到,果然还是 ChangeAwareClassLoader 没有被释放掉的问题,点开详情看看,到底是谁那么讨厌,拿着 classLoader 不放。 从这个图可以看出,ChangeAwareClassLoader 没有被释放掉是被 skywalking 中的一个Map 给持有了。这个 Map 到底有啥用,这点需要去源码看一看。 根据 ClassLoader 卸载原则,要清空 INSTANCE_CACHE 和 EXTEND_PLUGIN_CLASSLOADERS 这两个 map。 由于这两个都是私有变量不能直接访问,这里需要反射一波。 四、如何释放 SkyWalking 缓存? 由于 weblogic 的特殊性,这里需要考虑到以下几点: 要准确清理应用的 classLoader,不能出现应用部署多次,只清理一个的情况。 只能清理当前应用的 classLoader,不能出现别的应用不需要清理的情况下,误清理。 基于以上2点,有点不好操作,因为这个 ChangeAwareClassLoader 的生命周期和 ServletContext 的生命周期是不一致的。在整个应用的生命周期中,ChangeAwareClassLoader 只会创建一次(除非重新部署)。但 ServletContext 则会创建多次,应用启动一次创建一次。 如果跟着 ServletContext 的生命周期走,在应用重复启动多次情况下,会把本不应该清理的 ClassLoader 给清理掉。因为我们需要在应用卸载的时候卸载 ClassLoader 而不是在应用停止的时候清理。 看了一下 weblogic 的官网文档,得知有个 ApplicationLifecycleListener。但这个东西是 weblogic 独有的,不是属于j2e规范。要使用这个东西就必须把 war 改成 ear。这就有点尴尬了。 既然要解决这个问题,本就是逆天改命之举。那也怪不得我使用奇淫巧技了。 五、SkyWalking 里的花招 虽然不能直接使用ApplicationLifecycleListener,那么能不能换个方式使用呢?了解过 skyWalking 的人都知道,skyWalking 可以通过 agent 的方式实现无侵入式的增强。幸好 skyWalking 留了一个口子,让我们自行扩展。我深信 skyWalking 留个口子不是拿来给我搞骚操作的。但没办法,还是要利用一下。那么呼之欲出的插件就来了。 skyWalking 是有一个插件功能的,这个插件可以理解为一个拦截器。至于插件要怎么写,这里就不详细介绍了,可以去看看 skyWalking 的官方文档。 可以简单看看官方项目自带的tomcat插件。 这里分为3个部分: 拦截器的定义 拦截器的具体逻辑 描述信息 下面代码为定义代码 // org.apache.skywalking.apm.plugin.tomcat78x.define.ApplicationDispatcherInstrumentation public class ApplicationDispatcherInstrumentation extends ClassInstanceMethodsEnhancePluginDefine { private static final String ENHANCE_CLASS = "org.apache.catalina.core.ApplicationDispatcher"; private static final String ENHANCE_METHOD = "forward"; public static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.tomcat78x.ForwardInterceptor"; /*** * 构造器拦截器 * */ @Override public ConstructorInterceptPoint[] getConstructorsInterceptPoints() { return new ConstructorInterceptPoint[] { new ConstructorInterceptPoint() { /*** * 描述如何匹配构造器 * */ @Override public ElementMatcher<MethodDescription> getConstructorMatcher() { return any(); } /*** * 使用哪个拦截器 * */ @Override public String getConstructorInterceptor() { return INTERCEPTOR_CLASS; } } }; } /*** * 方法拦截器 * **/ @Override public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() { return new InstanceMethodsInterceptPoint[] { new InstanceMethodsInterceptPoint() { /*** * 描述如何匹配方法 * */ @Override public ElementMatcher<MethodDescription> getMethodsMatcher() { return named(ENHANCE_METHOD); } /*** * 使用哪个拦截器 * */ @Override public String getMethodsInterceptor() { return INTERCEPTOR_CLASS; } /*** * 是否覆盖参数 * */ @Override public boolean isOverrideArgs() { return false; } } }; } @Override protected ClassMatch enhanceClass() { return byName(ENHANCE_CLASS); } } 下面代码为拦截器代码 // org.apache.skywalking.apm.plugin.tomcat78x.ForwardInterceptor public class ForwardInterceptor implements InstanceMethodsAroundInterceptor, InstanceConstructorInterceptor { /*** * 目标方法执行前 * @param objInst 执行方法的目标对象 * @param method 目标方法 * @param allArguments 方法参数 * @param argumentsTypes 参数类型 * @param result 返回值 * */ @Override public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, MethodInterceptResult result) throws Throwable { if (ContextManager.isActive()) { AbstractSpan abstractTracingSpan = ContextManager.activeSpan(); Map<String, String> eventMap = new HashMap<String, String>(); eventMap.put("forward-url", objInst.getSkyWalkingDynamicField() == null ? "" : String.valueOf(objInst.getSkyWalkingDynamicField())); abstractTracingSpan.log(System.currentTimeMillis(), eventMap); ContextManager.getRuntimeContext().put(Constants.FORWARD_REQUEST_FLAG, true); } } /** * 目标方法执行后 * @param objInst 执行方法的目标对象 * @param method 目标方法 * @param allArguments 方法参数 * @param argumentsTypes 参数类型 * @param ret 返回值 * **/ @Override public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Object ret) throws Throwable { ContextManager.getRuntimeContext().remove(Constants.FORWARD_REQUEST_FLAG); return ret; } /*** * 处理异常 * @param objInst 执行方法的目标对象 * @param method 目标方法 * @param allArguments 方法参数 * @param argumentsTypes 参数类型 * @param t 异常 * */ @Override public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Throwable t) { } /** * 构造方法执行后 * @param objInst 目标对象 * @param allArguments 构造器参数 * */ @Override public void onConstruct(EnhancedInstance objInst, Object[] allArguments) { objInst.setSkyWalkingDynamicField(allArguments[1]); } } 下面代码是描述 tomcat-7.x/8.x=org.apache.skywalking.apm.plugin.tomcat78x.define.TomcatInstrumentation tomcat-7.x/8.x=org.apache.skywalking.apm.plugin.tomcat78x.define.ApplicationDispatcherInstrumentation 好了,现在你已经能够熟练的编写一个插件了。 六、清理 ClassLoader 的插件 根据 webLogic 的特性,需要增强 WebAppModule 这个类,这个类安装应用只会创建一次。这是个很好的人选。那么生命周期监听器在那里添加呢? WebAppModule 这个类提供了获取 WebApplicationContext 对象的方法,只需要在 WebAppModule 初始化方法 init 调用完毕之后,就直接把 listener 添加到 WebApplicationContext 中去。 listener 的具体逻辑是,在 postStart 方法里持有 ChangeAwareClassLoader 引用,然后在 postStop 方法里进行清理。清理不用说了,反射直接莽。 定义: 插件: 描述: 一切准备就绪,只需要打成 jar 包,丢到 skywalking 到 plugin 目录即可。 七、事成之后 加入插件之后,可以用 jdk 自带的调试工具来欣赏一下期待已久的 Metaspace 内存使用图。 weblogic 还是太坑了,主要不是 weblogic 的问题,是使用新技术与不敢去老技术栈的矛盾问题。其实呢,全部用老技术,遵循 weblogic 这一套,也不会出幺蛾子。但现在要推新技术,老的技术栈不去,只能天天填坑。WebLogic Metaspace OOM 解决案例2021-01-19T22:01:25+08:002021-01-19T22:01:25+08:00https://onew.me/java/2021/01/19/Weblogic-Metaspace-OOM<h1 id="weblogic-metaspace-oom-解决案例">WebLogic Metaspace OOM 解决案例</h1>
<h2 id="一前言">一、前言</h2>
<p> 估计也只有我这么惨了,都0202年了还在用weblogic这种上古神器。故事要从前段时间说起,至于是多久时间,我也忘记了。</p>
<p> 某日,线上发布版本,在weblogic控制台更新的时候,直接卡死无响应。一打开日志一瞧,好家伙,OOM了,还是个metaspace的OOM。</p>
<p> 这玩意儿就有点奇怪了,metaspace按道理是存放的类信息,字面量(Literal)、类静态变量(Class Static)、符号引用(Symbols Reference)等相关信息。类相关信息在metaspace里面又分为2块区域,<strong>Klass MetaSpace</strong>和<strong>NoKlass MetaSpace</strong>。这就不细讲了,不然扯不完。</p>
<p> 也就说一般情况下这玩意儿是不会OOM掉的(除开metaspace大小设置不合理的情况)</p>
<h2 id="二分析">二、分析</h2>
<p> 结合实际情况Metaspace OOM 可能的情况是,以下2种情况:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- 大量使用反射,由于JVM的优化机制,会定义一些类出来,导致类加载数量增多。
- JAXB BUG 导致,网上有很多文章在分析
</code></pre></div></div>
<h3 id="21-情况一">2.1 情况一:</h3>
<p> 的确项目里面存在大量的反射,再说了使用了spring 框架,反射是避免不了的,这个没办法。但是这种情况说不通,就算类大量的增长,但从未见过有卸载类的情况。排除~!</p>
<h3 id="22-情况二">2.2 情况二:</h3>
<p> JAXB 这个情况的确可能存在,毕竟是老项目,但这个没有实际的证据,需要进一步的进行分析。</p>
<h3 id="23-什么时候卸载类">2.3 什么时候卸载类?</h3>
<p> 卸载类要满足3个条件,GC才会对其进行卸载,并回收空间:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- 该类所有的实例已经被回收
- 加载该类的CLassLoader已经被回收
- 该类对应的CLass对象没有任何引用
</code></pre></div></div>
<p>看得出,卸载一个类条件比较苛刻,那就按照上述3个条件进行问题排查。</p>
<h2 id="三排查">三、排查</h2>
<h3 id="31-复现">3.1 复现</h3>
<p> 解决问题的前提是能够复现问题,好在这次问题比较容易复现出来。</p>
<ul>
<li>环境:</li>
<li>weblogic 12c</li>
<li>jdk 1.8</li>
<li>metaspace 512M maxMetaspace1024M</li>
<li>步骤:
<ul>
<li>在控制台中使用更新功能,重复部署多次</li>
</ul>
</li>
<li>观察:
<ul>
<li>使用jdk自带<em>Java VisualVM</em>,观察metaspace内存的增长,以及一个class的加载数量和卸载数量</li>
</ul>
</li>
<li>现象:
<ul>
<li>class一直在增长,没有出现过大幅度的下跌</li>
<li><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/Ih4A7r.jpg" alt="Ih4A7r" /></li>
<li>总共更新了2次载入了7W+的类,卸载却不到3K,这个结果就离谱。</li>
</ul>
</li>
</ul>
<h3 id="32-内存分析">3.2 内存分析</h3>
<p> 把heap dump下来,看看。到底是啥导致没有卸载。前文说了,卸载一个类要满足3个条件。那就按照3个条件进行分析。</p>
<p> 但加载类是在太多,不可能一个一个的去分析。从3个条件来看,分析classloader是最靠谱的,毕竟所有类的加载都是由classloader进行加载的,而且classloader数量相对较少。</p>
<p> 通过mat分析,检测出有3个问题,2个都是ChangeAwareCloader:</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/VSAoKt.jpg" alt="VSAoKt" /></p>
<p>看来方向没错,去weblogic官方看了一下,上图中的classloader是负责更新class的。点开详情看一下,发现是nacos的线程hold住了classloader导致,嘿嘿破案了。</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/xGL86x.jpg" alt="xGL86x" /></p>
<h3 id="33-这该死的线程">3.3 这该死的线程</h3>
<p> 通过上面的分析,发现是nacos在spring停止的时候并没有停止相关线程,导致该线程一直在后台活跃。由于线程没有退出,那么相应的classloader就不能被回收。</p>
<p> 我TM反手一个<a href="https://github.com/alibaba/spring-cloud-alibaba/pull/1892">pr</a>到nacos。</p>
<h3 id="34-验证">3.4 验证</h3>
<p> 问题原因找到了,就替换掉原由项目的nacos,换上一个停止spring的时候销毁nacos线程版本,验证一下是否解决。</p>
<p> 由于是线程引起的,所以在验证的过程中,要格外注意,nacos线程是否被正常关闭。</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/yT7j6g.jpg" alt="yT7j6g" /></p>
<p>上图为weblogic一个线程截图。可以看到nacos相关的线程有6个。此时停止应用,nacos线程已经被正常的销毁了。</p>
<p> 线程已经被正常销毁,再来验证是否能够正常卸载class。重复部署2次,再进行观察。</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/R0d9xk.jpg" alt="R0d9xk" /></p>
<p> 还是离谱,依旧没有被卸载,看来问题没有被根本解决。会不会是还有啥线程没有被关闭呢。再去找找看看。先停止应用,看看哪些线程还在后台运行。</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/NBo2Ot.jpg" alt="NBo2Ot" /></p>
<p>扒了一下,还有一个线程没有正常销毁。改改代码再试一下吧0.0.</p>
<h3 id="35-还是这该死的线程">3.5 还是这该死的线程</h3>
<p> 虽然把nacos的线程给销毁了,但还有业务线程还在跑,再测试一把,看看能不能正常的回收class。经过测试没有出现可以的线程了。感觉自己又行了。</p>
<p> 继续测试,重复部署,验证是否能够正常卸载class。</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/Ek1XEF.jpg" alt="Ek1XEF" /></p>
<p>还是离谱,加载了7W多的类,卸载才4K多点,这还是不正常。果然,这个工程的问题很多呀。</p>
<h3 id="36-重新分析内存讨厌的监控">3.6 重新分析内存,讨厌的监控</h3>
<p> 线程的问题解决了,但问题依旧,只能再dump一份内存看看。</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/NSP45r.jpg" alt="NSP45r" /></p>
<p>问题还是在classLoader上,去详情看看。</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/bALy32.jpg" alt="bALy32" /></p>
<p>classloader被Logger给hold住了,这有点奇怪了。由上图可以看出,changeAwareClassLoader加载了LoggingHandler,在Logger中引用了LoggingHandler,这个Logger是系统类,</p>
<p> 由于Logger是系统类,由jvm的<code class="language-plaintext highlighter-rouge">Bootstrap ClassLoader</code>加载,这个classloader的生命周期就很长了,只有jvm进程退出,才会被销毁掉。</p>
<p> 只能翻一下LoggingHandler的代码,看下为啥要去跟Logger扯上关系。</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/l7z3FW.jpg" alt="l7z3FW" /></p>
<p>这个玩意儿在启动的时候回去注册一下,获取的是系统的Logger,怪不得会扯上关系。不知道为啥没有被取消注册,取消注册的方法倒是有个。</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/vuWgup.jpg" alt="vuWgup" /></p>
<p>猜测是jar包版本冲突导致出现了异常,就没有把取消注册流程给走完。问了一下同事,说这个LoggingHandler是属于一个监控,这个监控比较老,可以直接下掉。那就不去纠结为啥没有取消注册了,直接下掉看疗效。</p>
<h3 id="37-重新验证">3.7 重新验证</h3>
<p> 把监控的jar包下掉,看看能不能达到预期的效果。</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/mE705e.jpg" alt="mE705e" /></p>
<p>weblogic初始状态,一片祥和。</p>
<p>重复部署3次:</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/EB4BE2.jpg" alt="EB4BE2" /></p>
<p>还是没卸载,离谱,看来要翻车了。不急陪他耍耍,等他个10分钟,看他自己投降。是不是觉得是玄学😂。对,还真不是玄学,有些东西没有及时释放,是因为在finalize队列中排队呢,等一下就好。</p>
<p>10分钟之后,不对应该是出去吃饭过后:</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/Fcw936.jpg" alt="Fcw936" /></p>
<p>metaspace的占用水平已回归到正常的水平,类卸载从4000到了28000。</p>
<p>反复部署后,metaspace内存也稳定到600M。ok,完美解决。</p>
<p></p>{"nick"=>"onew", "link"=>"https://onew.me"}WebLogic Metaspace OOM 解决案例 一、前言 估计也只有我这么惨了,都0202年了还在用weblogic这种上古神器。故事要从前段时间说起,至于是多久时间,我也忘记了。 某日,线上发布版本,在weblogic控制台更新的时候,直接卡死无响应。一打开日志一瞧,好家伙,OOM了,还是个metaspace的OOM。 这玩意儿就有点奇怪了,metaspace按道理是存放的类信息,字面量(Literal)、类静态变量(Class Static)、符号引用(Symbols Reference)等相关信息。类相关信息在metaspace里面又分为2块区域,Klass MetaSpace和NoKlass MetaSpace。这就不细讲了,不然扯不完。 也就说一般情况下这玩意儿是不会OOM掉的(除开metaspace大小设置不合理的情况) 二、分析 结合实际情况Metaspace OOM 可能的情况是,以下2种情况: - 大量使用反射,由于JVM的优化机制,会定义一些类出来,导致类加载数量增多。 - JAXB BUG 导致,网上有很多文章在分析 2.1 情况一: 的确项目里面存在大量的反射,再说了使用了spring 框架,反射是避免不了的,这个没办法。但是这种情况说不通,就算类大量的增长,但从未见过有卸载类的情况。排除~! 2.2 情况二: JAXB 这个情况的确可能存在,毕竟是老项目,但这个没有实际的证据,需要进一步的进行分析。 2.3 什么时候卸载类? 卸载类要满足3个条件,GC才会对其进行卸载,并回收空间: - 该类所有的实例已经被回收 - 加载该类的CLassLoader已经被回收 - 该类对应的CLass对象没有任何引用 看得出,卸载一个类条件比较苛刻,那就按照上述3个条件进行问题排查。 三、排查 3.1 复现 解决问题的前提是能够复现问题,好在这次问题比较容易复现出来。 环境: weblogic 12c jdk 1.8 metaspace 512M maxMetaspace1024M 步骤: 在控制台中使用更新功能,重复部署多次 观察: 使用jdk自带Java VisualVM,观察metaspace内存的增长,以及一个class的加载数量和卸载数量 现象: class一直在增长,没有出现过大幅度的下跌 总共更新了2次载入了7W+的类,卸载却不到3K,这个结果就离谱。 3.2 内存分析 把heap dump下来,看看。到底是啥导致没有卸载。前文说了,卸载一个类要满足3个条件。那就按照3个条件进行分析。 但加载类是在太多,不可能一个一个的去分析。从3个条件来看,分析classloader是最靠谱的,毕竟所有类的加载都是由classloader进行加载的,而且classloader数量相对较少。 通过mat分析,检测出有3个问题,2个都是ChangeAwareCloader: 看来方向没错,去weblogic官方看了一下,上图中的classloader是负责更新class的。点开详情看一下,发现是nacos的线程hold住了classloader导致,嘿嘿破案了。 3.3 这该死的线程 通过上面的分析,发现是nacos在spring停止的时候并没有停止相关线程,导致该线程一直在后台活跃。由于线程没有退出,那么相应的classloader就不能被回收。 我TM反手一个pr到nacos。 3.4 验证 问题原因找到了,就替换掉原由项目的nacos,换上一个停止spring的时候销毁nacos线程版本,验证一下是否解决。 由于是线程引起的,所以在验证的过程中,要格外注意,nacos线程是否被正常关闭。 上图为weblogic一个线程截图。可以看到nacos相关的线程有6个。此时停止应用,nacos线程已经被正常的销毁了。 线程已经被正常销毁,再来验证是否能够正常卸载class。重复部署2次,再进行观察。 还是离谱,依旧没有被卸载,看来问题没有被根本解决。会不会是还有啥线程没有被关闭呢。再去找找看看。先停止应用,看看哪些线程还在后台运行。 扒了一下,还有一个线程没有正常销毁。改改代码再试一下吧0.0. 3.5 还是这该死的线程 虽然把nacos的线程给销毁了,但还有业务线程还在跑,再测试一把,看看能不能正常的回收class。经过测试没有出现可以的线程了。感觉自己又行了。 继续测试,重复部署,验证是否能够正常卸载class。 还是离谱,加载了7W多的类,卸载才4K多点,这还是不正常。果然,这个工程的问题很多呀。 3.6 重新分析内存,讨厌的监控 线程的问题解决了,但问题依旧,只能再dump一份内存看看。 问题还是在classLoader上,去详情看看。 classloader被Logger给hold住了,这有点奇怪了。由上图可以看出,changeAwareClassLoader加载了LoggingHandler,在Logger中引用了LoggingHandler,这个Logger是系统类, 由于Logger是系统类,由jvm的Bootstrap ClassLoader加载,这个classloader的生命周期就很长了,只有jvm进程退出,才会被销毁掉。 只能翻一下LoggingHandler的代码,看下为啥要去跟Logger扯上关系。 这个玩意儿在启动的时候回去注册一下,获取的是系统的Logger,怪不得会扯上关系。不知道为啥没有被取消注册,取消注册的方法倒是有个。 猜测是jar包版本冲突导致出现了异常,就没有把取消注册流程给走完。问了一下同事,说这个LoggingHandler是属于一个监控,这个监控比较老,可以直接下掉。那就不去纠结为啥没有取消注册了,直接下掉看疗效。 3.7 重新验证 把监控的jar包下掉,看看能不能达到预期的效果。 weblogic初始状态,一片祥和。 重复部署3次: 还是没卸载,离谱,看来要翻车了。不急陪他耍耍,等他个10分钟,看他自己投降。是不是觉得是玄学😂。对,还真不是玄学,有些东西没有及时释放,是因为在finalize队列中排队呢,等一下就好。 10分钟之后,不对应该是出去吃饭过后: metaspace的占用水平已回归到正常的水平,类卸载从4000到了28000。 反复部署后,metaspace内存也稳定到600M。ok,完美解决。 spring-boot2 idea jsp 404 问题探究(tomcat启动流程探究)2020-03-14T09:20:25+08:002020-03-14T09:20:25+08:00https://onew.me/spring/2020/03/14/debug-spring-boot2-jsp<h1 id="一前言">一、前言</h1>
<p> 最近有小朋友在学习spring boot的时候遇到了一个问题,按照教程上操作始终是404.于是就百事不得其解.问我的时候,我也一脸蒙B,毕竟jsp这玩意儿好久都没碰到过了,之前碰jsp的时候还是在ssh的时候.</p>
<p> 既然遇到问题就来分析一下呗,趁着最近在看spring的源码.</p>
<h1 id="二案发现场">二、案发现场</h1>
<p>ymal:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">server</span><span class="pi">:</span>
<span class="na">port</span><span class="pi">:</span> <span class="m">9090</span>
<span class="na">spring</span><span class="pi">:</span>
<span class="na">mvc</span><span class="pi">:</span>
<span class="na">view</span><span class="pi">:</span>
<span class="na">prefix</span><span class="pi">:</span> <span class="s">/WEB-INF/jsp/</span>
<span class="na">suffix</span><span class="pi">:</span> <span class="s">.jsp</span>
</code></pre></div></div>
<p>pom:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nt"><dependencies></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-starter-aop<span class="nt"></artifactId></span>
<span class="nt"></dependency></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-starter-web<span class="nt"></artifactId></span>
<span class="nt"></dependency></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-starter-test<span class="nt"></artifactId></span>
<span class="nt"><scope></span>test<span class="nt"></scope></span>
<span class="nt"></dependency></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.apache.tomcat.embed<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>tomcat-embed-jasper<span class="nt"></artifactId></span>
<span class="nt"></dependency></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-starter-tomcat<span class="nt"></artifactId></span>
<span class="nt"></dependency></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>javax.servlet<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>jstl<span class="nt"></artifactId></span>
<span class="nt"></dependency></span>
<span class="nt"></dependencies></span>
<span class="nt"><build></span>
<span class="nt"><plugins></span>
<span class="nt"><plugin></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-maven-plugin<span class="nt"></artifactId></span>
<span class="nt"></plugin></span>
<span class="nt"></plugins></span>
<span class="nt"></build></span>
</code></pre></div></div>
<p>代码:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Controller</span>
<span class="nd">@EnableAutoConfiguration</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">App</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">App</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">"/test"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">String</span> <span class="nf">test</span><span class="o">(){</span>
<span class="k">return</span> <span class="s">"index"</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 按照以上代码,应该是没有什么问题的,毕竟人家的教程也是这么做的,只不过别人是用eclipse,他是用的idea罢了.我也懒得去分析为啥eclipse没得问题了,直接来看看为啥会有这个问题.</p>
<h1 id="三分析">三、分析</h1>
<p> 众所周知,spring boot只是在spring上面包了一层皮,里面还是利用了spring的一些机制来完成,当然加载自动化配置,开箱即用,感觉很智能.</p>
<p> springboot为我们开发者省去了很多配置上的麻烦,大部分都默认配置好了,但是虽然便利了开发者,但也带来了一些麻烦,就如这个问题,整个日子输出窗口都没有日志显示为啥会是404,文件明明在那,为啥会找不到呢?</p>
<p> 要解决这个问题,就要从springBoot的自动配置上入手.按照上面的配置,用的是嵌入式的tomcat,那么就从tomcat的配置开始.</p>
<h2 id="31-servletwebserverfactoryconfiguration">3.1 ServletWebServerFactoryConfiguration</h2>
<p> 当使用springboot的wen功能的时候,有个关键的配置就避免不了了,代码如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Configuration</span><span class="o">(</span><span class="n">proxyBeanMethods</span> <span class="o">=</span> <span class="kc">false</span><span class="o">)</span>
<span class="kd">class</span> <span class="nc">ServletWebServerFactoryConfiguration</span> <span class="o">{</span>
<span class="c1">// 判断classpath是否存在 Servlet,Tomcat,UpgradeProtocol类</span>
<span class="c1">// 如果存在就启用此配置</span>
<span class="c1">// 当然还要 ServletWebServerFactory 期子类没有 在容器中</span>
<span class="nd">@Configuration</span><span class="o">(</span><span class="n">proxyBeanMethods</span> <span class="o">=</span> <span class="kc">false</span><span class="o">)</span>
<span class="nd">@ConditionalOnClass</span><span class="o">({</span> <span class="nc">Servlet</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="nc">Tomcat</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="nc">UpgradeProtocol</span><span class="o">.</span><span class="na">class</span> <span class="o">})</span>
<span class="nd">@ConditionalOnMissingBean</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="nc">ServletWebServerFactory</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="n">search</span> <span class="o">=</span> <span class="nc">SearchStrategy</span><span class="o">.</span><span class="na">CURRENT</span><span class="o">)</span>
<span class="kd">static</span> <span class="kd">class</span> <span class="nc">EmbeddedTomcat</span> <span class="o">{</span>
<span class="nd">@Bean</span>
<span class="nc">TomcatServletWebServerFactory</span> <span class="nf">tomcatServletWebServerFactory</span><span class="o">(</span>
<span class="nc">ObjectProvider</span><span class="o"><</span><span class="nc">TomcatConnectorCustomizer</span><span class="o">></span> <span class="n">connectorCustomizers</span><span class="o">,</span>
<span class="nc">ObjectProvider</span><span class="o"><</span><span class="nc">TomcatContextCustomizer</span><span class="o">></span> <span class="n">contextCustomizers</span><span class="o">,</span>
<span class="nc">ObjectProvider</span><span class="o"><</span><span class="nc">TomcatProtocolHandlerCustomizer</span><span class="o"><?>></span> <span class="n">protocolHandlerCustomizers</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// 创建tomcat 工厂</span>
<span class="nc">TomcatServletWebServerFactory</span> <span class="n">factory</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">TomcatServletWebServerFactory</span><span class="o">();</span>
<span class="n">factory</span><span class="o">.</span><span class="na">getTomcatConnectorCustomizers</span><span class="o">()</span>
<span class="o">.</span><span class="na">addAll</span><span class="o">(</span><span class="n">connectorCustomizers</span><span class="o">.</span><span class="na">orderedStream</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="n">factory</span><span class="o">.</span><span class="na">getTomcatContextCustomizers</span><span class="o">()</span>
<span class="o">.</span><span class="na">addAll</span><span class="o">(</span><span class="n">contextCustomizers</span><span class="o">.</span><span class="na">orderedStream</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="n">factory</span><span class="o">.</span><span class="na">getTomcatProtocolHandlerCustomizers</span><span class="o">()</span>
<span class="o">.</span><span class="na">addAll</span><span class="o">(</span><span class="n">protocolHandlerCustomizers</span><span class="o">.</span><span class="na">orderedStream</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="k">return</span> <span class="n">factory</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="cm">/**
* Nested configuration if Jetty is being used.
* 同tomcat的逻辑
*/</span>
<span class="nd">@Configuration</span><span class="o">(</span><span class="n">proxyBeanMethods</span> <span class="o">=</span> <span class="kc">false</span><span class="o">)</span>
<span class="nd">@ConditionalOnClass</span><span class="o">({</span> <span class="nc">Servlet</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="nc">Server</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="nc">Loader</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="nc">WebAppContext</span><span class="o">.</span><span class="na">class</span> <span class="o">})</span>
<span class="nd">@ConditionalOnMissingBean</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="nc">ServletWebServerFactory</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="n">search</span> <span class="o">=</span> <span class="nc">SearchStrategy</span><span class="o">.</span><span class="na">CURRENT</span><span class="o">)</span>
<span class="kd">static</span> <span class="kd">class</span> <span class="nc">EmbeddedJetty</span> <span class="o">{</span>
<span class="nd">@Bean</span>
<span class="nc">JettyServletWebServerFactory</span> <span class="nf">JettyServletWebServerFactory</span><span class="o">(</span>
<span class="nc">ObjectProvider</span><span class="o"><</span><span class="nc">JettyServerCustomizer</span><span class="o">></span> <span class="n">serverCustomizers</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">JettyServletWebServerFactory</span> <span class="n">factory</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">JettyServletWebServerFactory</span><span class="o">();</span>
<span class="n">factory</span><span class="o">.</span><span class="na">getServerCustomizers</span><span class="o">().</span><span class="na">addAll</span><span class="o">(</span><span class="n">serverCustomizers</span><span class="o">.</span><span class="na">orderedStream</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="k">return</span> <span class="n">factory</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="cm">/**
* Nested configuration if Undertow is being used.
* 同tomcat的逻辑
*/</span>
<span class="nd">@Configuration</span><span class="o">(</span><span class="n">proxyBeanMethods</span> <span class="o">=</span> <span class="kc">false</span><span class="o">)</span>
<span class="nd">@ConditionalOnClass</span><span class="o">({</span> <span class="nc">Servlet</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="nc">Undertow</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="nc">SslClientAuthMode</span><span class="o">.</span><span class="na">class</span> <span class="o">})</span>
<span class="nd">@ConditionalOnMissingBean</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="nc">ServletWebServerFactory</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="n">search</span> <span class="o">=</span> <span class="nc">SearchStrategy</span><span class="o">.</span><span class="na">CURRENT</span><span class="o">)</span>
<span class="kd">static</span> <span class="kd">class</span> <span class="nc">EmbeddedUndertow</span> <span class="o">{</span>
<span class="nd">@Bean</span>
<span class="nc">UndertowServletWebServerFactory</span> <span class="nf">undertowServletWebServerFactory</span><span class="o">(</span>
<span class="nc">ObjectProvider</span><span class="o"><</span><span class="nc">UndertowDeploymentInfoCustomizer</span><span class="o">></span> <span class="n">deploymentInfoCustomizers</span><span class="o">,</span>
<span class="nc">ObjectProvider</span><span class="o"><</span><span class="nc">UndertowBuilderCustomizer</span><span class="o">></span> <span class="n">builderCustomizers</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">UndertowServletWebServerFactory</span> <span class="n">factory</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">UndertowServletWebServerFactory</span><span class="o">();</span>
<span class="n">factory</span><span class="o">.</span><span class="na">getDeploymentInfoCustomizers</span><span class="o">()</span>
<span class="o">.</span><span class="na">addAll</span><span class="o">(</span><span class="n">deploymentInfoCustomizers</span><span class="o">.</span><span class="na">orderedStream</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="n">factory</span><span class="o">.</span><span class="na">getBuilderCustomizers</span><span class="o">().</span><span class="na">addAll</span><span class="o">(</span><span class="n">builderCustomizers</span><span class="o">.</span><span class="na">orderedStream</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="k">return</span> <span class="n">factory</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 这里可以看到,配置类按照classpath中的类创建了不同的<code class="language-plaintext highlighter-rouge">ServletWebServerFactory</code>,本文这里加入了tomcat,所以这里将会创建<code class="language-plaintext highlighter-rouge">TomcatServletWebServerFactory</code>.</p>
<p> 当然光看这个还是不行的,要明白为啥会这么创建,那么这一切要从springboot的启动流程开始分析才能解释整个情况.</p>
<h1 id="四springboot启动流程分析">四、SpringBoot启动流程分析</h1>
<p> 太阳底下无新鲜事,来揭开名为方便的面纱.当然这只是初步的探讨.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@EnableAutoConfiguration</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Main</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">Main</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> 熟悉springBoot的人看到这样的写法是否是感到平淡无奇?那么这短短的一行代码后面到底发生了啥??</p>
<h2 id="41-springapplicationrun">4.1 SpringApplication.run</h2>
<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">ConfigurableApplicationContext</span> <span class="nf">run</span><span class="o">(</span><span class="nc">Class</span><span class="o"><?></span> <span class="n">primarySource</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="k">return</span> <span class="nf">run</span><span class="o">(</span><span class="k">new</span> <span class="nc">Class</span><span class="o"><?>[]</span> <span class="o">{</span> <span class="n">primarySource</span> <span class="o">},</span> <span class="n">args</span><span class="o">);</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="nc">ConfigurableApplicationContext</span> <span class="nf">run</span><span class="o">(</span><span class="nc">Class</span><span class="o"><?>[]</span> <span class="n">primarySources</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="k">return</span> <span class="k">new</span> <span class="nf">SpringApplication</span><span class="o">(</span><span class="n">primarySources</span><span class="o">).</span><span class="na">run</span><span class="o">(</span><span class="n">args</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 创建了个<code class="language-plaintext highlighter-rouge">SpringApplication</code>对象在run?看看构造函数是否有啥逻辑.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nf">SpringApplication</span><span class="o">(</span><span class="nc">Class</span><span class="o"><?>...</span> <span class="n">primarySources</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">(</span><span class="kc">null</span><span class="o">,</span> <span class="n">primarySources</span><span class="o">);</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="nf">SpringApplication</span><span class="o">(</span><span class="nc">ResourceLoader</span> <span class="n">resourceLoader</span><span class="o">,</span> <span class="nc">Class</span><span class="o"><?>...</span> <span class="n">primarySources</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">resourceLoader</span> <span class="o">=</span> <span class="n">resourceLoader</span><span class="o">;</span>
<span class="nc">Assert</span><span class="o">.</span><span class="na">notNull</span><span class="o">(</span><span class="n">primarySources</span><span class="o">,</span> <span class="s">"PrimarySources must not be null"</span><span class="o">);</span>
<span class="k">this</span><span class="o">.</span><span class="na">primarySources</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">LinkedHashSet</span><span class="o"><>(</span><span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="n">primarySources</span><span class="o">));</span>
<span class="c1">// 判断 web 应用的类型</span>
<span class="c1">// 判断依据为 是否存在指定 DispatcherServlet,DispatcherHandler,ServletContainer,WebApplicationContext,ReactiveWebApplicationContext等类</span>
<span class="c1">// 使用 class.forName 进行查找</span>
<span class="c1">// 判断spring程序的类型</span>
<span class="k">this</span><span class="o">.</span><span class="na">webApplicationType</span> <span class="o">=</span> <span class="nc">WebApplicationType</span><span class="o">.</span><span class="na">deduceFromClasspath</span><span class="o">();</span>
<span class="c1">// 加载 META-INF/spring.factories 配置文件,并把 ApplicationContextInitializer 相关的类全部实例化</span>
<span class="n">setInitializers</span><span class="o">((</span><span class="nc">Collection</span><span class="o">)</span> <span class="n">getSpringFactoriesInstances</span><span class="o">(</span><span class="nc">ApplicationContextInitializer</span><span class="o">.</span><span class="na">class</span><span class="o">));</span>
<span class="c1">// 加载 META-INF/spring.factories 配置文件,并把 ApplicationListener 相关的类 全部实例化</span>
<span class="n">setListeners</span><span class="o">((</span><span class="nc">Collection</span><span class="o">)</span> <span class="n">getSpringFactoriesInstances</span><span class="o">(</span><span class="nc">ApplicationListener</span><span class="o">.</span><span class="na">class</span><span class="o">));</span>
<span class="c1">// 检查 main 方法所在的类</span>
<span class="k">this</span><span class="o">.</span><span class="na">mainApplicationClass</span> <span class="o">=</span> <span class="n">deduceMainApplicationClass</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 在构造的时候还是做了不少的逻辑,这里就不得不提一下spring的黑魔法了,那就是<code class="language-plaintext highlighter-rouge">SpringFactoriesLoader</code>,这个东西有点像java中的spi机制,与之不同是spring是读取的是<code class="language-plaintext highlighter-rouge">META-INF/spring.factories</code>文件.至于为啥不用spi要自己单搞个,emmmmmm.</p>
<p> 构造的逻辑很简单,不是很复杂,就是检测一下要启动什么类型的spring,具体操作是在<code class="language-plaintext highlighter-rouge">WebApplicationType.deduceFromClasspath();</code>,这个类型判断还是很重要的,后面创建spring上下文的时候会用得上.</p>
<h2 id="42-springapplicationrun">4.2 springApplication.run</h2>
<p> 对象创建好了,又要继续run了.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">ConfigurableApplicationContext</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="c1">// 创建一个用于记录 启动-关闭 时间的 StopWatch</span>
<span class="nc">StopWatch</span> <span class="n">stopWatch</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">StopWatch</span><span class="o">();</span>
<span class="n">stopWatch</span><span class="o">.</span><span class="na">start</span><span class="o">();</span>
<span class="nc">ConfigurableApplicationContext</span> <span class="n">context</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
<span class="nc">Collection</span><span class="o"><</span><span class="nc">SpringBootExceptionReporter</span><span class="o">></span> <span class="n">exceptionReporters</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o"><>();</span>
<span class="c1">// 设置环境变量</span>
<span class="n">configureHeadlessProperty</span><span class="o">();</span>
<span class="c1">// 创建 EventPublishingRunListener</span>
<span class="c1">// 相当于是个组合模式,所有listener 都集中在 SpringApplicationRunListeners 中</span>
<span class="nc">SpringApplicationRunListeners</span> <span class="n">listeners</span> <span class="o">=</span> <span class="n">getRunListeners</span><span class="o">(</span><span class="n">args</span><span class="o">);</span>
<span class="c1">// 启动容器,发送时间</span>
<span class="n">listeners</span><span class="o">.</span><span class="na">starting</span><span class="o">();</span>
<span class="k">try</span> <span class="o">{</span>
<span class="c1">// 把 args 封装为对象,映射到环境中</span>
<span class="nc">ApplicationArguments</span> <span class="n">applicationArguments</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DefaultApplicationArguments</span><span class="o">(</span><span class="n">args</span><span class="o">);</span>
<span class="c1">// 初始化环境</span>
<span class="nc">ConfigurableEnvironment</span> <span class="n">environment</span> <span class="o">=</span> <span class="n">prepareEnvironment</span><span class="o">(</span><span class="n">listeners</span><span class="o">,</span> <span class="n">applicationArguments</span><span class="o">);</span>
<span class="c1">// 设置环境变量 spring.beaninfo.ignore</span>
<span class="n">configureIgnoreBeanInfo</span><span class="o">(</span><span class="n">environment</span><span class="o">);</span>
<span class="c1">// 获取 需要打印的 Banner 并把 banner 打印到控制台</span>
<span class="nc">Banner</span> <span class="n">printedBanner</span> <span class="o">=</span> <span class="n">printBanner</span><span class="o">(</span><span class="n">environment</span><span class="o">);</span>
<span class="c1">// 根据不同的类型 创建不同的上下文</span>
<span class="n">context</span> <span class="o">=</span> <span class="n">createApplicationContext</span><span class="o">();</span>
<span class="c1">// 获取 所有 SpringBootExceptionReporter 相关的类</span>
<span class="n">exceptionReporters</span> <span class="o">=</span> <span class="n">getSpringFactoriesInstances</span><span class="o">(</span><span class="nc">SpringBootExceptionReporter</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
<span class="k">new</span> <span class="nc">Class</span><span class="o">[]</span> <span class="o">{</span> <span class="nc">ConfigurableApplicationContext</span><span class="o">.</span><span class="na">class</span> <span class="o">},</span> <span class="n">context</span><span class="o">);</span>
<span class="c1">// 准备上下文</span>
<span class="n">prepareContext</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">environment</span><span class="o">,</span> <span class="n">listeners</span><span class="o">,</span> <span class="n">applicationArguments</span><span class="o">,</span> <span class="n">printedBanner</span><span class="o">);</span>
<span class="c1">// 刷新上下文,发送事件</span>
<span class="n">refreshContext</span><span class="o">(</span><span class="n">context</span><span class="o">);</span>
<span class="c1">// 模板方法</span>
<span class="n">afterRefresh</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">applicationArguments</span><span class="o">);</span>
<span class="c1">// 停止</span>
<span class="n">stopWatch</span><span class="o">.</span><span class="na">stop</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">logStartupInfo</span><span class="o">)</span> <span class="o">{</span>
<span class="k">new</span> <span class="nf">StartupInfoLogger</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">mainApplicationClass</span><span class="o">).</span><span class="na">logStarted</span><span class="o">(</span><span class="n">getApplicationLog</span><span class="o">(),</span> <span class="n">stopWatch</span><span class="o">);</span>
<span class="o">}</span>
<span class="c1">// 通知监听器,已经启动</span>
<span class="n">listeners</span><span class="o">.</span><span class="na">started</span><span class="o">(</span><span class="n">context</span><span class="o">);</span>
<span class="c1">// 调用 runner的 run 方法</span>
<span class="n">callRunners</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">applicationArguments</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">catch</span> <span class="o">(</span><span class="nc">Throwable</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// 处理运行时的错误</span>
<span class="n">handleRunFailure</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">ex</span><span class="o">,</span> <span class="n">exceptionReporters</span><span class="o">,</span> <span class="n">listeners</span><span class="o">);</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalStateException</span><span class="o">(</span><span class="n">ex</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">try</span> <span class="o">{</span>
<span class="c1">// 通知监听器,正在运行</span>
<span class="n">listeners</span><span class="o">.</span><span class="na">running</span><span class="o">(</span><span class="n">context</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">catch</span> <span class="o">(</span><span class="nc">Throwable</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// 处理运行时的错误</span>
<span class="n">handleRunFailure</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">ex</span><span class="o">,</span> <span class="n">exceptionReporters</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="nf">IllegalStateException</span><span class="o">(</span><span class="n">ex</span><span class="o">);</span>
<span class="o">}</span>
<span class="c1">// 返回上下文</span>
<span class="k">return</span> <span class="n">context</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 由于本文不是分析代码的文章,所以关注点放在创建spring上下文和上下文的操作上.分别是<code class="language-plaintext highlighter-rouge">createApplicationContext</code>和<code class="language-plaintext highlighter-rouge">refreshContext</code></p>
<h2 id="43-createapplicationcontext">4.3 createApplicationContext</h2>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">protected</span> <span class="nc">ConfigurableApplicationContext</span> <span class="nf">createApplicationContext</span><span class="o">()</span> <span class="o">{</span>
<span class="nc">Class</span><span class="o"><?></span> <span class="n">contextClass</span> <span class="o">=</span> <span class="k">this</span><span class="o">.</span><span class="na">applicationContextClass</span><span class="o">;</span>
<span class="k">if</span> <span class="o">(</span><span class="n">contextClass</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="k">try</span> <span class="o">{</span>
<span class="k">switch</span> <span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">webApplicationType</span><span class="o">)</span> <span class="o">{</span>
<span class="k">case</span> <span class="nl">SERVLET:</span>
<span class="n">contextClass</span> <span class="o">=</span> <span class="nc">Class</span><span class="o">.</span><span class="na">forName</span><span class="o">(</span><span class="no">DEFAULT_SERVLET_WEB_CONTEXT_CLASS</span><span class="o">);</span>
<span class="k">break</span><span class="o">;</span>
<span class="k">case</span> <span class="nl">REACTIVE:</span>
<span class="n">contextClass</span> <span class="o">=</span> <span class="nc">Class</span><span class="o">.</span><span class="na">forName</span><span class="o">(</span><span class="no">DEFAULT_REACTIVE_WEB_CONTEXT_CLASS</span><span class="o">);</span>
<span class="k">break</span><span class="o">;</span>
<span class="k">default</span><span class="o">:</span>
<span class="n">contextClass</span> <span class="o">=</span> <span class="nc">Class</span><span class="o">.</span><span class="na">forName</span><span class="o">(</span><span class="no">DEFAULT_CONTEXT_CLASS</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="k">catch</span> <span class="o">(</span><span class="nc">ClassNotFoundException</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalStateException</span><span class="o">(</span>
<span class="s">"Unable create a default ApplicationContext, please specify an ApplicationContextClass"</span><span class="o">,</span> <span class="n">ex</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="k">return</span> <span class="o">(</span><span class="nc">ConfigurableApplicationContext</span><span class="o">)</span> <span class="nc">BeanUtils</span><span class="o">.</span><span class="na">instantiateClass</span><span class="o">(</span><span class="n">contextClass</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 这里的类型,是按照之前构造方法中的类型进行创建的,如果不清楚的可以往上翻一哈.</p>
<ol>
<li>如果类型为: SERVLET 就创建 AnnotationConfigServletWebServerApplicationContext</li>
<li>如果类型为: REACTIVE 就创建 AnnotationConfigReactiveWebServerApplicationContext</li>
<li>默认创建 : AnnotationConfigApplicationContext</li>
</ol>
<p> 很显然这里的类型是 SERVLET 所以创建了 AnnotationConfigServletWebServerApplicationContext,继承关系如下.</p>
<p><img src="https://itinfo.oss-cn-hongkong.aliyuncs.com/img/LgjEPe.jpg" alt="LgjEPe" /></p>
<p> 熟悉spring的同学是不是感觉与<code class="language-plaintext highlighter-rouge">ClassPathXmlApplicationContext</code>差不多?我觉得是差不多的,只是干事的方式有点区别.</p>
<p> 这里把对象创建完了,然后进行一顿骚操作,设置值,环境等等.不再这里进行分析.要看的关键点是<code class="language-plaintext highlighter-rouge">refreshContext</code>.</p>
<h2 id="45-refreshcontext">4.5 refreshContext</h2>
<p> 刷新上下文,这里刷新会有什么骚操作呢?来瞧瞧就知道了.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">void</span> <span class="nf">refreshContext</span><span class="o">(</span><span class="nc">ConfigurableApplicationContext</span> <span class="n">context</span><span class="o">)</span> <span class="o">{</span>
<span class="n">refresh</span><span class="o">((</span><span class="nc">ApplicationContext</span><span class="o">)</span> <span class="n">context</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">registerShutdownHook</span><span class="o">)</span> <span class="o">{</span>
<span class="k">try</span> <span class="o">{</span>
<span class="c1">// 添加关闭钩子,关闭程序时,关闭上下文 释放资源</span>
<span class="n">context</span><span class="o">.</span><span class="na">registerShutdownHook</span><span class="o">();</span>
<span class="o">}</span>
<span class="k">catch</span> <span class="o">(</span><span class="nc">AccessControlException</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// Not allowed in some environments.</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">@Deprecated</span>
<span class="kd">protected</span> <span class="kt">void</span> <span class="nf">refresh</span><span class="o">(</span><span class="nc">ApplicationContext</span> <span class="n">applicationContext</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">Assert</span><span class="o">.</span><span class="na">isInstanceOf</span><span class="o">(</span><span class="nc">ConfigurableApplicationContext</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="n">applicationContext</span><span class="o">);</span>
<span class="n">refresh</span><span class="o">((</span><span class="nc">ConfigurableApplicationContext</span><span class="o">)</span> <span class="n">applicationContext</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 虽然这个方法过时,但spring还是没有直接删除,真够良心的,不像某Final,直接删,真TMSB.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">protected</span> <span class="kt">void</span> <span class="nf">refresh</span><span class="o">(</span><span class="nc">ConfigurableApplicationContext</span> <span class="n">applicationContext</span><span class="o">)</span> <span class="o">{</span>
<span class="n">applicationContext</span><span class="o">.</span><span class="na">refresh</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 注意这里的context 实际上是<code class="language-plaintext highlighter-rouge">AnnotationConfigServletWebServerApplicationContext</code>,而<code class="language-plaintext highlighter-rouge">AnnotationConfigServletWebServerApplicationContext</code>没有重写这个方法,是继承的它父类<code class="language-plaintext highlighter-rouge">ServletWebServerApplicationContext</code>.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ServletWebServerApplicationContext</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kd">final</span> <span class="kt">void</span> <span class="nf">refresh</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">BeansException</span><span class="o">,</span> <span class="nc">IllegalStateException</span> <span class="o">{</span>
<span class="k">try</span> <span class="o">{</span>
<span class="kd">super</span><span class="o">.</span><span class="na">refresh</span><span class="o">();</span>
<span class="o">}</span>
<span class="k">catch</span> <span class="o">(</span><span class="nc">RuntimeException</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
<span class="n">stopAndReleaseWebServer</span><span class="o">();</span>
<span class="k">throw</span> <span class="n">ex</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p> <code class="language-plaintext highlighter-rouge">ServletWebServerApplicationContext</code>的父类是<code class="language-plaintext highlighter-rouge">AbstractApplicationContext</code></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// AbstractApplicationContext</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">refresh</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">BeansException</span><span class="o">,</span> <span class="nc">IllegalStateException</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="na">startupShutdownMonitor</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// Prepare this context for refreshing.</span>
<span class="c1">// 准备刷新上下文环境</span>
<span class="n">prepareRefresh</span><span class="o">();</span>
<span class="c1">// Tell the subclass to refresh the internal bean factory.</span>
<span class="c1">// 初始化beanFactory,进行xml预读取</span>
<span class="nc">ConfigurableListableBeanFactory</span> <span class="n">beanFactory</span> <span class="o">=</span> <span class="n">obtainFreshBeanFactory</span><span class="o">();</span>
<span class="c1">// Prepare the bean factory for use in this context.</span>
<span class="c1">// 对beanFactory进行填充</span>
<span class="n">prepareBeanFactory</span><span class="o">(</span><span class="n">beanFactory</span><span class="o">);</span>
<span class="k">try</span> <span class="o">{</span>
<span class="c1">// Allows post-processing of the bean factory in context subclasses.</span>
<span class="c1">// 子类覆盖方法做额外的处理</span>
<span class="n">postProcessBeanFactory</span><span class="o">(</span><span class="n">beanFactory</span><span class="o">);</span>
<span class="c1">// Invoke factory processors registered as beans in the context.</span>
<span class="c1">// 激活各种beanFactoryProcessors</span>
<span class="n">invokeBeanFactoryPostProcessors</span><span class="o">(</span><span class="n">beanFactory</span><span class="o">);</span>
<span class="c1">// Register bean processors that intercept bean creation.</span>
<span class="c1">//注册拦截bean创建的bean处理器</span>
<span class="n">registerBeanPostProcessors</span><span class="o">(</span><span class="n">beanFactory</span><span class="o">);</span>
<span class="c1">// Initialize message source for this context.</span>
<span class="c1">// 初始化上下文的消息源</span>
<span class="n">initMessageSource</span><span class="o">();</span>
<span class="c1">// Initialize event multicaster for this context.</span>
<span class="c1">// 初始化上下文的消息广播</span>
<span class="n">initApplicationEventMulticaster</span><span class="o">();</span>
<span class="c1">// Initialize other special beans in specific context subclasses.</span>
<span class="c1">// 留给子类来初始化其他的bean</span>
<span class="n">onRefresh</span><span class="o">();</span>
<span class="c1">// Check for listener beans and register them.</span>
<span class="c1">// 注册所有bean的监听器</span>
<span class="n">registerListeners</span><span class="o">();</span>
<span class="c1">// Instantiate all remaining (non-lazy-init) singletons.</span>
<span class="c1">// 初始化延迟加载的bean</span>
<span class="n">finishBeanFactoryInitialization</span><span class="o">(</span><span class="n">beanFactory</span><span class="o">);</span>
<span class="c1">// Last step: publish corresponding event.</span>
<span class="c1">// 最后一步,发布消息</span>
<span class="n">finishRefresh</span><span class="o">();</span>
<span class="o">}</span>
<span class="k">catch</span> <span class="o">(</span><span class="nc">BeansException</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">logger</span><span class="o">.</span><span class="na">isWarnEnabled</span><span class="o">())</span> <span class="o">{</span>
<span class="n">logger</span><span class="o">.</span><span class="na">warn</span><span class="o">(</span><span class="s">"Exception encountered during context initialization - "</span> <span class="o">+</span>
<span class="s">"cancelling refresh attempt: "</span> <span class="o">+</span> <span class="n">ex</span><span class="o">);</span>
<span class="o">}</span>
<span class="c1">//发生异常,销毁所有bean</span>
<span class="c1">// Destroy already created singletons to avoid dangling resources.</span>
<span class="n">destroyBeans</span><span class="o">();</span>
<span class="c1">// Reset 'active' flag.</span>
<span class="c1">// 重置flag</span>
<span class="n">cancelRefresh</span><span class="o">(</span><span class="n">ex</span><span class="o">);</span>
<span class="c1">// Propagate exception to caller.</span>
<span class="k">throw</span> <span class="n">ex</span><span class="o">;</span>
<span class="o">}</span>
<span class="k">finally</span> <span class="o">{</span>
<span class="c1">// Reset common introspection caches in Spring's core, since we</span>
<span class="c1">// might not ever need metadata for singleton beans anymore...</span>
<span class="c1">// 重置缓存</span>
<span class="n">resetCommonCaches</span><span class="o">();</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 这里的重点是<code class="language-plaintext highlighter-rouge">onRefresh</code>,这里<code class="language-plaintext highlighter-rouge">onRefresh</code>是由子类<code class="language-plaintext highlighter-rouge">ServletWebServerApplicationContext</code>进行实现的.</p>
<h2 id="46-onrefresh">4.6 onRefresh</h2>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c1">// ServletWebServerApplicationContext</span>
<span class="nd">@Override</span>
<span class="kd">protected</span> <span class="kt">void</span> <span class="nf">onRefresh</span><span class="o">()</span> <span class="o">{</span>
<span class="kd">super</span><span class="o">.</span><span class="na">onRefresh</span><span class="o">();</span>
<span class="k">try</span> <span class="o">{</span>
<span class="c1">// 创建server</span>
<span class="n">createWebServer</span><span class="o">();</span>
<span class="o">}</span>
<span class="k">catch</span> <span class="o">(</span><span class="nc">Throwable</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">ApplicationContextException</span><span class="o">(</span><span class="s">"Unable to start web server"</span><span class="o">,</span> <span class="n">ex</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 终于绕到了创建 server 这里了,不知少侠是否还记得那个配置类??<code class="language-plaintext highlighter-rouge">ServletWebServerFactoryConfiguration</code>.那么现在才真正的开始了.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">void</span> <span class="nf">createWebServer</span><span class="o">()</span> <span class="o">{</span>
<span class="nc">WebServer</span> <span class="n">webServer</span> <span class="o">=</span> <span class="k">this</span><span class="o">.</span><span class="na">webServer</span><span class="o">;</span>
<span class="c1">// 获取servlet 上下文</span>
<span class="nc">ServletContext</span> <span class="n">servletContext</span> <span class="o">=</span> <span class="n">getServletContext</span><span class="o">();</span>
<span class="c1">// 如果 server 为空 或者 servlet上下文为空,就创建server</span>
<span class="k">if</span> <span class="o">(</span><span class="n">webServer</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">&&</span> <span class="n">servletContext</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">ServletWebServerFactory</span> <span class="n">factory</span> <span class="o">=</span> <span class="n">getWebServerFactory</span><span class="o">();</span>
<span class="k">this</span><span class="o">.</span><span class="na">webServer</span> <span class="o">=</span> <span class="n">factory</span><span class="o">.</span><span class="na">getWebServer</span><span class="o">(</span><span class="n">getSelfInitializer</span><span class="o">());</span>
<span class="o">}</span>
<span class="k">else</span> <span class="nf">if</span> <span class="o">(</span><span class="n">servletContext</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="k">try</span> <span class="o">{</span>
<span class="n">getSelfInitializer</span><span class="o">().</span><span class="na">onStartup</span><span class="o">(</span><span class="n">servletContext</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">catch</span> <span class="o">(</span><span class="nc">ServletException</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">ApplicationContextException</span><span class="o">(</span><span class="s">"Cannot initialize servlet context"</span><span class="o">,</span> <span class="n">ex</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="n">initPropertySources</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>
<ol>
<li>判断server是否创建</li>
<li>未创建就创建</li>
<li>初始化</li>
<li>初始化资源</li>
</ol>
<p> 这里的 <code class="language-plaintext highlighter-rouge">getWebServerFactory()</code> 方法从容器中获取的,容器里面的是之前配置类中创建的.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 从容器中获取 ServletWebServerFactory</span>
<span class="kd">protected</span> <span class="nc">ServletWebServerFactory</span> <span class="nf">getWebServerFactory</span><span class="o">()</span> <span class="o">{</span>
<span class="c1">// Use bean names so that we don't consider the hierarchy</span>
<span class="nc">String</span><span class="o">[]</span> <span class="n">beanNames</span> <span class="o">=</span> <span class="n">getBeanFactory</span><span class="o">().</span><span class="na">getBeanNamesForType</span><span class="o">(</span><span class="nc">ServletWebServerFactory</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">beanNames</span><span class="o">.</span><span class="na">length</span> <span class="o">==</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">ApplicationContextException</span><span class="o">(</span><span class="s">"Unable to start ServletWebServerApplicationContext due to missing "</span>
<span class="o">+</span> <span class="s">"ServletWebServerFactory bean."</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">if</span> <span class="o">(</span><span class="n">beanNames</span><span class="o">.</span><span class="na">length</span> <span class="o">></span> <span class="mi">1</span><span class="o">)</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">ApplicationContextException</span><span class="o">(</span><span class="s">"Unable to start ServletWebServerApplicationContext due to multiple "</span>
<span class="o">+</span> <span class="s">"ServletWebServerFactory beans : "</span> <span class="o">+</span> <span class="nc">StringUtils</span><span class="o">.</span><span class="na">arrayToCommaDelimitedString</span><span class="o">(</span><span class="n">beanNames</span><span class="o">));</span>
<span class="o">}</span>
<span class="k">return</span> <span class="nf">getBeanFactory</span><span class="o">().</span><span class="na">getBean</span><span class="o">(</span><span class="n">beanNames</span><span class="o">[</span><span class="mi">0</span><span class="o">],</span> <span class="nc">ServletWebServerFactory</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<h1 id="五tomcat的创建">五、tomcat的创建</h1>
<p> 前面千辛万苦的获取到了 tomcatServer的工厂,接下来就看看是怎么创建的吧.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// TomcatServletWebServerFactory</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">WebServer</span> <span class="nf">getWebServer</span><span class="o">(</span><span class="nc">ServletContextInitializer</span><span class="o">...</span> <span class="n">initializers</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// .. 省略</span>
<span class="c1">// 初始化</span>
<span class="n">prepareContext</span><span class="o">(</span><span class="n">tomcat</span><span class="o">.</span><span class="na">getHost</span><span class="o">(),</span> <span class="n">initializers</span><span class="o">);</span>
<span class="k">return</span> <span class="nf">getTomcatWebServer</span><span class="o">(</span><span class="n">tomcat</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 这里<code class="language-plaintext highlighter-rouge">ServletContextInitializer</code>是不是和<code class="language-plaintext highlighter-rouge">ServletContainerInitializer</code>有点神似?别说不仔细看还是会看错,至于这两个是啥关系,这里就不琢磨了,毕竟这个不是重点.</p>
<h2 id="51-初始化">5.1 初始化</h2>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// TomcatServletWebServerFactory</span>
<span class="kd">protected</span> <span class="kt">void</span> <span class="nf">prepareContext</span><span class="o">(</span><span class="nc">Host</span> <span class="n">host</span><span class="o">,</span> <span class="nc">ServletContextInitializer</span><span class="o">[]</span> <span class="n">initializers</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">//... 省略</span>
<span class="c1">// 添加监听器</span>
<span class="n">context</span><span class="o">.</span><span class="na">addLifecycleListener</span><span class="o">(</span><span class="k">new</span> <span class="nc">StaticResourceConfigurer</span><span class="o">(</span><span class="n">context</span><span class="o">));</span>
<span class="nc">ServletContextInitializer</span><span class="o">[]</span> <span class="n">initializersToUse</span> <span class="o">=</span> <span class="n">mergeInitializers</span><span class="o">(</span><span class="n">initializers</span><span class="o">);</span>
<span class="n">host</span><span class="o">.</span><span class="na">addChild</span><span class="o">(</span><span class="n">context</span><span class="o">);</span>
<span class="n">configureContext</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">initializersToUse</span><span class="o">);</span>
<span class="n">postProcessContext</span><span class="o">(</span><span class="n">context</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">private</span> <span class="kd">final</span> <span class="kd">class</span> <span class="nc">StaticResourceConfigurer</span> <span class="kd">implements</span> <span class="nc">LifecycleListener</span> <span class="o">{</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="nc">Context</span> <span class="n">context</span><span class="o">;</span>
<span class="kd">private</span> <span class="nf">StaticResourceConfigurer</span><span class="o">(</span><span class="nc">Context</span> <span class="n">context</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">context</span> <span class="o">=</span> <span class="n">context</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">lifecycleEvent</span><span class="o">(</span><span class="nc">LifecycleEvent</span> <span class="n">event</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">event</span><span class="o">.</span><span class="na">getType</span><span class="o">().</span><span class="na">equals</span><span class="o">(</span><span class="nc">Lifecycle</span><span class="o">.</span><span class="na">CONFIGURE_START_EVENT</span><span class="o">))</span> <span class="o">{</span>
<span class="n">addResourceJars</span><span class="o">(</span><span class="n">getUrlsOfJarsWithMetaInfResources</span><span class="o">());</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="kt">void</span> <span class="nf">addResourceJars</span><span class="o">(</span><span class="nc">List</span><span class="o"><</span><span class="no">URL</span><span class="o">></span> <span class="n">resourceJarUrls</span><span class="o">)</span> <span class="o">{</span>
<span class="k">for</span> <span class="o">(</span><span class="no">URL</span> <span class="n">url</span> <span class="o">:</span> <span class="n">resourceJarUrls</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">String</span> <span class="n">path</span> <span class="o">=</span> <span class="n">url</span><span class="o">.</span><span class="na">getPath</span><span class="o">();</span>
<span class="k">if</span> <span class="o">(</span><span class="n">path</span><span class="o">.</span><span class="na">endsWith</span><span class="o">(</span><span class="s">".jar"</span><span class="o">)</span> <span class="o">||</span> <span class="n">path</span><span class="o">.</span><span class="na">endsWith</span><span class="o">(</span><span class="s">".jar!/"</span><span class="o">))</span> <span class="o">{</span>
<span class="nc">String</span> <span class="n">jar</span> <span class="o">=</span> <span class="n">url</span><span class="o">.</span><span class="na">toString</span><span class="o">();</span>
<span class="k">if</span> <span class="o">(!</span><span class="n">jar</span><span class="o">.</span><span class="na">startsWith</span><span class="o">(</span><span class="s">"jar:"</span><span class="o">))</span> <span class="o">{</span>
<span class="c1">// A jar file in the file system. Convert to Jar URL.</span>
<span class="n">jar</span> <span class="o">=</span> <span class="s">"jar:"</span> <span class="o">+</span> <span class="n">jar</span> <span class="o">+</span> <span class="s">"!/"</span><span class="o">;</span>
<span class="o">}</span>
<span class="n">addResourceSet</span><span class="o">(</span><span class="n">jar</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">else</span> <span class="o">{</span>
<span class="n">addResourceSet</span><span class="o">(</span><span class="n">url</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="kd">private</span> <span class="kt">void</span> <span class="nf">addResourceSet</span><span class="o">(</span><span class="nc">String</span> <span class="n">resource</span><span class="o">)</span> <span class="o">{</span>
<span class="k">try</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">isInsideNestedJar</span><span class="o">(</span><span class="n">resource</span><span class="o">))</span> <span class="o">{</span>
<span class="c1">// It's a nested jar but we now don't want the suffix because Tomcat</span>
<span class="c1">// is going to try and locate it as a root URL (not the resource</span>
<span class="c1">// inside it)</span>
<span class="n">resource</span> <span class="o">=</span> <span class="n">resource</span><span class="o">.</span><span class="na">substring</span><span class="o">(</span><span class="mi">0</span><span class="o">,</span> <span class="n">resource</span><span class="o">.</span><span class="na">length</span><span class="o">()</span> <span class="o">-</span> <span class="mi">2</span><span class="o">);</span>
<span class="o">}</span>
<span class="no">URL</span> <span class="n">url</span> <span class="o">=</span> <span class="k">new</span> <span class="no">URL</span><span class="o">(</span><span class="n">resource</span><span class="o">);</span>
<span class="nc">String</span> <span class="n">path</span> <span class="o">=</span> <span class="s">"/META-INF/resources"</span><span class="o">;</span>
<span class="k">this</span><span class="o">.</span><span class="na">context</span><span class="o">.</span><span class="na">getResources</span><span class="o">().</span><span class="na">createWebResourceSet</span><span class="o">(</span><span class="nc">ResourceSetType</span><span class="o">.</span><span class="na">RESOURCE_JAR</span><span class="o">,</span> <span class="s">"/"</span><span class="o">,</span> <span class="n">url</span><span class="o">,</span> <span class="n">path</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">ex</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// Ignore (probably not a directory)</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="kt">boolean</span> <span class="nf">isInsideNestedJar</span><span class="o">(</span><span class="nc">String</span> <span class="n">dir</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">dir</span><span class="o">.</span><span class="na">indexOf</span><span class="o">(</span><span class="s">"!/"</span><span class="o">)</span> <span class="o"><</span> <span class="n">dir</span><span class="o">.</span><span class="na">lastIndexOf</span><span class="o">(</span><span class="s">"!/"</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 从这个监听器的代码逻辑上看 是往<code class="language-plaintext highlighter-rouge">context.getResources</code>丢东西呀.貌似是路径啥的,这会不会与开头的404问题有关呢?</p>
<p> <code class="language-plaintext highlighter-rouge">createWebResourceSet</code>这个方法嫌疑很大,去看看.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// StandardRoot </span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">createWebResourceSet</span><span class="o">(</span><span class="nc">ResourceSetType</span> <span class="n">type</span><span class="o">,</span> <span class="nc">String</span> <span class="n">webAppMount</span><span class="o">,</span>
<span class="nc">String</span> <span class="n">base</span><span class="o">,</span> <span class="nc">String</span> <span class="n">archivePath</span><span class="o">,</span> <span class="nc">String</span> <span class="n">internalPath</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">List</span><span class="o"><</span><span class="nc">WebResourceSet</span><span class="o">></span> <span class="n">resourceList</span><span class="o">;</span>
<span class="nc">WebResourceSet</span> <span class="n">resourceSet</span><span class="o">;</span>
<span class="k">switch</span> <span class="o">(</span><span class="n">type</span><span class="o">)</span> <span class="o">{</span>
<span class="k">case</span> <span class="nl">PRE:</span>
<span class="n">resourceList</span> <span class="o">=</span> <span class="n">preResources</span><span class="o">;</span>
<span class="k">break</span><span class="o">;</span>
<span class="k">case</span> <span class="nl">CLASSES_JAR:</span>
<span class="n">resourceList</span> <span class="o">=</span> <span class="n">classResources</span><span class="o">;</span>
<span class="k">break</span><span class="o">;</span>
<span class="k">case</span> <span class="nl">RESOURCE_JAR:</span>
<span class="n">resourceList</span> <span class="o">=</span> <span class="n">jarResources</span><span class="o">;</span>
<span class="k">break</span><span class="o">;</span>
<span class="k">case</span> <span class="nl">POST:</span>
<span class="n">resourceList</span> <span class="o">=</span> <span class="n">postResources</span><span class="o">;</span>
<span class="k">break</span><span class="o">;</span>
<span class="k">default</span><span class="o">:</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalArgumentException</span><span class="o">(</span>
<span class="n">sm</span><span class="o">.</span><span class="na">getString</span><span class="o">(</span><span class="s">"standardRoot.createUnknownType"</span><span class="o">,</span> <span class="n">type</span><span class="o">));</span>
<span class="o">}</span>
<span class="c1">// ..... 省略</span>
<span class="n">resourceList</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">resourceSet</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 以上逻辑为根据不同的类型,往不同的list中添加路径.好吧还是看不出来这个到底有什么用,404的问题也没找到,不如果跟踪一下请求看看.</p>
<h1 id="六请求的跟踪">六、请求的跟踪</h1>
<p> springMVC对请求的处理逻辑一般为 DispatcherServlet接管请求->查找handler->查找handlerDapter->视图解析器->解析视图->渲染视图.</p>
<p> 虽然这个逻辑不不完善,可能还是错的,但差不多,笔者认为哈.</p>
<p> 那么问题来了,在springMVC中的视图解析器是啥呢?看看ViewResolver的子类就知道是InternalResourceViewResolver了.但这个没啥用呀,因为具体的渲染逻辑是在视图对象里,解析器在这里没啥太大的用处.所以来看看jsp的视图<code class="language-plaintext highlighter-rouge">JstlView</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">JstlView</span> <span class="kd">extends</span> <span class="nc">InternalResourceView</span> <span class="o">{</span>
<span class="nd">@Nullable</span>
<span class="kd">private</span> <span class="nc">MessageSource</span> <span class="n">messageSource</span><span class="o">;</span>
<span class="cm">/**
* Constructor for use as a bean.
* @see #setUrl
*/</span>
<span class="kd">public</span> <span class="nf">JstlView</span><span class="o">()</span> <span class="o">{</span>
<span class="o">}</span>
<span class="cm">/**
* Create a new JstlView with the given URL.
* @param url the URL to forward to
*/</span>
<span class="kd">public</span> <span class="nf">JstlView</span><span class="o">(</span><span class="nc">String</span> <span class="n">url</span><span class="o">)</span> <span class="o">{</span>
<span class="kd">super</span><span class="o">(</span><span class="n">url</span><span class="o">);</span>
<span class="o">}</span>
<span class="cm">/**
* Create a new JstlView with the given URL.
* @param url the URL to forward to
* @param messageSource the MessageSource to expose to JSTL tags
* (will be wrapped with a JSTL-aware MessageSource that is aware of JSTL's
* {@code javax.servlet.jsp.jstl.fmt.localizationContext} context-param)
* @see JstlUtils#getJstlAwareMessageSource
*/</span>
<span class="kd">public</span> <span class="nf">JstlView</span><span class="o">(</span><span class="nc">String</span> <span class="n">url</span><span class="o">,</span> <span class="nc">MessageSource</span> <span class="n">messageSource</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">(</span><span class="n">url</span><span class="o">);</span>
<span class="k">this</span><span class="o">.</span><span class="na">messageSource</span> <span class="o">=</span> <span class="n">messageSource</span><span class="o">;</span>
<span class="o">}</span>
<span class="cm">/**
* Wraps the MessageSource with a JSTL-aware MessageSource that is aware
* of JSTL's {@code javax.servlet.jsp.jstl.fmt.localizationContext}
* context-param.
* @see JstlUtils#getJstlAwareMessageSource
*/</span>
<span class="nd">@Override</span>
<span class="kd">protected</span> <span class="kt">void</span> <span class="nf">initServletContext</span><span class="o">(</span><span class="nc">ServletContext</span> <span class="n">servletContext</span><span class="o">)</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">messageSource</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">messageSource</span> <span class="o">=</span> <span class="nc">JstlUtils</span><span class="o">.</span><span class="na">getJstlAwareMessageSource</span><span class="o">(</span><span class="n">servletContext</span><span class="o">,</span> <span class="k">this</span><span class="o">.</span><span class="na">messageSource</span><span class="o">);</span>
<span class="o">}</span>
<span class="kd">super</span><span class="o">.</span><span class="na">initServletContext</span><span class="o">(</span><span class="n">servletContext</span><span class="o">);</span>
<span class="o">}</span>
<span class="cm">/**
* Exposes a JSTL LocalizationContext for Spring's locale and MessageSource.
* @see JstlUtils#exposeLocalizationContext
*/</span>
<span class="nd">@Override</span>
<span class="kd">protected</span> <span class="kt">void</span> <span class="nf">exposeHelpers</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</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">messageSource</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">JstlUtils</span><span class="o">.</span><span class="na">exposeLocalizationContext</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="k">this</span><span class="o">.</span><span class="na">messageSource</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">else</span> <span class="o">{</span>
<span class="nc">JstlUtils</span><span class="o">.</span><span class="na">exposeLocalizationContext</span><span class="o">(</span><span class="k">new</span> <span class="nc">RequestContext</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">getServletContext</span><span class="o">()));</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 在jstlView中并没有看到jsp的处理逻辑,去父类看看.</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">InternalResourceView</span> <span class="kd">extends</span> <span class="nc">AbstractUrlBasedView</span> <span class="o">{</span>
<span class="c1">// 省略...</span>
<span class="cm">/**
* Render the internal resource given the specified model.
* This includes setting the model as request attributes.
*/</span>
<span class="nd">@Override</span>
<span class="kd">protected</span> <span class="kt">void</span> <span class="nf">renderMergedOutputModel</span><span class="o">(</span>
<span class="nc">Map</span><span class="o"><</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">></span> <span class="n">model</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="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
<span class="c1">// Expose the model object as request attributes.</span>
<span class="n">exposeModelAsRequestAttributes</span><span class="o">(</span><span class="n">model</span><span class="o">,</span> <span class="n">request</span><span class="o">);</span>
<span class="c1">// Expose helpers as request attributes, if any.</span>
<span class="n">exposeHelpers</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
<span class="c1">// Determine the path for the request dispatcher.</span>
<span class="nc">String</span> <span class="n">dispatcherPath</span> <span class="o">=</span> <span class="n">prepareForRendering</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="c1">// Obtain a RequestDispatcher for the target resource (typically a JSP).</span>
<span class="nc">RequestDispatcher</span> <span class="n">rd</span> <span class="o">=</span> <span class="n">getRequestDispatcher</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">dispatcherPath</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">rd</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">ServletException</span><span class="o">(</span><span class="s">"Could not get RequestDispatcher for ["</span> <span class="o">+</span> <span class="n">getUrl</span><span class="o">()</span> <span class="o">+</span>
<span class="s">"]: Check that the corresponding file exists within your web application archive!"</span><span class="o">);</span>
<span class="o">}</span>
<span class="c1">// If already included or response already committed, perform include, else forward.</span>
<span class="k">if</span> <span class="o">(</span><span class="n">useInclude</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="n">response</span><span class="o">.</span><span class="na">setContentType</span><span class="o">(</span><span class="n">getContentType</span><span class="o">());</span>
<span class="k">if</span> <span class="o">(</span><span class="n">logger</span><span class="o">.</span><span class="na">isDebugEnabled</span><span class="o">())</span> <span class="o">{</span>
<span class="n">logger</span><span class="o">.</span><span class="na">debug</span><span class="o">(</span><span class="s">"Including ["</span> <span class="o">+</span> <span class="n">getUrl</span><span class="o">()</span> <span class="o">+</span> <span class="s">"]"</span><span class="o">);</span>
<span class="o">}</span>
<span class="n">rd</span><span class="o">.</span><span class="na">include</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="k">else</span> <span class="o">{</span>
<span class="c1">// Note: The forwarded resource is supposed to determine the content type itself.</span>
<span class="k">if</span> <span class="o">(</span><span class="n">logger</span><span class="o">.</span><span class="na">isDebugEnabled</span><span class="o">())</span> <span class="o">{</span>
<span class="n">logger</span><span class="o">.</span><span class="na">debug</span><span class="o">(</span><span class="s">"Forwarding to ["</span> <span class="o">+</span> <span class="n">getUrl</span><span class="o">()</span> <span class="o">+</span> <span class="s">"]"</span><span class="o">);</span>
<span class="o">}</span>
<span class="n">rd</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="c1">// 省略....</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 核心处理逻辑在<code class="language-plaintext highlighter-rouge">renderMergedOutputModel</code>,经过<code class="language-plaintext highlighter-rouge">renderMergedOutputModel</code>方法一顿骚操作以后,最后发现请求被<code class="language-plaintext highlighter-rouge">RequestDispatcher</code>接管了.emmm,线索又断了,那么最后是被谁接管的呢?在j2ee的世界里,能被啥接管?不就是个servlet或者filter嘛.</p>
<p> 那么接下来要搞清楚是谁接管了请求,并渲染了jsp,虽然答案很显然了,但还是要走一下流程.</p>
<h2 id="61-猜测是servlet接管了请求">6.1 猜测是servlet接管了请求</h2>
<p> springMVC 里 servlet?那不就是DispatcherServlet嘛?但总觉得不可能,不可能请求从DispatcherServlet来又回去吧?那就看看在创建tomcat的时候有没有注册其他的servelt.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// TomcatServletWebServerFactory</span>
<span class="kd">protected</span> <span class="kt">void</span> <span class="nf">prepareContext</span><span class="o">(</span><span class="nc">Host</span> <span class="n">host</span><span class="o">,</span> <span class="nc">ServletContextInitializer</span><span class="o">[]</span> <span class="n">initializers</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">isRegisterDefaultServlet</span><span class="o">())</span> <span class="o">{</span>
<span class="n">addDefaultServlet</span><span class="o">(</span><span class="n">context</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">if</span> <span class="o">(</span><span class="n">shouldRegisterJspServlet</span><span class="o">())</span> <span class="o">{</span>
<span class="n">addJspServlet</span><span class="o">(</span><span class="n">context</span><span class="o">);</span>
<span class="n">addJasperInitializer</span><span class="o">(</span><span class="n">context</span><span class="o">);</span>
<span class="o">}</span>
<span class="c1">// 省略 ...</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 可以看到在初始化的时候,注册了两个servlet,一个默认的servlet一个是jsp的servlet</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// TomcatServletWebServerFactory</span>
<span class="kd">private</span> <span class="kt">void</span> <span class="nf">addDefaultServlet</span><span class="o">(</span><span class="nc">Context</span> <span class="n">context</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">Wrapper</span> <span class="n">defaultServlet</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="na">createWrapper</span><span class="o">();</span>
<span class="c1">// 名称</span>
<span class="n">defaultServlet</span><span class="o">.</span><span class="na">setName</span><span class="o">(</span><span class="s">"default"</span><span class="o">);</span>
<span class="n">defaultServlet</span><span class="o">.</span><span class="na">setServletClass</span><span class="o">(</span><span class="s">"org.apache.catalina.servlets.DefaultServlet"</span><span class="o">);</span>
<span class="n">defaultServlet</span><span class="o">.</span><span class="na">addInitParameter</span><span class="o">(</span><span class="s">"debug"</span><span class="o">,</span> <span class="s">"0"</span><span class="o">);</span>
<span class="n">defaultServlet</span><span class="o">.</span><span class="na">addInitParameter</span><span class="o">(</span><span class="s">"listings"</span><span class="o">,</span> <span class="s">"false"</span><span class="o">);</span>
<span class="n">defaultServlet</span><span class="o">.</span><span class="na">setLoadOnStartup</span><span class="o">(</span><span class="mi">1</span><span class="o">);</span>
<span class="c1">// Otherwise the default location of a Spring DispatcherServlet cannot be set</span>
<span class="c1">// 是否运行覆盖,这是为了dispatcherServlet做准备</span>
<span class="c1">// 方便在后免把这个默认的servlet给覆盖掉</span>
<span class="n">defaultServlet</span><span class="o">.</span><span class="na">setOverridable</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
<span class="n">context</span><span class="o">.</span><span class="na">addChild</span><span class="o">(</span><span class="n">defaultServlet</span><span class="o">);</span>
<span class="n">context</span><span class="o">.</span><span class="na">addServletMappingDecoded</span><span class="o">(</span><span class="s">"/"</span><span class="o">,</span> <span class="s">"default"</span><span class="o">);</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="kt">void</span> <span class="nf">addJspServlet</span><span class="o">(</span><span class="nc">Context</span> <span class="n">context</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">Wrapper</span> <span class="n">jspServlet</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="na">createWrapper</span><span class="o">();</span>
<span class="n">jspServlet</span><span class="o">.</span><span class="na">setName</span><span class="o">(</span><span class="s">"jsp"</span><span class="o">);</span>
<span class="n">jspServlet</span><span class="o">.</span><span class="na">setServletClass</span><span class="o">(</span><span class="n">getJsp</span><span class="o">().</span><span class="na">getClassName</span><span class="o">());</span>
<span class="n">jspServlet</span><span class="o">.</span><span class="na">addInitParameter</span><span class="o">(</span><span class="s">"fork"</span><span class="o">,</span> <span class="s">"false"</span><span class="o">);</span>
<span class="n">getJsp</span><span class="o">().</span><span class="na">getInitParameters</span><span class="o">().</span><span class="na">forEach</span><span class="o">(</span><span class="nl">jspServlet:</span><span class="o">:</span><span class="n">addInitParameter</span><span class="o">);</span>
<span class="n">jspServlet</span><span class="o">.</span><span class="na">setLoadOnStartup</span><span class="o">(</span><span class="mi">3</span><span class="o">);</span>
<span class="n">context</span><span class="o">.</span><span class="na">addChild</span><span class="o">(</span><span class="n">jspServlet</span><span class="o">);</span>
<span class="c1">// 拦截 *.jsp 后缀的请求</span>
<span class="n">context</span><span class="o">.</span><span class="na">addServletMappingDecoded</span><span class="o">(</span><span class="s">"*.jsp"</span><span class="o">,</span> <span class="s">"jsp"</span><span class="o">);</span>
<span class="n">context</span><span class="o">.</span><span class="na">addServletMappingDecoded</span><span class="o">(</span><span class="s">"*.jspx"</span><span class="o">,</span> <span class="s">"jsp"</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>两个servlet,一个默认的,一个jsp的.看到这儿会不会有点奇怪,为啥不注册dispatcherServlet?emmm不是很明白昂,但是这个核心的servlet是不会落下的.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@AutoConfigureOrder</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="nd">@Configuration</span><span class="o">(</span><span class="n">proxyBeanMethods</span> <span class="o">=</span> <span class="kc">false</span><span class="o">)</span>
<span class="cm">/**
* 匹配容器为 servlet
* **/</span>
<span class="nd">@ConditionalOnWebApplication</span><span class="o">(</span><span class="n">type</span> <span class="o">=</span> <span class="nc">Type</span><span class="o">.</span><span class="na">SERVLET</span><span class="o">)</span>
<span class="cm">/**
* 判断 class path 路径下有 DispatcherServlet.class
* **/</span>
<span class="nd">@ConditionalOnClass</span><span class="o">(</span><span class="nc">DispatcherServlet</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="cm">/***
* 在 ServletWebServerFactoryAutoConfiguration 之后生效
* */</span>
<span class="nd">@AutoConfigureAfter</span><span class="o">(</span><span class="nc">ServletWebServerFactoryAutoConfiguration</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">DispatcherServletAutoConfiguration</span> <span class="o">{</span>
<span class="cm">/*
* The bean name for a DispatcherServlet that will be mapped to the root URL "/"
*/</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">DEFAULT_DISPATCHER_SERVLET_BEAN_NAME</span> <span class="o">=</span> <span class="s">"dispatcherServlet"</span><span class="o">;</span>
<span class="cm">/*
* The bean name for a ServletRegistrationBean for the DispatcherServlet "/"
*/</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME</span> <span class="o">=</span> <span class="s">"dispatcherServletRegistration"</span><span class="o">;</span>
<span class="nd">@Configuration</span><span class="o">(</span><span class="n">proxyBeanMethods</span> <span class="o">=</span> <span class="kc">false</span><span class="o">)</span>
<span class="nd">@Conditional</span><span class="o">(</span><span class="nc">DefaultDispatcherServletCondition</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="nd">@ConditionalOnClass</span><span class="o">(</span><span class="nc">ServletRegistration</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="c1">// 启用配置 WebMvcProperties</span>
<span class="nd">@EnableConfigurationProperties</span><span class="o">(</span><span class="nc">WebMvcProperties</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="kd">protected</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">DispatcherServletConfiguration</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="no">DEFAULT_DISPATCHER_SERVLET_BEAN_NAME</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">DispatcherServlet</span> <span class="nf">dispatcherServlet</span><span class="o">(</span><span class="nc">WebMvcProperties</span> <span class="n">webMvcProperties</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">DispatcherServlet</span> <span class="n">dispatcherServlet</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DispatcherServlet</span><span class="o">();</span>
<span class="n">dispatcherServlet</span><span class="o">.</span><span class="na">setDispatchOptionsRequest</span><span class="o">(</span><span class="n">webMvcProperties</span><span class="o">.</span><span class="na">isDispatchOptionsRequest</span><span class="o">());</span>
<span class="n">dispatcherServlet</span><span class="o">.</span><span class="na">setDispatchTraceRequest</span><span class="o">(</span><span class="n">webMvcProperties</span><span class="o">.</span><span class="na">isDispatchTraceRequest</span><span class="o">());</span>
<span class="n">dispatcherServlet</span><span class="o">.</span><span class="na">setThrowExceptionIfNoHandlerFound</span><span class="o">(</span><span class="n">webMvcProperties</span><span class="o">.</span><span class="na">isThrowExceptionIfNoHandlerFound</span><span class="o">());</span>
<span class="n">dispatcherServlet</span><span class="o">.</span><span class="na">setPublishEvents</span><span class="o">(</span><span class="n">webMvcProperties</span><span class="o">.</span><span class="na">isPublishRequestHandledEvents</span><span class="o">());</span>
<span class="n">dispatcherServlet</span><span class="o">.</span><span class="na">setEnableLoggingRequestDetails</span><span class="o">(</span><span class="n">webMvcProperties</span><span class="o">.</span><span class="na">isLogRequestDetails</span><span class="o">());</span>
<span class="k">return</span> <span class="n">dispatcherServlet</span><span class="o">;</span>
<span class="o">}</span>
<span class="nd">@Bean</span>
<span class="nd">@ConditionalOnBean</span><span class="o">(</span><span class="nc">MultipartResolver</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="nd">@ConditionalOnMissingBean</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="nc">DispatcherServlet</span><span class="o">.</span><span class="na">MULTIPART_RESOLVER_BEAN_NAME</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">MultipartResolver</span> <span class="nf">multipartResolver</span><span class="o">(</span><span class="nc">MultipartResolver</span> <span class="n">resolver</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// Detect if the user has created a MultipartResolver but named it incorrectly</span>
<span class="k">return</span> <span class="n">resolver</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="nd">@Configuration</span><span class="o">(</span><span class="n">proxyBeanMethods</span> <span class="o">=</span> <span class="kc">false</span><span class="o">)</span>
<span class="nd">@Conditional</span><span class="o">(</span><span class="nc">DispatcherServletRegistrationCondition</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="nd">@ConditionalOnClass</span><span class="o">(</span><span class="nc">ServletRegistration</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="nd">@EnableConfigurationProperties</span><span class="o">(</span><span class="nc">WebMvcProperties</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="nd">@Import</span><span class="o">(</span><span class="nc">DispatcherServletConfiguration</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="kd">protected</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">DispatcherServletRegistrationConfiguration</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="no">DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME</span><span class="o">)</span>
<span class="nd">@ConditionalOnBean</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="nc">DispatcherServlet</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="n">name</span> <span class="o">=</span> <span class="no">DEFAULT_DISPATCHER_SERVLET_BEAN_NAME</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">DispatcherServletRegistrationBean</span> <span class="nf">dispatcherServletRegistration</span><span class="o">(</span><span class="nc">DispatcherServlet</span> <span class="n">dispatcherServlet</span><span class="o">,</span>
<span class="nc">WebMvcProperties</span> <span class="n">webMvcProperties</span><span class="o">,</span> <span class="nc">ObjectProvider</span><span class="o"><</span><span class="nc">MultipartConfigElement</span><span class="o">></span> <span class="n">multipartConfig</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">DispatcherServletRegistrationBean</span> <span class="n">registration</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DispatcherServletRegistrationBean</span><span class="o">(</span><span class="n">dispatcherServlet</span><span class="o">,</span>
<span class="n">webMvcProperties</span><span class="o">.</span><span class="na">getServlet</span><span class="o">().</span><span class="na">getPath</span><span class="o">());</span>
<span class="c1">// 名称</span>
<span class="n">registration</span><span class="o">.</span><span class="na">setName</span><span class="o">(</span><span class="no">DEFAULT_DISPATCHER_SERVLET_BEAN_NAME</span><span class="o">);</span>
<span class="c1">// 启动顺序</span>
<span class="n">registration</span><span class="o">.</span><span class="na">setLoadOnStartup</span><span class="o">(</span><span class="n">webMvcProperties</span><span class="o">.</span><span class="na">getServlet</span><span class="o">().</span><span class="na">getLoadOnStartup</span><span class="o">());</span>
<span class="n">multipartConfig</span><span class="o">.</span><span class="na">ifAvailable</span><span class="o">(</span><span class="nl">registration:</span><span class="o">:</span><span class="n">setMultipartConfig</span><span class="o">);</span>
<span class="k">return</span> <span class="n">registration</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="c1">// 省略....</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 在自动配置的这个类里面生成了一个<code class="language-plaintext highlighter-rouge">DispatcherServletRegistrationBean</code>对象,这个对象就是用于组测dispatcherServlet的.<code class="language-plaintext highlighter-rouge">DispatcherServletRegistrationBean</code>是<code class="language-plaintext highlighter-rouge">ServletContextInitializer</code>的子类,用于初始化,注册等操作等.</p>
<p> 那么这个对象是在什么时候注册的呢?又要回到tomcat创建的时候了.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// TomcatServletWebServerFactory</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">WebServer</span> <span class="nf">getWebServer</span><span class="o">(</span><span class="nc">ServletContextInitializer</span><span class="o">...</span> <span class="n">initializers</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// 省略..</span>
<span class="n">prepareContext</span><span class="o">(</span><span class="n">tomcat</span><span class="o">.</span><span class="na">getHost</span><span class="o">(),</span> <span class="n">initializers</span><span class="o">);</span>
<span class="k">return</span> <span class="nf">getTomcatWebServer</span><span class="o">(</span><span class="n">tomcat</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 在调用初始化上下文的时候把这个玩意儿给传进去了.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// TomcatServletWebServerFactory</span>
<span class="kd">protected</span> <span class="kt">void</span> <span class="nf">prepareContext</span><span class="o">(</span><span class="nc">Host</span> <span class="n">host</span><span class="o">,</span> <span class="nc">ServletContextInitializer</span><span class="o">[]</span> <span class="n">initializers</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// 省略...</span>
<span class="nc">ServletContextInitializer</span><span class="o">[]</span> <span class="n">initializersToUse</span> <span class="o">=</span> <span class="n">mergeInitializers</span><span class="o">(</span><span class="n">initializers</span><span class="o">);</span>
<span class="n">host</span><span class="o">.</span><span class="na">addChild</span><span class="o">(</span><span class="n">context</span><span class="o">);</span>
<span class="n">configureContext</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">initializersToUse</span><span class="o">);</span>
<span class="n">postProcessContext</span><span class="o">(</span><span class="n">context</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 这里合并了所有的<code class="language-plaintext highlighter-rouge">ServletContextInitializer</code>对象,传入到了<code class="language-plaintext highlighter-rouge">configureContext</code>方法中.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// TomcatServletWebServerFactory</span>
<span class="kd">protected</span> <span class="kt">void</span> <span class="nf">configureContext</span><span class="o">(</span><span class="nc">Context</span> <span class="n">context</span><span class="o">,</span> <span class="nc">ServletContextInitializer</span><span class="o">[]</span> <span class="n">initializers</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">TomcatStarter</span> <span class="n">starter</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">TomcatStarter</span><span class="o">(</span><span class="n">initializers</span><span class="o">);</span>
<span class="n">context</span><span class="o">.</span><span class="na">addServletContainerInitializer</span><span class="o">(</span><span class="n">starter</span><span class="o">,</span> <span class="no">NO_CLASSES</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 这里把<code class="language-plaintext highlighter-rouge">ServletContextInitializer</code>转成了一个<code class="language-plaintext highlighter-rouge">TomcatStarter</code>对象,并把这个对象添加到了上下文中去.这个</p>
<p><code class="language-plaintext highlighter-rouge">TomcatStarter</code>就厉害了.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">TomcatStarter</span> <span class="kd">implements</span> <span class="nc">ServletContainerInitializer</span> <span class="o">{</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">Log</span> <span class="n">logger</span> <span class="o">=</span> <span class="nc">LogFactory</span><span class="o">.</span><span class="na">getLog</span><span class="o">(</span><span class="nc">TomcatStarter</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="nc">ServletContextInitializer</span><span class="o">[]</span> <span class="n">initializers</span><span class="o">;</span>
<span class="kd">private</span> <span class="kd">volatile</span> <span class="nc">Exception</span> <span class="n">startUpException</span><span class="o">;</span>
<span class="nc">TomcatStarter</span><span class="o">(</span><span class="nc">ServletContextInitializer</span><span class="o">[]</span> <span class="n">initializers</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">initializers</span> <span class="o">=</span> <span class="n">initializers</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">onStartup</span><span class="o">(</span><span class="nc">Set</span><span class="o"><</span><span class="nc">Class</span><span class="o"><?>></span> <span class="n">classes</span><span class="o">,</span> <span class="nc">ServletContext</span> <span class="n">servletContext</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">ServletException</span> <span class="o">{</span>
<span class="k">try</span> <span class="o">{</span>
<span class="c1">// 初始化所有需要 初始化的类</span>
<span class="k">for</span> <span class="o">(</span><span class="nc">ServletContextInitializer</span> <span class="n">initializer</span> <span class="o">:</span> <span class="k">this</span><span class="o">.</span><span class="na">initializers</span><span class="o">)</span> <span class="o">{</span>
<span class="n">initializer</span><span class="o">.</span><span class="na">onStartup</span><span class="o">(</span><span class="n">servletContext</span><span class="o">);</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">ex</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">startUpException</span> <span class="o">=</span> <span class="n">ex</span><span class="o">;</span>
<span class="c1">// Prevent Tomcat from logging and re-throwing when we know we can</span>
<span class="c1">// deal with it in the main thread, but log for information here.</span>
<span class="k">if</span> <span class="o">(</span><span class="n">logger</span><span class="o">.</span><span class="na">isErrorEnabled</span><span class="o">())</span> <span class="o">{</span>
<span class="n">logger</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="s">"Error starting Tomcat context. Exception: "</span> <span class="o">+</span> <span class="n">ex</span><span class="o">.</span><span class="na">getClass</span><span class="o">().</span><span class="na">getName</span><span class="o">()</span> <span class="o">+</span> <span class="s">". Message: "</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>
<span class="o">}</span>
<span class="nc">Exception</span> <span class="nf">getStartUpException</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">startUpException</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p> <code class="language-plaintext highlighter-rouge">TomcatStarter</code>实现了<code class="language-plaintext highlighter-rouge">ServletContainerInitializer</code>接口,这个接口就厉害了,在servlet的生命中期中,会调用实现这个接口的<code class="language-plaintext highlighter-rouge">onStartup</code>方法,至于什么是servlet的生命周期,就不引出了,估计一时半会也说不完.</p>
<p> 嗯,知道这个东西的厉害,说了这么多,也没说<code class="language-plaintext highlighter-rouge">ServletContextInitializer</code>这个东西哪来的.不是方法传进来的嘛?不行就回去看看.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">WebServer</span> <span class="nf">getWebServer</span><span class="o">(</span><span class="nc">ServletContextInitializer</span><span class="o">...</span> <span class="n">initializers</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// 省略...</span>
<span class="k">return</span> <span class="nf">getTomcatWebServer</span><span class="o">(</span><span class="n">tomcat</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 嗯,是传进来的,怎么传进来的?当然是创建的时候传的了0.0</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ServletWebServerApplicationContext</span>
<span class="kd">private</span> <span class="kt">void</span> <span class="nf">createWebServer</span><span class="o">()</span> <span class="o">{</span>
<span class="nc">WebServer</span> <span class="n">webServer</span> <span class="o">=</span> <span class="k">this</span><span class="o">.</span><span class="na">webServer</span><span class="o">;</span>
<span class="nc">ServletContext</span> <span class="n">servletContext</span> <span class="o">=</span> <span class="n">getServletContext</span><span class="o">();</span>
<span class="k">if</span> <span class="o">(</span><span class="n">webServer</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">&&</span> <span class="n">servletContext</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">ServletWebServerFactory</span> <span class="n">factory</span> <span class="o">=</span> <span class="n">getWebServerFactory</span><span class="o">();</span>
<span class="k">this</span><span class="o">.</span><span class="na">webServer</span> <span class="o">=</span> <span class="n">factory</span><span class="o">.</span><span class="na">getWebServer</span><span class="o">(</span><span class="n">getSelfInitializer</span><span class="o">());</span>
<span class="o">}</span>
<span class="k">else</span> <span class="nf">if</span> <span class="o">(</span><span class="n">servletContext</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="k">try</span> <span class="o">{</span>
<span class="n">getSelfInitializer</span><span class="o">().</span><span class="na">onStartup</span><span class="o">(</span><span class="n">servletContext</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">catch</span> <span class="o">(</span><span class="nc">ServletException</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">ApplicationContextException</span><span class="o">(</span><span class="s">"Cannot initialize servlet context"</span><span class="o">,</span> <span class="n">ex</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="n">initPropertySources</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>
<h2 id="62-servletcontextinitializer-怎么来的">6.2 ServletContextInitializer 怎么来的</h2>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ServletWebServerApplicationContext</span>
<span class="kd">private</span> <span class="n">org</span><span class="o">.</span><span class="na">springframework</span><span class="o">.</span><span class="na">boot</span><span class="o">.</span><span class="na">web</span><span class="o">.</span><span class="na">servlet</span><span class="o">.</span><span class="na">ServletContextInitializer</span> <span class="nf">getSelfInitializer</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="n">selfInitialize</span><span class="o">;</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="kt">void</span> <span class="nf">selfInitialize</span><span class="o">(</span><span class="nc">ServletContext</span> <span class="n">servletContext</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">ServletException</span> <span class="o">{</span>
<span class="n">prepareWebApplicationContext</span><span class="o">(</span><span class="n">servletContext</span><span class="o">);</span>
<span class="n">registerApplicationScope</span><span class="o">(</span><span class="n">servletContext</span><span class="o">);</span>
<span class="nc">WebApplicationContextUtils</span><span class="o">.</span><span class="na">registerEnvironmentBeans</span><span class="o">(</span><span class="n">getBeanFactory</span><span class="o">(),</span> <span class="n">servletContext</span><span class="o">);</span>
<span class="k">for</span> <span class="o">(</span><span class="nc">ServletContextInitializer</span> <span class="n">beans</span> <span class="o">:</span> <span class="n">getServletContextInitializerBeans</span><span class="o">())</span> <span class="o">{</span>
<span class="n">beans</span><span class="o">.</span><span class="na">onStartup</span><span class="o">(</span><span class="n">servletContext</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 这就是了,那配置类<code class="language-plaintext highlighter-rouge">DispatcherServletAutoConfiguration</code>里面的<code class="language-plaintext highlighter-rouge">DispatcherServletRegistrationBean</code>哪去了?这里的确没有,不过在<code class="language-plaintext highlighter-rouge">getServletContextInitializerBeans()</code>方法返回的集合里面.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ServletWebServerApplicationContext</span>
<span class="kd">protected</span> <span class="nc">Collection</span><span class="o"><</span><span class="nc">ServletContextInitializer</span><span class="o">></span> <span class="nf">getServletContextInitializerBeans</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">ServletContextInitializerBeans</span><span class="o">(</span><span class="n">getBeanFactory</span><span class="o">());</span>
<span class="o">}</span>
</code></pre></div></div>
<p> <code class="language-plaintext highlighter-rouge">ServletContextInitializerBeans</code>是一个继承了<code class="language-plaintext highlighter-rouge">AbstractCollection</code>的集合对象.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ServletContextInitializerBeans</span>
<span class="kd">public</span> <span class="nf">ServletContextInitializerBeans</span><span class="o">(</span><span class="nc">ListableBeanFactory</span> <span class="n">beanFactory</span><span class="o">,</span>
<span class="nc">Class</span><span class="o"><?</span> <span class="kd">extends</span> <span class="nc">ServletContextInitializer</span><span class="o">>...</span> <span class="n">initializerTypes</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">initializers</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">LinkedMultiValueMap</span><span class="o"><>();</span>
<span class="k">this</span><span class="o">.</span><span class="na">initializerTypes</span> <span class="o">=</span> <span class="o">(</span><span class="n">initializerTypes</span><span class="o">.</span><span class="na">length</span> <span class="o">!=</span> <span class="mi">0</span><span class="o">)</span> <span class="o">?</span> <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="n">initializerTypes</span><span class="o">)</span>
<span class="o">:</span> <span class="nc">Collections</span><span class="o">.</span><span class="na">singletonList</span><span class="o">(</span><span class="nc">ServletContextInitializer</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
<span class="n">addServletContextInitializerBeans</span><span class="o">(</span><span class="n">beanFactory</span><span class="o">);</span>
<span class="n">addAdaptableBeans</span><span class="o">(</span><span class="n">beanFactory</span><span class="o">);</span>
<span class="nc">List</span><span class="o"><</span><span class="nc">ServletContextInitializer</span><span class="o">></span> <span class="n">sortedInitializers</span> <span class="o">=</span> <span class="k">this</span><span class="o">.</span><span class="na">initializers</span><span class="o">.</span><span class="na">values</span><span class="o">().</span><span class="na">stream</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">)</span> <span class="o">-></span> <span class="n">value</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">sorted</span><span class="o">(</span><span class="nc">AnnotationAwareOrderComparator</span><span class="o">.</span><span class="na">INSTANCE</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>
<span class="k">this</span><span class="o">.</span><span class="na">sortedList</span> <span class="o">=</span> <span class="nc">Collections</span><span class="o">.</span><span class="na">unmodifiableList</span><span class="o">(</span><span class="n">sortedInitializers</span><span class="o">);</span>
<span class="n">logMappings</span><span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">initializers</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 核心逻辑就在构造方法中的<code class="language-plaintext highlighter-rouge">addServletContextInitializerBeans</code>方法中.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ServletContextInitializerBeans</span>
<span class="kd">private</span> <span class="kt">void</span> <span class="nf">addServletContextInitializerBeans</span><span class="o">(</span><span class="nc">ListableBeanFactory</span> <span class="n">beanFactory</span><span class="o">)</span> <span class="o">{</span>
<span class="k">for</span> <span class="o">(</span><span class="nc">Class</span><span class="o"><?</span> <span class="kd">extends</span> <span class="nc">ServletContextInitializer</span><span class="o">></span> <span class="n">initializerType</span> <span class="o">:</span> <span class="k">this</span><span class="o">.</span><span class="na">initializerTypes</span><span class="o">)</span> <span class="o">{</span>
<span class="k">for</span> <span class="o">(</span><span class="nc">Entry</span><span class="o"><</span><span class="nc">String</span><span class="o">,</span> <span class="o">?</span> <span class="kd">extends</span> <span class="nc">ServletContextInitializer</span><span class="o">></span> <span class="n">initializerBean</span> <span class="o">:</span> <span class="n">getOrderedBeansOfType</span><span class="o">(</span><span class="n">beanFactory</span><span class="o">,</span>
<span class="n">initializerType</span><span class="o">))</span> <span class="o">{</span>
<span class="n">addServletContextInitializerBean</span><span class="o">(</span><span class="n">initializerBean</span><span class="o">.</span><span class="na">getKey</span><span class="o">(),</span> <span class="n">initializerBean</span><span class="o">.</span><span class="na">getValue</span><span class="o">(),</span> <span class="n">beanFactory</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="o"><</span><span class="no">T</span><span class="o">></span> <span class="nc">List</span><span class="o"><</span><span class="nc">Entry</span><span class="o"><</span><span class="nc">String</span><span class="o">,</span> <span class="no">T</span><span class="o">>></span> <span class="nf">getOrderedBeansOfType</span><span class="o">(</span><span class="nc">ListableBeanFactory</span> <span class="n">beanFactory</span><span class="o">,</span> <span class="nc">Class</span><span class="o"><</span><span class="no">T</span><span class="o">></span> <span class="n">type</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="nf">getOrderedBeansOfType</span><span class="o">(</span><span class="n">beanFactory</span><span class="o">,</span> <span class="n">type</span><span class="o">,</span> <span class="nc">Collections</span><span class="o">.</span><span class="na">emptySet</span><span class="o">());</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="o"><</span><span class="no">T</span><span class="o">></span> <span class="nc">List</span><span class="o"><</span><span class="nc">Entry</span><span class="o"><</span><span class="nc">String</span><span class="o">,</span> <span class="no">T</span><span class="o">>></span> <span class="nf">getOrderedBeansOfType</span><span class="o">(</span><span class="nc">ListableBeanFactory</span> <span class="n">beanFactory</span><span class="o">,</span> <span class="nc">Class</span><span class="o"><</span><span class="no">T</span><span class="o">></span> <span class="n">type</span><span class="o">,</span>
<span class="nc">Set</span><span class="o"><?></span> <span class="n">excludes</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">String</span><span class="o">[]</span> <span class="n">names</span> <span class="o">=</span> <span class="n">beanFactory</span><span class="o">.</span><span class="na">getBeanNamesForType</span><span class="o">(</span><span class="n">type</span><span class="o">,</span> <span class="kc">true</span><span class="o">,</span> <span class="kc">false</span><span class="o">);</span>
<span class="nc">Map</span><span class="o"><</span><span class="nc">String</span><span class="o">,</span> <span class="no">T</span><span class="o">></span> <span class="n">map</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">LinkedHashMap</span><span class="o"><>();</span>
<span class="k">for</span> <span class="o">(</span><span class="nc">String</span> <span class="n">name</span> <span class="o">:</span> <span class="n">names</span><span class="o">)</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(!</span><span class="n">excludes</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="n">name</span><span class="o">)</span> <span class="o">&&</span> <span class="o">!</span><span class="nc">ScopedProxyUtils</span><span class="o">.</span><span class="na">isScopedTarget</span><span class="o">(</span><span class="n">name</span><span class="o">))</span> <span class="o">{</span>
<span class="no">T</span> <span class="n">bean</span> <span class="o">=</span> <span class="n">beanFactory</span><span class="o">.</span><span class="na">getBean</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="n">type</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(!</span><span class="n">excludes</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="n">bean</span><span class="o">))</span> <span class="o">{</span>
<span class="n">map</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="n">bean</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="nc">List</span><span class="o"><</span><span class="nc">Entry</span><span class="o"><</span><span class="nc">String</span><span class="o">,</span> <span class="no">T</span><span class="o">>></span> <span class="n">beans</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o"><>(</span><span class="n">map</span><span class="o">.</span><span class="na">entrySet</span><span class="o">());</span>
<span class="n">beans</span><span class="o">.</span><span class="na">sort</span><span class="o">((</span><span class="n">o1</span><span class="o">,</span> <span class="n">o2</span><span class="o">)</span> <span class="o">-></span> <span class="nc">AnnotationAwareOrderComparator</span><span class="o">.</span><span class="na">INSTANCE</span><span class="o">.</span><span class="na">compare</span><span class="o">(</span><span class="n">o1</span><span class="o">.</span><span class="na">getValue</span><span class="o">(),</span> <span class="n">o2</span><span class="o">.</span><span class="na">getValue</span><span class="o">()));</span>
<span class="k">return</span> <span class="n">beans</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 从ioc中获取所有<code class="language-plaintext highlighter-rouge">ServletContextInitialize</code>的子类,而这个bean就刚好是之前配置类中的bean,<code class="language-plaintext highlighter-rouge">DispatcherServletRegistrationBean</code>.</p>
<p> 至于注册的逻辑就不阐述了,很简单,顺着<code class="language-plaintext highlighter-rouge">DispatcherServletRegistrationBean</code>的父类<code class="language-plaintext highlighter-rouge">RegistrationBean</code>看下去就好.</p>
<h1 id="七整理线索">七、整理线索</h1>
<p> 前面说了那么多跟404完全没有关系呀,其实前面是在做铺垫而已,看官别着急.由前文的逻辑可以知道在tomcat里面至少注册了2个servetl,一个是spring的(后面覆盖的),一个是jsp的.那么完全由理由猜测,最后由JstlView转发的请求到了jspServlet里面.来看看jspSerlvet里面干了啥?</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c1">// JspServlet</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">service</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="kd">throws</span> <span class="nc">ServletException</span><span class="o">,</span> <span class="nc">IOException</span> <span class="o">{</span>
<span class="c1">// 省略....</span>
<span class="k">try</span> <span class="o">{</span>
<span class="n">serviceJspFile</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">jspUri</span><span class="o">,</span> <span class="n">precompile</span><span class="o">);</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">RuntimeException</span> <span class="n">e</span><span class="o">)</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="k">catch</span> <span class="o">(</span><span class="nc">ServletException</span> <span class="n">e</span><span class="o">)</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="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="n">e</span><span class="o">;</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Throwable</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">ExceptionUtils</span><span class="o">.</span><span class="na">handleThrowable</span><span class="o">(</span><span class="n">e</span><span class="o">);</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">ServletException</span><span class="o">(</span><span class="n">e</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 别问我为啥只贴了service这个方法.先看看第一个方法<code class="language-plaintext highlighter-rouge">serviceJspFile</code>.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// JspServlet </span>
<span class="kd">private</span> <span class="kt">void</span> <span class="nf">serviceJspFile</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">String</span> <span class="n">jspUri</span><span class="o">,</span>
<span class="kt">boolean</span> <span class="n">precompile</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">JspServletWrapper</span> <span class="n">wrapper</span> <span class="o">=</span> <span class="n">rctxt</span><span class="o">.</span><span class="na">getWrapper</span><span class="o">(</span><span class="n">jspUri</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">wrapper</span> <span class="o">==</span> <span class="kc">null</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">wrapper</span> <span class="o">=</span> <span class="n">rctxt</span><span class="o">.</span><span class="na">getWrapper</span><span class="o">(</span><span class="n">jspUri</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">wrapper</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// Check if the requested JSP page exists, to avoid</span>
<span class="c1">// creating unnecessary directories and files.</span>
<span class="c1">// 判断 jsp 文件是否存在</span>
<span class="k">if</span> <span class="o">(</span><span class="kc">null</span> <span class="o">==</span> <span class="n">context</span><span class="o">.</span><span class="na">getResource</span><span class="o">(</span><span class="n">jspUri</span><span class="o">))</span> <span class="o">{</span>
<span class="n">handleMissingResource</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">jspUri</span><span class="o">);</span>
<span class="k">return</span><span class="o">;</span>
<span class="o">}</span>
<span class="n">wrapper</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">JspServletWrapper</span><span class="o">(</span><span class="n">config</span><span class="o">,</span> <span class="n">options</span><span class="o">,</span> <span class="n">jspUri</span><span class="o">,</span>
<span class="n">rctxt</span><span class="o">);</span>
<span class="n">rctxt</span><span class="o">.</span><span class="na">addWrapper</span><span class="o">(</span><span class="n">jspUri</span><span class="o">,</span><span class="n">wrapper</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="k">try</span> <span class="o">{</span>
<span class="n">wrapper</span><span class="o">.</span><span class="na">service</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">precompile</span><span class="o">);</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">FileNotFoundException</span> <span class="n">fnfe</span><span class="o">)</span> <span class="o">{</span>
<span class="n">handleMissingResource</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">jspUri</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 诺,核心代码被抓了.这个SerlvetContext,既然在tomcat里面那么它的实现类肯定是<code class="language-plaintext highlighter-rouge">ApplicationContext</code>,不用怀疑,不信你跟跟流程看看.</p>
<p> 既然是<code class="language-plaintext highlighter-rouge">ApplicationContext</code>中获取资源,那来看看是怎么拿的文件吧.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ApplicationContext</span>
<span class="kd">public</span> <span class="no">URL</span> <span class="nf">getResource</span><span class="o">(</span><span class="nc">String</span> <span class="n">path</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">MalformedURLException</span> <span class="o">{</span>
<span class="c1">// 省略... </span>
<span class="nc">WebResourceRoot</span> <span class="n">resources</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="na">getResources</span><span class="o">();</span>
<span class="k">if</span> <span class="o">(</span><span class="n">resources</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// 获取资源</span>
<span class="k">return</span> <span class="n">resources</span><span class="o">.</span><span class="na">getResource</span><span class="o">(</span><span class="n">validatedPath</span><span class="o">).</span><span class="na">getURL</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>
</code></pre></div></div>
<p> 转战到 <code class="language-plaintext highlighter-rouge">WebResourceRoot</code>里去,<code class="language-plaintext highlighter-rouge">WebResourceRoot</code>是个接口,在本案例中的唯一子类是<code class="language-plaintext highlighter-rouge">StandardRoot</code>.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// StandardRoot</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">WebResource</span> <span class="nf">getResource</span><span class="o">(</span><span class="nc">String</span> <span class="n">path</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="nf">getResource</span><span class="o">(</span><span class="n">path</span><span class="o">,</span> <span class="kc">true</span><span class="o">,</span> <span class="kc">false</span><span class="o">);</span>
<span class="o">}</span>
<span class="kd">protected</span> <span class="nc">WebResource</span> <span class="nf">getResource</span><span class="o">(</span><span class="nc">String</span> <span class="n">path</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">validate</span><span class="o">,</span>
<span class="kt">boolean</span> <span class="n">useClassLoaderResources</span><span class="o">)</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">validate</span><span class="o">)</span> <span class="o">{</span>
<span class="n">path</span> <span class="o">=</span> <span class="n">validate</span><span class="o">(</span><span class="n">path</span><span class="o">);</span>
<span class="o">}</span>
<span class="c1">// 判断是否允许缓存,这个默认值是true</span>
<span class="k">if</span> <span class="o">(</span><span class="n">isCachingAllowed</span><span class="o">())</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">cache</span><span class="o">.</span><span class="na">getResource</span><span class="o">(</span><span class="n">path</span><span class="o">,</span> <span class="n">useClassLoaderResources</span><span class="o">);</span>
<span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
<span class="k">return</span> <span class="nf">getResourceInternal</span><span class="o">(</span><span class="n">path</span><span class="o">,</span> <span class="n">useClassLoaderResources</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 这里分两种情况,一个是从换成里面获取,另外则从非缓存中获取,缓存里面的世界很精彩的.</p>
<h2 id="71-从缓存里面获取">7.1 从缓存里面获取</h2>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Cache</span>
<span class="kd">protected</span> <span class="nc">WebResource</span> <span class="nf">getResource</span><span class="o">(</span><span class="nc">String</span> <span class="n">path</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">useClassLoaderResources</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// 省略....</span>
<span class="c1">// 判断是否获取到缓存</span>
<span class="k">if</span> <span class="o">(</span><span class="n">cacheEntry</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// Local copy to ensure consistency</span>
<span class="kt">int</span> <span class="n">objectMaxSizeBytes</span> <span class="o">=</span> <span class="n">getObjectMaxSizeBytes</span><span class="o">();</span>
<span class="c1">// 创建缓存对象</span>
<span class="nc">CachedResource</span> <span class="n">newCacheEntry</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">CachedResource</span><span class="o">(</span><span class="k">this</span><span class="o">,</span> <span class="n">root</span><span class="o">,</span> <span class="n">path</span><span class="o">,</span> <span class="n">getTtl</span><span class="o">(),</span>
<span class="n">objectMaxSizeBytes</span><span class="o">,</span> <span class="n">useClassLoaderResources</span><span class="o">);</span>
<span class="c1">// Concurrent callers will end up with the same CachedResource</span>
<span class="c1">// instance</span>
<span class="c1">// 放入缓存中</span>
<span class="n">cacheEntry</span> <span class="o">=</span> <span class="n">resourceCache</span><span class="o">.</span><span class="na">putIfAbsent</span><span class="o">(</span><span class="n">path</span><span class="o">,</span> <span class="n">newCacheEntry</span><span class="o">);</span>
<span class="c1">// 二次判断</span>
<span class="k">if</span> <span class="o">(</span><span class="n">cacheEntry</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// newCacheEntry was inserted into the cache - validate it</span>
<span class="n">cacheEntry</span> <span class="o">=</span> <span class="n">newCacheEntry</span><span class="o">;</span>
<span class="c1">// 验证资源的合法性(这里就石锤了)</span>
<span class="n">cacheEntry</span><span class="o">.</span><span class="na">validateResource</span><span class="o">(</span><span class="n">useClassLoaderResources</span><span class="o">);</span>
<span class="c1">// 省略....</span>
<span class="k">return</span> <span class="n">cacheEntry</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 还差2步</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// CachedResource</span>
<span class="kd">protected</span> <span class="kt">boolean</span> <span class="nf">validateResource</span><span class="o">(</span><span class="kt">boolean</span> <span class="n">useClassLoaderResources</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// 省略..</span>
<span class="c1">// 非空检查</span>
<span class="k">if</span> <span class="o">(</span><span class="n">webResource</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// 加锁</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="c1">// 双重检查</span>
<span class="k">if</span> <span class="o">(</span><span class="n">webResource</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="n">webResource</span> <span class="o">=</span> <span class="n">root</span><span class="o">.</span><span class="na">getResourceInternal</span><span class="o">(</span>
<span class="n">webAppPath</span><span class="o">,</span> <span class="n">useClassLoaderResources</span><span class="o">);</span>
<span class="n">getLastModified</span><span class="o">();</span>
<span class="n">getContentLength</span><span class="o">();</span>
<span class="n">nextCheck</span> <span class="o">=</span> <span class="n">ttl</span> <span class="o">+</span> <span class="n">now</span><span class="o">;</span>
<span class="c1">// exists() is a relatively expensive check for a file so</span>
<span class="c1">// use the fact that we know if it exists at this point</span>
<span class="k">if</span> <span class="o">(</span><span class="n">webResource</span> <span class="k">instanceof</span> <span class="nc">EmptyResource</span><span class="o">)</span> <span class="o">{</span>
<span class="n">cachedExists</span> <span class="o">=</span> <span class="nc">Boolean</span><span class="o">.</span><span class="na">FALSE</span><span class="o">;</span>
<span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
<span class="n">cachedExists</span> <span class="o">=</span> <span class="nc">Boolean</span><span class="o">.</span><span class="na">TRUE</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="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 程序初次允许的时候,webResource肯定是为空的,别说缓存了.这里的root是<code class="language-plaintext highlighter-rouge">StandardRoot</code>,又调用回去了.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// StandardRoot</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="nc">List</span><span class="o"><</span><span class="nc">List</span><span class="o"><</span><span class="nc">WebResourceSet</span><span class="o">>></span> <span class="n">allResources</span> <span class="o">=</span>
<span class="k">new</span> <span class="nc">ArrayList</span><span class="o"><>();</span>
<span class="o">{</span>
<span class="n">allResources</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">preResources</span><span class="o">);</span>
<span class="n">allResources</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">mainResources</span><span class="o">);</span>
<span class="n">allResources</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">classResources</span><span class="o">);</span>
<span class="n">allResources</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">jarResources</span><span class="o">);</span>
<span class="n">allResources</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">postResources</span><span class="o">);</span>
<span class="o">}</span>
<span class="kd">protected</span> <span class="kd">final</span> <span class="nc">WebResource</span> <span class="nf">getResourceInternal</span><span class="o">(</span><span class="nc">String</span> <span class="n">path</span><span class="o">,</span>
<span class="kt">boolean</span> <span class="n">useClassLoaderResources</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">WebResource</span> <span class="n">result</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
<span class="nc">WebResource</span> <span class="n">virtual</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
<span class="nc">WebResource</span> <span class="n">mainEmpty</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
<span class="k">for</span> <span class="o">(</span><span class="nc">List</span><span class="o"><</span><span class="nc">WebResourceSet</span><span class="o">></span> <span class="n">list</span> <span class="o">:</span> <span class="n">allResources</span><span class="o">)</span> <span class="o">{</span>
<span class="k">for</span> <span class="o">(</span><span class="nc">WebResourceSet</span> <span class="n">webResourceSet</span> <span class="o">:</span> <span class="n">list</span><span class="o">)</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(!</span><span class="n">useClassLoaderResources</span> <span class="o">&&</span> <span class="o">!</span><span class="n">webResourceSet</span><span class="o">.</span><span class="na">getClassLoaderOnly</span><span class="o">()</span> <span class="o">||</span>
<span class="n">useClassLoaderResources</span> <span class="o">&&</span> <span class="o">!</span><span class="n">webResourceSet</span><span class="o">.</span><span class="na">getStaticOnly</span><span class="o">())</span> <span class="o">{</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">webResourceSet</span><span class="o">.</span><span class="na">getResource</span><span class="o">(</span><span class="n">path</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">result</span><span class="o">.</span><span class="na">exists</span><span class="o">())</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">result</span><span class="o">;</span>
<span class="o">}</span>
<span class="k">if</span> <span class="o">(</span><span class="n">virtual</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">result</span><span class="o">.</span><span class="na">isVirtual</span><span class="o">())</span> <span class="o">{</span>
<span class="n">virtual</span> <span class="o">=</span> <span class="n">result</span><span class="o">;</span>
<span class="o">}</span> <span class="k">else</span> <span class="k">if</span> <span class="o">(</span><span class="n">main</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">webResourceSet</span><span class="o">))</span> <span class="o">{</span>
<span class="n">mainEmpty</span> <span class="o">=</span> <span class="n">result</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="c1">// 省略...</span>
<span class="c1">// Default is empty resource in main resources</span>
<span class="k">return</span> <span class="n">mainEmpty</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 这里遍历不同资源类别,来判断文件是否存在,如果存在就返回,然后放入缓存中.至于从非缓存中获取的逻辑就上面<code class="language-plaintext highlighter-rouge">getResourceInternal</code>的逻辑,就不罗嗦了.</p>
<p> 嗯,这个有啥用呢?还是未解决404的问题呀,别急快了.</p>
<h1 id="八被遗忘的静态资源监听器staticresourceconfigurer">八、被遗忘的静态资源监听器(StaticResourceConfigurer)</h1>
<p> 少侠是否记得在创建tomcat的时候在servletContext中添加了个这个监听器呢?</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// TomcatServletWebServerFactory</span>
<span class="kd">protected</span> <span class="kt">void</span> <span class="nf">prepareContext</span><span class="o">(</span><span class="nc">Host</span> <span class="n">host</span><span class="o">,</span> <span class="nc">ServletContextInitializer</span><span class="o">[]</span> <span class="n">initializers</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// 省略...</span>
<span class="n">context</span><span class="o">.</span><span class="na">addLifecycleListener</span><span class="o">(</span><span class="k">new</span> <span class="nc">StaticResourceConfigurer</span><span class="o">(</span><span class="n">context</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="c1">// TomcatServletWebServerFactory$StaticResourceConfigurer</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="kd">class</span> <span class="nc">StaticResourceConfigurer</span> <span class="kd">implements</span> <span class="nc">LifecycleListener</span> <span class="o">{</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="nc">Context</span> <span class="n">context</span><span class="o">;</span>
<span class="kd">private</span> <span class="nf">StaticResourceConfigurer</span><span class="o">(</span><span class="nc">Context</span> <span class="n">context</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">context</span> <span class="o">=</span> <span class="n">context</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">lifecycleEvent</span><span class="o">(</span><span class="nc">LifecycleEvent</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">getType</span><span class="o">().</span><span class="na">equals</span><span class="o">(</span><span class="nc">Lifecycle</span><span class="o">.</span><span class="na">CONFIGURE_START_EVENT</span><span class="o">))</span> <span class="o">{</span>
<span class="n">addResourceJars</span><span class="o">(</span><span class="n">getUrlsOfJarsWithMetaInfResources</span><span class="o">());</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="kt">void</span> <span class="nf">addResourceJars</span><span class="o">(</span><span class="nc">List</span><span class="o"><</span><span class="no">URL</span><span class="o">></span> <span class="n">resourceJarUrls</span><span class="o">)</span> <span class="o">{</span>
<span class="k">for</span> <span class="o">(</span><span class="no">URL</span> <span class="n">url</span> <span class="o">:</span> <span class="n">resourceJarUrls</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">String</span> <span class="n">path</span> <span class="o">=</span> <span class="n">url</span><span class="o">.</span><span class="na">getPath</span><span class="o">();</span>
<span class="k">if</span> <span class="o">(</span><span class="n">path</span><span class="o">.</span><span class="na">endsWith</span><span class="o">(</span><span class="s">".jar"</span><span class="o">)</span> <span class="o">||</span> <span class="n">path</span><span class="o">.</span><span class="na">endsWith</span><span class="o">(</span><span class="s">".jar!/"</span><span class="o">))</span> <span class="o">{</span>
<span class="nc">String</span> <span class="n">jar</span> <span class="o">=</span> <span class="n">url</span><span class="o">.</span><span class="na">toString</span><span class="o">();</span>
<span class="k">if</span> <span class="o">(!</span><span class="n">jar</span><span class="o">.</span><span class="na">startsWith</span><span class="o">(</span><span class="s">"jar:"</span><span class="o">))</span> <span class="o">{</span>
<span class="c1">// A jar file in the file system. Convert to Jar URL.</span>
<span class="n">jar</span> <span class="o">=</span> <span class="s">"jar:"</span> <span class="o">+</span> <span class="n">jar</span> <span class="o">+</span> <span class="s">"!/"</span><span class="o">;</span>
<span class="o">}</span>
<span class="n">addResourceSet</span><span class="o">(</span><span class="n">jar</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">else</span> <span class="o">{</span>
<span class="n">addResourceSet</span><span class="o">(</span><span class="n">url</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="kd">private</span> <span class="kt">void</span> <span class="nf">addResourceSet</span><span class="o">(</span><span class="nc">String</span> <span class="n">resource</span><span class="o">)</span> <span class="o">{</span>
<span class="k">try</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">isInsideNestedJar</span><span class="o">(</span><span class="n">resource</span><span class="o">))</span> <span class="o">{</span>
<span class="c1">// It's a nested jar but we now don't want the suffix because Tomcat</span>
<span class="c1">// is going to try and locate it as a root URL (not the resource</span>
<span class="c1">// inside it)</span>
<span class="n">resource</span> <span class="o">=</span> <span class="n">resource</span><span class="o">.</span><span class="na">substring</span><span class="o">(</span><span class="mi">0</span><span class="o">,</span> <span class="n">resource</span><span class="o">.</span><span class="na">length</span><span class="o">()</span> <span class="o">-</span> <span class="mi">2</span><span class="o">);</span>
<span class="o">}</span>
<span class="no">URL</span> <span class="n">url</span> <span class="o">=</span> <span class="k">new</span> <span class="no">URL</span><span class="o">(</span><span class="n">resource</span><span class="o">);</span>
<span class="nc">String</span> <span class="n">path</span> <span class="o">=</span> <span class="s">"/META-INF/resources"</span><span class="o">;</span>
<span class="k">this</span><span class="o">.</span><span class="na">context</span><span class="o">.</span><span class="na">getResources</span><span class="o">().</span><span class="na">createWebResourceSet</span><span class="o">(</span><span class="nc">ResourceSetType</span><span class="o">.</span><span class="na">RESOURCE_JAR</span><span class="o">,</span> <span class="s">"/"</span><span class="o">,</span> <span class="n">url</span><span class="o">,</span> <span class="n">path</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">ex</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// Ignore (probably not a directory)</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="kt">boolean</span> <span class="nf">isInsideNestedJar</span><span class="o">(</span><span class="nc">String</span> <span class="n">dir</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">dir</span><span class="o">.</span><span class="na">indexOf</span><span class="o">(</span><span class="s">"!/"</span><span class="o">)</span> <span class="o"><</span> <span class="n">dir</span><span class="o">.</span><span class="na">lastIndexOf</span><span class="o">(</span><span class="s">"!/"</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 无论逻辑怎么变化,最后监听器的代码都会走到<code class="language-plaintext highlighter-rouge">this.context.getResources().createWebResourceSet(ResourceSetType.RESOURCE_JAR, "/", url, path);</code>这句话来.</p>
<p> 看到<code class="language-plaintext highlighter-rouge">ResourceSetType.RESOURCE_JAR</code>这个常量是否有点感觉呢?没感觉就脱掉衣服再看看.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// StandardRoot </span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">createWebResourceSet</span><span class="o">(</span><span class="nc">ResourceSetType</span> <span class="n">type</span><span class="o">,</span> <span class="nc">String</span> <span class="n">webAppMount</span><span class="o">,</span>
<span class="no">URL</span> <span class="n">url</span><span class="o">,</span> <span class="nc">String</span> <span class="n">internalPath</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">BaseLocation</span> <span class="n">baseLocation</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">BaseLocation</span><span class="o">(</span><span class="n">url</span><span class="o">);</span>
<span class="n">createWebResourceSet</span><span class="o">(</span><span class="n">type</span><span class="o">,</span> <span class="n">webAppMount</span><span class="o">,</span> <span class="n">baseLocation</span><span class="o">.</span><span class="na">getBasePath</span><span class="o">(),</span>
<span class="n">baseLocation</span><span class="o">.</span><span class="na">getArchivePath</span><span class="o">(),</span> <span class="n">internalPath</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">createWebResourceSet</span><span class="o">(</span><span class="nc">ResourceSetType</span> <span class="n">type</span><span class="o">,</span> <span class="nc">String</span> <span class="n">webAppMount</span><span class="o">,</span>
<span class="nc">String</span> <span class="n">base</span><span class="o">,</span> <span class="nc">String</span> <span class="n">archivePath</span><span class="o">,</span> <span class="nc">String</span> <span class="n">internalPath</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">List</span><span class="o"><</span><span class="nc">WebResourceSet</span><span class="o">></span> <span class="n">resourceList</span><span class="o">;</span>
<span class="nc">WebResourceSet</span> <span class="n">resourceSet</span><span class="o">;</span>
<span class="k">switch</span> <span class="o">(</span><span class="n">type</span><span class="o">)</span> <span class="o">{</span>
<span class="k">case</span> <span class="nl">PRE:</span>
<span class="n">resourceList</span> <span class="o">=</span> <span class="n">preResources</span><span class="o">;</span>
<span class="k">break</span><span class="o">;</span>
<span class="k">case</span> <span class="nl">CLASSES_JAR:</span>
<span class="n">resourceList</span> <span class="o">=</span> <span class="n">classResources</span><span class="o">;</span>
<span class="k">break</span><span class="o">;</span>
<span class="k">case</span> <span class="nl">RESOURCE_JAR:</span>
<span class="n">resourceList</span> <span class="o">=</span> <span class="n">jarResources</span><span class="o">;</span>
<span class="k">break</span><span class="o">;</span>
<span class="k">case</span> <span class="nl">POST:</span>
<span class="n">resourceList</span> <span class="o">=</span> <span class="n">postResources</span><span class="o">;</span>
<span class="k">break</span><span class="o">;</span>
<span class="k">default</span><span class="o">:</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalArgumentException</span><span class="o">(</span>
<span class="n">sm</span><span class="o">.</span><span class="na">getString</span><span class="o">(</span><span class="s">"standardRoot.createUnknownType"</span><span class="o">,</span> <span class="n">type</span><span class="o">));</span>
<span class="o">}</span>
<span class="c1">// 省略</span>
<span class="n">resourceList</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">resourceSet</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 是不是这一切都园回去了?是不是感觉疑惑都没了?如果还是不解在去看看资源解析的那块儿.</p>
<h1 id="九真相">九、真相</h1>
<p> 为甚么是404呢?那是因为在StandardRoot里面的resourceList中不存在指定的根路径,所以是404.又要有小伙伴要杠了,说MATE-INFO目录下面的都没问题.</p>
<p> 是,没问题.因为springBoot把这个路径加进去了.不信你看.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// StaticResourceConfigurer</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="kd">class</span> <span class="nc">StaticResourceConfigurer</span> <span class="kd">implements</span> <span class="nc">LifecycleListener</span> <span class="o">{</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="nc">Context</span> <span class="n">context</span><span class="o">;</span>
<span class="kd">private</span> <span class="nf">StaticResourceConfigurer</span><span class="o">(</span><span class="nc">Context</span> <span class="n">context</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">context</span> <span class="o">=</span> <span class="n">context</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">lifecycleEvent</span><span class="o">(</span><span class="nc">LifecycleEvent</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">getType</span><span class="o">().</span><span class="na">equals</span><span class="o">(</span><span class="nc">Lifecycle</span><span class="o">.</span><span class="na">CONFIGURE_START_EVENT</span><span class="o">))</span> <span class="o">{</span>
<span class="n">addResourceJars</span><span class="o">(</span><span class="n">getUrlsOfJarsWithMetaInfResources</span><span class="o">());</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="kt">void</span> <span class="nf">addResourceJars</span><span class="o">(</span><span class="nc">List</span><span class="o"><</span><span class="no">URL</span><span class="o">></span> <span class="n">resourceJarUrls</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// 省略</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="kt">void</span> <span class="nf">addResourceSet</span><span class="o">(</span><span class="nc">String</span> <span class="n">resource</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// 省略</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="kt">boolean</span> <span class="nf">isInsideNestedJar</span><span class="o">(</span><span class="nc">String</span> <span class="n">dir</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">dir</span><span class="o">.</span><span class="na">indexOf</span><span class="o">(</span><span class="s">"!/"</span><span class="o">)</span> <span class="o"><</span> <span class="n">dir</span><span class="o">.</span><span class="na">lastIndexOf</span><span class="o">(</span><span class="s">"!/"</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p> 在<code class="language-plaintext highlighter-rouge">getUrlsOfJarsWithMetaInfResources</code>方法里面就有这个路径.逻辑简单就不贴出来了.</p>
<h2 id="91-如何解决404">9.1 如何解决404</h2>
<p> 根据以上的分析,可以和spring一样弄个监听器,在resourceList添加指定的路径.最简单暴力的方法如下:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Controller</span>
<span class="nd">@EnableAutoConfiguration</span>
<span class="nd">@Configuration</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Main</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">Main</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">"/test-static"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">String</span> <span class="nf">testStatic</span><span class="o">(){</span>
<span class="k">return</span> <span class="s">"index-static"</span><span class="o">;</span>
<span class="o">}</span>
<span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">"/test-public"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">String</span> <span class="nf">testPublic</span><span class="o">(){</span>
<span class="k">return</span> <span class="s">"index-public"</span><span class="o">;</span>
<span class="o">}</span>
<span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">"/test-resources"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">String</span> <span class="nf">testResources</span><span class="o">(){</span>
<span class="k">return</span> <span class="s">"index-resources"</span><span class="o">;</span>
<span class="o">}</span>
<span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">"/test-meta"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">String</span> <span class="nf">testMeta</span><span class="o">(){</span>
<span class="k">return</span> <span class="s">"index-meta"</span><span class="o">;</span>
<span class="o">}</span>
<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">TomcatServletWebServerFactory</span> <span class="nf">getTomcatServletWebServerFactory</span><span class="o">(){</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">AdvTomcatServletWebServerFactory</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">AdvTomcatServletWebServerFactory</span> <span class="kd">extends</span> <span class="nc">TomcatServletWebServerFactory</span> <span class="o">{</span>
<span class="kd">private</span> <span class="nc">Context</span> <span class="n">context</span><span class="o">;</span>
<span class="kd">public</span> <span class="nf">AdvTomcatServletWebServerFactory</span><span class="o">(){</span>
<span class="n">getContextLifecycleListeners</span><span class="o">().</span><span class="na">add</span><span class="o">(</span><span class="k">new</span> <span class="nc">AdvResourceListener</span><span class="o">());</span>
<span class="o">}</span>
<span class="c1">// 这个方法是 spring留下的模板方法</span>
<span class="c1">// 可以通过这个方法进行扩展</span>
<span class="nd">@Override</span>
<span class="kd">protected</span> <span class="kt">void</span> <span class="nf">postProcessContext</span><span class="o">(</span><span class="nc">Context</span> <span class="n">context</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">context</span> <span class="o">=</span> <span class="n">context</span><span class="o">;</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="kd">class</span> <span class="nc">AdvResourceListener</span> <span class="kd">implements</span> <span class="nc">LifecycleListener</span><span class="o">{</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">lifecycleEvent</span><span class="o">(</span><span class="nc">LifecycleEvent</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">getType</span><span class="o">().</span><span class="na">equals</span><span class="o">(</span><span class="nc">Lifecycle</span><span class="o">.</span><span class="na">CONFIGURE_START_EVENT</span><span class="o">))</span> <span class="o">{</span>
<span class="kd">final</span> <span class="no">URL</span> <span class="n">resource</span> <span class="o">=</span> <span class="nc">AdvTomcatServletWebServerFactory</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getClassLoader</span><span class="o">().</span><span class="na">getResource</span><span class="o">(</span><span class="s">"."</span><span class="o">);</span>
<span class="kd">final</span> <span class="nc">WebResourceRoot</span> <span class="n">resources</span> <span class="o">=</span> <span class="nc">AdvTomcatServletWebServerFactory</span><span class="o">.</span><span class="na">this</span><span class="o">.</span><span class="na">context</span><span class="o">.</span><span class="na">getResources</span><span class="o">();</span>
<span class="n">resources</span><span class="o">.</span><span class="na">createWebResourceSet</span><span class="o">(</span><span class="nc">WebResourceRoot</span><span class="o">.</span><span class="na">ResourceSetType</span><span class="o">.</span><span class="na">RESOURCE_JAR</span><span class="o">,</span> <span class="s">"/"</span><span class="o">,</span> <span class="n">resource</span><span class="o">,</span> <span class="s">"/static"</span><span class="o">);</span>
<span class="n">resources</span><span class="o">.</span><span class="na">createWebResourceSet</span><span class="o">(</span><span class="nc">WebResourceRoot</span><span class="o">.</span><span class="na">ResourceSetType</span><span class="o">.</span><span class="na">RESOURCE_JAR</span><span class="o">,</span> <span class="s">"/"</span><span class="o">,</span> <span class="n">resource</span><span class="o">,</span> <span class="s">"/public"</span><span class="o">);</span>
<span class="n">resources</span><span class="o">.</span><span class="na">createWebResourceSet</span><span class="o">(</span><span class="nc">WebResourceRoot</span><span class="o">.</span><span class="na">ResourceSetType</span><span class="o">.</span><span class="na">RESOURCE_JAR</span><span class="o">,</span> <span class="s">"/"</span><span class="o">,</span> <span class="n">resource</span><span class="o">,</span> <span class="s">"/resources"</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> 通过自己的工厂替换掉spring的工厂,实现注册自己的监听器,当然方法还有很多,如果你熟悉tomcat的启动流程的话.</p>
<h1 id="十小结">十、小结</h1>
<p> 在springBoot的懒人套餐下,出现问题往往会让人防不胜防呀,比如这个404,根本没有任何信息说哪里有问题(除非是开了debug日志).</p>
<p> 本来就想谢谢解决方案的,结果写了这么多,吃饭了.</p>{"nick"=>"onew", "link"=>"https://onew.me"}一、前言 最近有小朋友在学习spring boot的时候遇到了一个问题,按照教程上操作始终是404.于是就百事不得其解.问我的时候,我也一脸蒙B,毕竟jsp这玩意儿好久都没碰到过了,之前碰jsp的时候还是在ssh的时候. 既然遇到问题就来分析一下呗,趁着最近在看spring的源码. 二、案发现场 ymal: server: port: 9090 spring: mvc: view: prefix: /WEB-INF/jsp/ suffix: .jsp pom: <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> 代码: @Controller @EnableAutoConfiguration public class App { public static void main(String[] args) { SpringApplication.run(App.class,args); } @RequestMapping("/test") public String test(){ return "index"; } } 按照以上代码,应该是没有什么问题的,毕竟人家的教程也是这么做的,只不过别人是用eclipse,他是用的idea罢了.我也懒得去分析为啥eclipse没得问题了,直接来看看为啥会有这个问题. 三、分析 众所周知,spring boot只是在spring上面包了一层皮,里面还是利用了spring的一些机制来完成,当然加载自动化配置,开箱即用,感觉很智能. springboot为我们开发者省去了很多配置上的麻烦,大部分都默认配置好了,但是虽然便利了开发者,但也带来了一些麻烦,就如这个问题,整个日子输出窗口都没有日志显示为啥会是404,文件明明在那,为啥会找不到呢? 要解决这个问题,就要从springBoot的自动配置上入手.按照上面的配置,用的是嵌入式的tomcat,那么就从tomcat的配置开始. 3.1 ServletWebServerFactoryConfiguration 当使用springboot的wen功能的时候,有个关键的配置就避免不了了,代码如下: @Configuration(proxyBeanMethods = false) class ServletWebServerFactoryConfiguration { // 判断classpath是否存在 Servlet,Tomcat,UpgradeProtocol类 // 如果存在就启用此配置 // 当然还要 ServletWebServerFactory 期子类没有 在容器中 @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class }) @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) static class EmbeddedTomcat { @Bean TomcatServletWebServerFactory tomcatServletWebServerFactory( ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers, ObjectProvider<TomcatContextCustomizer> contextCustomizers, ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) { // 创建tomcat 工厂 TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); factory.getTomcatConnectorCustomizers() .addAll(connectorCustomizers.orderedStream().collect(Collectors.toList())); factory.getTomcatContextCustomizers() .addAll(contextCustomizers.orderedStream().collect(Collectors.toList())); factory.getTomcatProtocolHandlerCustomizers() .addAll(protocolHandlerCustomizers.orderedStream().collect(Collectors.toList())); return factory; } } /** * Nested configuration if Jetty is being used. * 同tomcat的逻辑 */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ Servlet.class, Server.class, Loader.class, WebAppContext.class }) @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) static class EmbeddedJetty { @Bean JettyServletWebServerFactory JettyServletWebServerFactory( ObjectProvider<JettyServerCustomizer> serverCustomizers) { JettyServletWebServerFactory factory = new JettyServletWebServerFactory(); factory.getServerCustomizers().addAll(serverCustomizers.orderedStream().collect(Collectors.toList())); return factory; } } /** * Nested configuration if Undertow is being used. * 同tomcat的逻辑 */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class }) @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) static class EmbeddedUndertow { @Bean UndertowServletWebServerFactory undertowServletWebServerFactory( ObjectProvider<UndertowDeploymentInfoCustomizer> deploymentInfoCustomizers, ObjectProvider<UndertowBuilderCustomizer> builderCustomizers) { UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory(); factory.getDeploymentInfoCustomizers() .addAll(deploymentInfoCustomizers.orderedStream().collect(Collectors.toList())); factory.getBuilderCustomizers().addAll(builderCustomizers.orderedStream().collect(Collectors.toList())); return factory; } } } 这里可以看到,配置类按照classpath中的类创建了不同的ServletWebServerFactory,本文这里加入了tomcat,所以这里将会创建TomcatServletWebServerFactory. 当然光看这个还是不行的,要明白为啥会这么创建,那么这一切要从springboot的启动流程开始分析才能解释整个情况. 四、SpringBoot启动流程分析 太阳底下无新鲜事,来揭开名为方便的面纱.当然这只是初步的探讨. @EnableAutoConfiguration public class Main { public static void main(String[] args) { SpringApplication.run(Main.class,args); } } 熟悉springBoot的人看到这样的写法是否是感到平淡无奇?那么这短短的一行代码后面到底发生了啥?? 4.1 SpringApplication.run public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) { return run(new Class<?>[] { primarySource }, args); } public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) { return new SpringApplication(primarySources).run(args); } 创建了个SpringApplication对象在run?看看构造函数是否有啥逻辑. public SpringApplication(Class<?>... primarySources) { this(null, primarySources); } public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) { this.resourceLoader = resourceLoader; Assert.notNull(primarySources, "PrimarySources must not be null"); this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); // 判断 web 应用的类型 // 判断依据为 是否存在指定 DispatcherServlet,DispatcherHandler,ServletContainer,WebApplicationContext,ReactiveWebApplicationContext等类 // 使用 class.forName 进行查找 // 判断spring程序的类型 this.webApplicationType = WebApplicationType.deduceFromClasspath(); // 加载 META-INF/spring.factories 配置文件,并把 ApplicationContextInitializer 相关的类全部实例化 setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); // 加载 META-INF/spring.factories 配置文件,并把 ApplicationListener 相关的类 全部实例化 setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); // 检查 main 方法所在的类 this.mainApplicationClass = deduceMainApplicationClass(); } 在构造的时候还是做了不少的逻辑,这里就不得不提一下spring的黑魔法了,那就是SpringFactoriesLoader,这个东西有点像java中的spi机制,与之不同是spring是读取的是META-INF/spring.factories文件.至于为啥不用spi要自己单搞个,emmmmmm. 构造的逻辑很简单,不是很复杂,就是检测一下要启动什么类型的spring,具体操作是在WebApplicationType.deduceFromClasspath();,这个类型判断还是很重要的,后面创建spring上下文的时候会用得上. 4.2 springApplication.run 对象创建好了,又要继续run了. public ConfigurableApplicationContext run(String... args) { // 创建一个用于记录 启动-关闭 时间的 StopWatch StopWatch stopWatch = new StopWatch(); stopWatch.start(); ConfigurableApplicationContext context = null; Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>(); // 设置环境变量 configureHeadlessProperty(); // 创建 EventPublishingRunListener // 相当于是个组合模式,所有listener 都集中在 SpringApplicationRunListeners 中 SpringApplicationRunListeners listeners = getRunListeners(args); // 启动容器,发送时间 listeners.starting(); try { // 把 args 封装为对象,映射到环境中 ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); // 初始化环境 ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); // 设置环境变量 spring.beaninfo.ignore configureIgnoreBeanInfo(environment); // 获取 需要打印的 Banner 并把 banner 打印到控制台 Banner printedBanner = printBanner(environment); // 根据不同的类型 创建不同的上下文 context = createApplicationContext(); // 获取 所有 SpringBootExceptionReporter 相关的类 exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context); // 准备上下文 prepareContext(context, environment, listeners, applicationArguments, printedBanner); // 刷新上下文,发送事件 refreshContext(context); // 模板方法 afterRefresh(context, applicationArguments); // 停止 stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch); } // 通知监听器,已经启动 listeners.started(context); // 调用 runner的 run 方法 callRunners(context, applicationArguments); } catch (Throwable ex) { // 处理运行时的错误 handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); } try { // 通知监听器,正在运行 listeners.running(context); } catch (Throwable ex) { // 处理运行时的错误 handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } // 返回上下文 return context; } 由于本文不是分析代码的文章,所以关注点放在创建spring上下文和上下文的操作上.分别是createApplicationContext和refreshContext 4.3 createApplicationContext protected ConfigurableApplicationContext createApplicationContext() { Class<?> contextClass = this.applicationContextClass; if (contextClass == null) { try { switch (this.webApplicationType) { case SERVLET: contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS); break; case REACTIVE: contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS); break; default: contextClass = Class.forName(DEFAULT_CONTEXT_CLASS); } } catch (ClassNotFoundException ex) { throw new IllegalStateException( "Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex); } } return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass); } 这里的类型,是按照之前构造方法中的类型进行创建的,如果不清楚的可以往上翻一哈. 如果类型为: SERVLET 就创建 AnnotationConfigServletWebServerApplicationContext 如果类型为: REACTIVE 就创建 AnnotationConfigReactiveWebServerApplicationContext 默认创建 : AnnotationConfigApplicationContext 很显然这里的类型是 SERVLET 所以创建了 AnnotationConfigServletWebServerApplicationContext,继承关系如下. 熟悉spring的同学是不是感觉与ClassPathXmlApplicationContext差不多?我觉得是差不多的,只是干事的方式有点区别. 这里把对象创建完了,然后进行一顿骚操作,设置值,环境等等.不再这里进行分析.要看的关键点是refreshContext. 4.5 refreshContext 刷新上下文,这里刷新会有什么骚操作呢?来瞧瞧就知道了. private void refreshContext(ConfigurableApplicationContext context) { refresh((ApplicationContext) context); if (this.registerShutdownHook) { try { // 添加关闭钩子,关闭程序时,关闭上下文 释放资源 context.registerShutdownHook(); } catch (AccessControlException ex) { // Not allowed in some environments. } } } 添加钩子这个可以不用管,不影响逻辑. @Deprecated protected void refresh(ApplicationContext applicationContext) { Assert.isInstanceOf(ConfigurableApplicationContext.class, applicationContext); refresh((ConfigurableApplicationContext) applicationContext); } 虽然这个方法过时,但spring还是没有直接删除,真够良心的,不像某Final,直接删,真TMSB. protected void refresh(ConfigurableApplicationContext applicationContext) { applicationContext.refresh(); } 注意这里的context 实际上是AnnotationConfigServletWebServerApplicationContext,而AnnotationConfigServletWebServerApplicationContext没有重写这个方法,是继承的它父类ServletWebServerApplicationContext. // ServletWebServerApplicationContext @Override public final void refresh() throws BeansException, IllegalStateException { try { super.refresh(); } catch (RuntimeException ex) { stopAndReleaseWebServer(); throw ex; } } ServletWebServerApplicationContext的父类是AbstractApplicationContext // AbstractApplicationContext public void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { // Prepare this context for refreshing. // 准备刷新上下文环境 prepareRefresh(); // Tell the subclass to refresh the internal bean factory. // 初始化beanFactory,进行xml预读取 ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // Prepare the bean factory for use in this context. // 对beanFactory进行填充 prepareBeanFactory(beanFactory); try { // Allows post-processing of the bean factory in context subclasses. // 子类覆盖方法做额外的处理 postProcessBeanFactory(beanFactory); // Invoke factory processors registered as beans in the context. // 激活各种beanFactoryProcessors invokeBeanFactoryPostProcessors(beanFactory); // Register bean processors that intercept bean creation. //注册拦截bean创建的bean处理器 registerBeanPostProcessors(beanFactory); // Initialize message source for this context. // 初始化上下文的消息源 initMessageSource(); // Initialize event multicaster for this context. // 初始化上下文的消息广播 initApplicationEventMulticaster(); // Initialize other special beans in specific context subclasses. // 留给子类来初始化其他的bean onRefresh(); // Check for listener beans and register them. // 注册所有bean的监听器 registerListeners(); // Instantiate all remaining (non-lazy-init) singletons. // 初始化延迟加载的bean finishBeanFactoryInitialization(beanFactory); // Last step: publish corresponding event. // 最后一步,发布消息 finishRefresh(); } catch (BeansException ex) { if (logger.isWarnEnabled()) { logger.warn("Exception encountered during context initialization - " + "cancelling refresh attempt: " + ex); } //发生异常,销毁所有bean // Destroy already created singletons to avoid dangling resources. destroyBeans(); // Reset 'active' flag. // 重置flag cancelRefresh(ex); // Propagate exception to caller. throw ex; } finally { // Reset common introspection caches in Spring's core, since we // might not ever need metadata for singleton beans anymore... // 重置缓存 resetCommonCaches(); } } } 这里的重点是onRefresh,这里onRefresh是由子类ServletWebServerApplicationContext进行实现的. 4.6 onRefresh // ServletWebServerApplicationContext @Override protected void onRefresh() { super.onRefresh(); try { // 创建server createWebServer(); } catch (Throwable ex) { throw new ApplicationContextException("Unable to start web server", ex); } } 终于绕到了创建 server 这里了,不知少侠是否还记得那个配置类??ServletWebServerFactoryConfiguration.那么现在才真正的开始了. private void createWebServer() { WebServer webServer = this.webServer; // 获取servlet 上下文 ServletContext servletContext = getServletContext(); // 如果 server 为空 或者 servlet上下文为空,就创建server if (webServer == null && servletContext == null) { ServletWebServerFactory factory = getWebServerFactory(); this.webServer = factory.getWebServer(getSelfInitializer()); } else if (servletContext != null) { try { getSelfInitializer().onStartup(servletContext); } catch (ServletException ex) { throw new ApplicationContextException("Cannot initialize servlet context", ex); } } initPropertySources(); } 判断server是否创建 未创建就创建 初始化 初始化资源 这里的 getWebServerFactory() 方法从容器中获取的,容器里面的是之前配置类中创建的. // 从容器中获取 ServletWebServerFactory protected ServletWebServerFactory getWebServerFactory() { // Use bean names so that we don't consider the hierarchy String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class); if (beanNames.length == 0) { throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to missing " + "ServletWebServerFactory bean."); } if (beanNames.length > 1) { throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple " + "ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames)); } return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class); } 五、tomcat的创建 前面千辛万苦的获取到了 tomcatServer的工厂,接下来就看看是怎么创建的吧. // TomcatServletWebServerFactory @Override public WebServer getWebServer(ServletContextInitializer... initializers) { // .. 省略 // 初始化 prepareContext(tomcat.getHost(), initializers); return getTomcatWebServer(tomcat); } 这里ServletContextInitializer是不是和ServletContainerInitializer有点神似?别说不仔细看还是会看错,至于这两个是啥关系,这里就不琢磨了,毕竟这个不是重点. 5.1 初始化 // TomcatServletWebServerFactory protected void prepareContext(Host host, ServletContextInitializer[] initializers) { //... 省略 // 添加监听器 context.addLifecycleListener(new StaticResourceConfigurer(context)); ServletContextInitializer[] initializersToUse = mergeInitializers(initializers); host.addChild(context); configureContext(context, initializersToUse); postProcessContext(context); } 在初始化过程中添加了一个名字为静态资源配置的监听器,名字都很怪怪的好吧.去看看这个监听器是干嘛的. private final class StaticResourceConfigurer implements LifecycleListener { private final Context context; private StaticResourceConfigurer(Context context) { this.context = context; } @Override public void lifecycleEvent(LifecycleEvent event) { // 判断时机 if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) { addResourceJars(getUrlsOfJarsWithMetaInfResources()); } } private void addResourceJars(List<URL> resourceJarUrls) { for (URL url : resourceJarUrls) { String path = url.getPath(); if (path.endsWith(".jar") || path.endsWith(".jar!/")) { String jar = url.toString(); if (!jar.startsWith("jar:")) { // A jar file in the file system. Convert to Jar URL. jar = "jar:" + jar + "!/"; } addResourceSet(jar); } else { addResourceSet(url.toString()); } } } private void addResourceSet(String resource) { try { if (isInsideNestedJar(resource)) { // It's a nested jar but we now don't want the suffix because Tomcat // is going to try and locate it as a root URL (not the resource // inside it) resource = resource.substring(0, resource.length() - 2); } URL url = new URL(resource); String path = "/META-INF/resources"; this.context.getResources().createWebResourceSet(ResourceSetType.RESOURCE_JAR, "/", url, path); } catch (Exception ex) { // Ignore (probably not a directory) } } private boolean isInsideNestedJar(String dir) { return dir.indexOf("!/") < dir.lastIndexOf("!/"); } } 从这个监听器的代码逻辑上看 是往context.getResources丢东西呀.貌似是路径啥的,这会不会与开头的404问题有关呢? createWebResourceSet这个方法嫌疑很大,去看看. // StandardRoot public void createWebResourceSet(ResourceSetType type, String webAppMount, String base, String archivePath, String internalPath) { List<WebResourceSet> resourceList; WebResourceSet resourceSet; switch (type) { case PRE: resourceList = preResources; break; case CLASSES_JAR: resourceList = classResources; break; case RESOURCE_JAR: resourceList = jarResources; break; case POST: resourceList = postResources; break; default: throw new IllegalArgumentException( sm.getString("standardRoot.createUnknownType", type)); } // ..... 省略 resourceList.add(resourceSet); } 以上逻辑为根据不同的类型,往不同的list中添加路径.好吧还是看不出来这个到底有什么用,404的问题也没找到,不如果跟踪一下请求看看. 六、请求的跟踪 springMVC对请求的处理逻辑一般为 DispatcherServlet接管请求->查找handler->查找handlerDapter->视图解析器->解析视图->渲染视图. 虽然这个逻辑不不完善,可能还是错的,但差不多,笔者认为哈. 那么问题来了,在springMVC中的视图解析器是啥呢?看看ViewResolver的子类就知道是InternalResourceViewResolver了.但这个没啥用呀,因为具体的渲染逻辑是在视图对象里,解析器在这里没啥太大的用处.所以来看看jsp的视图JstlView中的操作吧. public class JstlView extends InternalResourceView { @Nullable private MessageSource messageSource; /** * Constructor for use as a bean. * @see #setUrl */ public JstlView() { } /** * Create a new JstlView with the given URL. * @param url the URL to forward to */ public JstlView(String url) { super(url); } /** * Create a new JstlView with the given URL. * @param url the URL to forward to * @param messageSource the MessageSource to expose to JSTL tags * (will be wrapped with a JSTL-aware MessageSource that is aware of JSTL's * {@code javax.servlet.jsp.jstl.fmt.localizationContext} context-param) * @see JstlUtils#getJstlAwareMessageSource */ public JstlView(String url, MessageSource messageSource) { this(url); this.messageSource = messageSource; } /** * Wraps the MessageSource with a JSTL-aware MessageSource that is aware * of JSTL's {@code javax.servlet.jsp.jstl.fmt.localizationContext} * context-param. * @see JstlUtils#getJstlAwareMessageSource */ @Override protected void initServletContext(ServletContext servletContext) { if (this.messageSource != null) { this.messageSource = JstlUtils.getJstlAwareMessageSource(servletContext, this.messageSource); } super.initServletContext(servletContext); } /** * Exposes a JSTL LocalizationContext for Spring's locale and MessageSource. * @see JstlUtils#exposeLocalizationContext */ @Override protected void exposeHelpers(HttpServletRequest request) throws Exception { if (this.messageSource != null) { JstlUtils.exposeLocalizationContext(request, this.messageSource); } else { JstlUtils.exposeLocalizationContext(new RequestContext(request, getServletContext())); } } } 在jstlView中并没有看到jsp的处理逻辑,去父类看看. public class InternalResourceView extends AbstractUrlBasedView { // 省略... /** * Render the internal resource given the specified model. * This includes setting the model as request attributes. */ @Override protected void renderMergedOutputModel( Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { // Expose the model object as request attributes. exposeModelAsRequestAttributes(model, request); // Expose helpers as request attributes, if any. exposeHelpers(request); // Determine the path for the request dispatcher. String dispatcherPath = prepareForRendering(request, response); // Obtain a RequestDispatcher for the target resource (typically a JSP). RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath); if (rd == null) { throw new ServletException("Could not get RequestDispatcher for [" + getUrl() + "]: Check that the corresponding file exists within your web application archive!"); } // If already included or response already committed, perform include, else forward. if (useInclude(request, response)) { response.setContentType(getContentType()); if (logger.isDebugEnabled()) { logger.debug("Including [" + getUrl() + "]"); } rd.include(request, response); } else { // Note: The forwarded resource is supposed to determine the content type itself. if (logger.isDebugEnabled()) { logger.debug("Forwarding to [" + getUrl() + "]"); } rd.forward(request, response); } } // 省略.... } 核心处理逻辑在renderMergedOutputModel,经过renderMergedOutputModel方法一顿骚操作以后,最后发现请求被RequestDispatcher接管了.emmm,线索又断了,那么最后是被谁接管的呢?在j2ee的世界里,能被啥接管?不就是个servlet或者filter嘛. 那么接下来要搞清楚是谁接管了请求,并渲染了jsp,虽然答案很显然了,但还是要走一下流程. 6.1 猜测是servlet接管了请求 springMVC 里 servlet?那不就是DispatcherServlet嘛?但总觉得不可能,不可能请求从DispatcherServlet来又回去吧?那就看看在创建tomcat的时候有没有注册其他的servelt. // TomcatServletWebServerFactory protected void prepareContext(Host host, ServletContextInitializer[] initializers) { // 省略 .. if (isRegisterDefaultServlet()) { addDefaultServlet(context); } if (shouldRegisterJspServlet()) { addJspServlet(context); addJasperInitializer(context); } // 省略 ... } 可以看到在初始化的时候,注册了两个servlet,一个默认的servlet一个是jsp的servlet // TomcatServletWebServerFactory private void addDefaultServlet(Context context) { Wrapper defaultServlet = context.createWrapper(); // 名称 defaultServlet.setName("default"); defaultServlet.setServletClass("org.apache.catalina.servlets.DefaultServlet"); defaultServlet.addInitParameter("debug", "0"); defaultServlet.addInitParameter("listings", "false"); defaultServlet.setLoadOnStartup(1); // Otherwise the default location of a Spring DispatcherServlet cannot be set // 是否运行覆盖,这是为了dispatcherServlet做准备 // 方便在后免把这个默认的servlet给覆盖掉 defaultServlet.setOverridable(true); context.addChild(defaultServlet); context.addServletMappingDecoded("/", "default"); } private void addJspServlet(Context context) { Wrapper jspServlet = context.createWrapper(); jspServlet.setName("jsp"); jspServlet.setServletClass(getJsp().getClassName()); jspServlet.addInitParameter("fork", "false"); getJsp().getInitParameters().forEach(jspServlet::addInitParameter); jspServlet.setLoadOnStartup(3); context.addChild(jspServlet); // 拦截 *.jsp 后缀的请求 context.addServletMappingDecoded("*.jsp", "jsp"); context.addServletMappingDecoded("*.jspx", "jsp"); } 两个servlet,一个默认的,一个jsp的.看到这儿会不会有点奇怪,为啥不注册dispatcherServlet?emmm不是很明白昂,但是这个核心的servlet是不会落下的. @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) @Configuration(proxyBeanMethods = false) /** * 匹配容器为 servlet * **/ @ConditionalOnWebApplication(type = Type.SERVLET) /** * 判断 class path 路径下有 DispatcherServlet.class * **/ @ConditionalOnClass(DispatcherServlet.class) /*** * 在 ServletWebServerFactoryAutoConfiguration 之后生效 * */ @AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class) public class DispatcherServletAutoConfiguration { /* * The bean name for a DispatcherServlet that will be mapped to the root URL "/" */ public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet"; /* * The bean name for a ServletRegistrationBean for the DispatcherServlet "/" */ public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration"; @Configuration(proxyBeanMethods = false) @Conditional(DefaultDispatcherServletCondition.class) @ConditionalOnClass(ServletRegistration.class) // 启用配置 WebMvcProperties @EnableConfigurationProperties(WebMvcProperties.class) protected static class DispatcherServletConfiguration { @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) { DispatcherServlet dispatcherServlet = new DispatcherServlet(); dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest()); dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest()); dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound()); dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents()); dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails()); return dispatcherServlet; } @Bean @ConditionalOnBean(MultipartResolver.class) @ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) public MultipartResolver multipartResolver(MultipartResolver resolver) { // Detect if the user has created a MultipartResolver but named it incorrectly return resolver; } } @Configuration(proxyBeanMethods = false) @Conditional(DispatcherServletRegistrationCondition.class) @ConditionalOnClass(ServletRegistration.class) @EnableConfigurationProperties(WebMvcProperties.class) @Import(DispatcherServletConfiguration.class) protected static class DispatcherServletRegistrationConfiguration { @Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME) @ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) { DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath()); // 名称 registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME); // 启动顺序 registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup()); multipartConfig.ifAvailable(registration::setMultipartConfig); return registration; } } // 省略.... } 在自动配置的这个类里面生成了一个DispatcherServletRegistrationBean对象,这个对象就是用于组测dispatcherServlet的.DispatcherServletRegistrationBean是ServletContextInitializer的子类,用于初始化,注册等操作等. 那么这个对象是在什么时候注册的呢?又要回到tomcat创建的时候了. // TomcatServletWebServerFactory @Override public WebServer getWebServer(ServletContextInitializer... initializers) { // 省略.. prepareContext(tomcat.getHost(), initializers); return getTomcatWebServer(tomcat); } 在调用初始化上下文的时候把这个玩意儿给传进去了. // TomcatServletWebServerFactory protected void prepareContext(Host host, ServletContextInitializer[] initializers) { // 省略... ServletContextInitializer[] initializersToUse = mergeInitializers(initializers); host.addChild(context); configureContext(context, initializersToUse); postProcessContext(context); } 这里合并了所有的ServletContextInitializer对象,传入到了configureContext方法中. // TomcatServletWebServerFactory protected void configureContext(Context context, ServletContextInitializer[] initializers) { TomcatStarter starter = new TomcatStarter(initializers); context.addServletContainerInitializer(starter, NO_CLASSES); } 这里把ServletContextInitializer转成了一个TomcatStarter对象,并把这个对象添加到了上下文中去.这个 TomcatStarter就厉害了. class TomcatStarter implements ServletContainerInitializer { private static final Log logger = LogFactory.getLog(TomcatStarter.class); private final ServletContextInitializer[] initializers; private volatile Exception startUpException; TomcatStarter(ServletContextInitializer[] initializers) { this.initializers = initializers; } @Override public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException { try { // 初始化所有需要 初始化的类 for (ServletContextInitializer initializer : this.initializers) { initializer.onStartup(servletContext); } } catch (Exception ex) { this.startUpException = ex; // Prevent Tomcat from logging and re-throwing when we know we can // deal with it in the main thread, but log for information here. if (logger.isErrorEnabled()) { logger.error("Error starting Tomcat context. Exception: " + ex.getClass().getName() + ". Message: " + ex.getMessage()); } } } Exception getStartUpException() { return this.startUpException; } } TomcatStarter实现了ServletContainerInitializer接口,这个接口就厉害了,在servlet的生命中期中,会调用实现这个接口的onStartup方法,至于什么是servlet的生命周期,就不引出了,估计一时半会也说不完. 嗯,知道这个东西的厉害,说了这么多,也没说ServletContextInitializer这个东西哪来的.不是方法传进来的嘛?不行就回去看看. @Override public WebServer getWebServer(ServletContextInitializer... initializers) { // 省略... return getTomcatWebServer(tomcat); } 嗯,是传进来的,怎么传进来的?当然是创建的时候传的了0.0 // ServletWebServerApplicationContext private void createWebServer() { WebServer webServer = this.webServer; ServletContext servletContext = getServletContext(); if (webServer == null && servletContext == null) { ServletWebServerFactory factory = getWebServerFactory(); this.webServer = factory.getWebServer(getSelfInitializer()); } else if (servletContext != null) { try { getSelfInitializer().onStartup(servletContext); } catch (ServletException ex) { throw new ApplicationContextException("Cannot initialize servlet context", ex); } } initPropertySources(); } 6.2 ServletContextInitializer 怎么来的 // ServletWebServerApplicationContext private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() { return this::selfInitialize; } private void selfInitialize(ServletContext servletContext) throws ServletException { prepareWebApplicationContext(servletContext); registerApplicationScope(servletContext); WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext); for (ServletContextInitializer beans : getServletContextInitializerBeans()) { beans.onStartup(servletContext); } } 这就是了,那配置类DispatcherServletAutoConfiguration里面的DispatcherServletRegistrationBean哪去了?这里的确没有,不过在getServletContextInitializerBeans()方法返回的集合里面. // ServletWebServerApplicationContext protected Collection<ServletContextInitializer> getServletContextInitializerBeans() { return new ServletContextInitializerBeans(getBeanFactory()); } ServletContextInitializerBeans是一个继承了AbstractCollection的集合对象. // ServletContextInitializerBeans public ServletContextInitializerBeans(ListableBeanFactory beanFactory, Class<? extends ServletContextInitializer>... initializerTypes) { this.initializers = new LinkedMultiValueMap<>(); this.initializerTypes = (initializerTypes.length != 0) ? Arrays.asList(initializerTypes) : Collections.singletonList(ServletContextInitializer.class); addServletContextInitializerBeans(beanFactory); addAdaptableBeans(beanFactory); List<ServletContextInitializer> sortedInitializers = this.initializers.values().stream() .flatMap((value) -> value.stream().sorted(AnnotationAwareOrderComparator.INSTANCE)) .collect(Collectors.toList()); this.sortedList = Collections.unmodifiableList(sortedInitializers); logMappings(this.initializers); } 核心逻辑就在构造方法中的addServletContextInitializerBeans方法中. // ServletContextInitializerBeans private void addServletContextInitializerBeans(ListableBeanFactory beanFactory) { for (Class<? extends ServletContextInitializer> initializerType : this.initializerTypes) { for (Entry<String, ? extends ServletContextInitializer> initializerBean : getOrderedBeansOfType(beanFactory, initializerType)) { addServletContextInitializerBean(initializerBean.getKey(), initializerBean.getValue(), beanFactory); } } } private <T> List<Entry<String, T>> getOrderedBeansOfType(ListableBeanFactory beanFactory, Class<T> type) { return getOrderedBeansOfType(beanFactory, type, Collections.emptySet()); } private <T> List<Entry<String, T>> getOrderedBeansOfType(ListableBeanFactory beanFactory, Class<T> type, Set<?> excludes) { String[] names = beanFactory.getBeanNamesForType(type, true, false); Map<String, T> map = new LinkedHashMap<>(); for (String name : names) { if (!excludes.contains(name) && !ScopedProxyUtils.isScopedTarget(name)) { T bean = beanFactory.getBean(name, type); if (!excludes.contains(bean)) { map.put(name, bean); } } } List<Entry<String, T>> beans = new ArrayList<>(map.entrySet()); beans.sort((o1, o2) -> AnnotationAwareOrderComparator.INSTANCE.compare(o1.getValue(), o2.getValue())); return beans; } 从ioc中获取所有ServletContextInitialize的子类,而这个bean就刚好是之前配置类中的bean,DispatcherServletRegistrationBean. 至于注册的逻辑就不阐述了,很简单,顺着DispatcherServletRegistrationBean的父类RegistrationBean看下去就好. 七、整理线索 前面说了那么多跟404完全没有关系呀,其实前面是在做铺垫而已,看官别着急.由前文的逻辑可以知道在tomcat里面至少注册了2个servetl,一个是spring的(后面覆盖的),一个是jsp的.那么完全由理由猜测,最后由JstlView转发的请求到了jspServlet里面.来看看jspSerlvet里面干了啥? // JspServlet public void service (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 省略.... try { serviceJspFile(request, response, jspUri, precompile); } catch (RuntimeException e) { throw e; } catch (ServletException e) { throw e; } catch (IOException e) { throw e; } catch (Throwable e) { ExceptionUtils.handleThrowable(e); throw new ServletException(e); } } 别问我为啥只贴了service这个方法.先看看第一个方法serviceJspFile. // JspServlet private void serviceJspFile(HttpServletRequest request, HttpServletResponse response, String jspUri, boolean precompile) throws ServletException, IOException { JspServletWrapper wrapper = rctxt.getWrapper(jspUri); if (wrapper == null) { synchronized(this) { wrapper = rctxt.getWrapper(jspUri); if (wrapper == null) { // Check if the requested JSP page exists, to avoid // creating unnecessary directories and files. // 判断 jsp 文件是否存在 if (null == context.getResource(jspUri)) { handleMissingResource(request, response, jspUri); return; } wrapper = new JspServletWrapper(config, options, jspUri, rctxt); rctxt.addWrapper(jspUri,wrapper); } } } try { wrapper.service(request, response, precompile); } catch (FileNotFoundException fnfe) { handleMissingResource(request, response, jspUri); } } 诺,核心代码被抓了.这个SerlvetContext,既然在tomcat里面那么它的实现类肯定是ApplicationContext,不用怀疑,不信你跟跟流程看看. 既然是ApplicationContext中获取资源,那来看看是怎么拿的文件吧. // ApplicationContext public URL getResource(String path) throws MalformedURLException { // 省略... WebResourceRoot resources = context.getResources(); if (resources != null) { // 获取资源 return resources.getResource(validatedPath).getURL(); } return null; } 转战到 WebResourceRoot里去,WebResourceRoot是个接口,在本案例中的唯一子类是StandardRoot. // StandardRoot @Override public WebResource getResource(String path) { return getResource(path, true, false); } protected WebResource getResource(String path, boolean validate, boolean useClassLoaderResources) { if (validate) { path = validate(path); } // 判断是否允许缓存,这个默认值是true if (isCachingAllowed()) { return cache.getResource(path, useClassLoaderResources); } else { return getResourceInternal(path, useClassLoaderResources); } } 这里分两种情况,一个是从换成里面获取,另外则从非缓存中获取,缓存里面的世界很精彩的. 7.1 从缓存里面获取 // Cache protected WebResource getResource(String path, boolean useClassLoaderResources) { // 省略.... // 判断是否获取到缓存 if (cacheEntry == null) { // Local copy to ensure consistency int objectMaxSizeBytes = getObjectMaxSizeBytes(); // 创建缓存对象 CachedResource newCacheEntry = new CachedResource(this, root, path, getTtl(), objectMaxSizeBytes, useClassLoaderResources); // Concurrent callers will end up with the same CachedResource // instance // 放入缓存中 cacheEntry = resourceCache.putIfAbsent(path, newCacheEntry); // 二次判断 if (cacheEntry == null) { // newCacheEntry was inserted into the cache - validate it cacheEntry = newCacheEntry; // 验证资源的合法性(这里就石锤了) cacheEntry.validateResource(useClassLoaderResources); // 省略.... return cacheEntry; } 还差2步 // CachedResource protected boolean validateResource(boolean useClassLoaderResources) { // 省略.. // 非空检查 if (webResource == null) { // 加锁 synchronized (this) { // 双重检查 if (webResource == null) { webResource = root.getResourceInternal( webAppPath, useClassLoaderResources); getLastModified(); getContentLength(); nextCheck = ttl + now; // exists() is a relatively expensive check for a file so // use the fact that we know if it exists at this point if (webResource instanceof EmptyResource) { cachedExists = Boolean.FALSE; } else { cachedExists = Boolean.TRUE; } return true; } } } } 程序初次允许的时候,webResource肯定是为空的,别说缓存了.这里的root是StandardRoot,又调用回去了. // StandardRoot private final List<List<WebResourceSet>> allResources = new ArrayList<>(); { allResources.add(preResources); allResources.add(mainResources); allResources.add(classResources); allResources.add(jarResources); allResources.add(postResources); } protected final WebResource getResourceInternal(String path, boolean useClassLoaderResources) { WebResource result = null; WebResource virtual = null; WebResource mainEmpty = null; for (List<WebResourceSet> list : allResources) { for (WebResourceSet webResourceSet : list) { if (!useClassLoaderResources && !webResourceSet.getClassLoaderOnly() || useClassLoaderResources && !webResourceSet.getStaticOnly()) { result = webResourceSet.getResource(path); if (result.exists()) { return result; } if (virtual == null) { if (result.isVirtual()) { virtual = result; } else if (main.equals(webResourceSet)) { mainEmpty = result; } } } } } // 省略... // Default is empty resource in main resources return mainEmpty; } 这里遍历不同资源类别,来判断文件是否存在,如果存在就返回,然后放入缓存中.至于从非缓存中获取的逻辑就上面getResourceInternal的逻辑,就不罗嗦了. 嗯,这个有啥用呢?还是未解决404的问题呀,别急快了. 八、被遗忘的静态资源监听器(StaticResourceConfigurer) 少侠是否记得在创建tomcat的时候在servletContext中添加了个这个监听器呢? // TomcatServletWebServerFactory protected void prepareContext(Host host, ServletContextInitializer[] initializers) { // 省略... context.addLifecycleListener(new StaticResourceConfigurer(context)); // 省略... } 再来看看这个监听器的逻辑吧. // TomcatServletWebServerFactory$StaticResourceConfigurer private final class StaticResourceConfigurer implements LifecycleListener { private final Context context; private StaticResourceConfigurer(Context context) { this.context = context; } @Override public void lifecycleEvent(LifecycleEvent event) { if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) { addResourceJars(getUrlsOfJarsWithMetaInfResources()); } } private void addResourceJars(List<URL> resourceJarUrls) { for (URL url : resourceJarUrls) { String path = url.getPath(); if (path.endsWith(".jar") || path.endsWith(".jar!/")) { String jar = url.toString(); if (!jar.startsWith("jar:")) { // A jar file in the file system. Convert to Jar URL. jar = "jar:" + jar + "!/"; } addResourceSet(jar); } else { addResourceSet(url.toString()); } } } private void addResourceSet(String resource) { try { if (isInsideNestedJar(resource)) { // It's a nested jar but we now don't want the suffix because Tomcat // is going to try and locate it as a root URL (not the resource // inside it) resource = resource.substring(0, resource.length() - 2); } URL url = new URL(resource); String path = "/META-INF/resources"; this.context.getResources().createWebResourceSet(ResourceSetType.RESOURCE_JAR, "/", url, path); } catch (Exception ex) { // Ignore (probably not a directory) } } private boolean isInsideNestedJar(String dir) { return dir.indexOf("!/") < dir.lastIndexOf("!/"); } } 无论逻辑怎么变化,最后监听器的代码都会走到this.context.getResources().createWebResourceSet(ResourceSetType.RESOURCE_JAR, "/", url, path);这句话来. 看到ResourceSetType.RESOURCE_JAR这个常量是否有点感觉呢?没感觉就脱掉衣服再看看. // StandardRoot public void createWebResourceSet(ResourceSetType type, String webAppMount, URL url, String internalPath) { BaseLocation baseLocation = new BaseLocation(url); createWebResourceSet(type, webAppMount, baseLocation.getBasePath(), baseLocation.getArchivePath(), internalPath); } @Override public void createWebResourceSet(ResourceSetType type, String webAppMount, String base, String archivePath, String internalPath) { List<WebResourceSet> resourceList; WebResourceSet resourceSet; switch (type) { case PRE: resourceList = preResources; break; case CLASSES_JAR: resourceList = classResources; break; case RESOURCE_JAR: resourceList = jarResources; break; case POST: resourceList = postResources; break; default: throw new IllegalArgumentException( sm.getString("standardRoot.createUnknownType", type)); } // 省略 resourceList.add(resourceSet); } 是不是这一切都园回去了?是不是感觉疑惑都没了?如果还是不解在去看看资源解析的那块儿. 九、真相 为甚么是404呢?那是因为在StandardRoot里面的resourceList中不存在指定的根路径,所以是404.又要有小伙伴要杠了,说MATE-INFO目录下面的都没问题. 是,没问题.因为springBoot把这个路径加进去了.不信你看. // StaticResourceConfigurer private final class StaticResourceConfigurer implements LifecycleListener { private final Context context; private StaticResourceConfigurer(Context context) { this.context = context; } @Override public void lifecycleEvent(LifecycleEvent event) { if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) { addResourceJars(getUrlsOfJarsWithMetaInfResources()); } } private void addResourceJars(List<URL> resourceJarUrls) { // 省略 } private void addResourceSet(String resource) { // 省略 } private boolean isInsideNestedJar(String dir) { return dir.indexOf("!/") < dir.lastIndexOf("!/"); } } 在getUrlsOfJarsWithMetaInfResources方法里面就有这个路径.逻辑简单就不贴出来了. 9.1 如何解决404 根据以上的分析,可以和spring一样弄个监听器,在resourceList添加指定的路径.最简单暴力的方法如下: @Controller @EnableAutoConfiguration @Configuration public class Main { public static void main(String[] args) { SpringApplication.run(Main.class,args); } @RequestMapping("/test-static") public String testStatic(){ return "index-static"; } @RequestMapping("/test-public") public String testPublic(){ return "index-public"; } @RequestMapping("/test-resources") public String testResources(){ return "index-resources"; } @RequestMapping("/test-meta") public String testMeta(){ return "index-meta"; } @Bean public TomcatServletWebServerFactory getTomcatServletWebServerFactory(){ return new AdvTomcatServletWebServerFactory(); } } 自定义工厂: public class AdvTomcatServletWebServerFactory extends TomcatServletWebServerFactory { private Context context; public AdvTomcatServletWebServerFactory(){ getContextLifecycleListeners().add(new AdvResourceListener()); } // 这个方法是 spring留下的模板方法 // 可以通过这个方法进行扩展 @Override protected void postProcessContext(Context context) { this.context = context; } private class AdvResourceListener implements LifecycleListener{ @Override public void lifecycleEvent(LifecycleEvent event) { if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) { final URL resource = AdvTomcatServletWebServerFactory.class.getClassLoader().getResource("."); final WebResourceRoot resources = AdvTomcatServletWebServerFactory.this.context.getResources(); resources.createWebResourceSet(WebResourceRoot.ResourceSetType.RESOURCE_JAR, "/", resource, "/static"); resources.createWebResourceSet(WebResourceRoot.ResourceSetType.RESOURCE_JAR, "/", resource, "/public"); resources.createWebResourceSet(WebResourceRoot.ResourceSetType.RESOURCE_JAR, "/", resource, "/resources"); } } } } 通过自己的工厂替换掉spring的工厂,实现注册自己的监听器,当然方法还有很多,如果你熟悉tomcat的启动流程的话. 十、小结 在springBoot的懒人套餐下,出现问题往往会让人防不胜防呀,比如这个404,根本没有任何信息说哪里有问题(除非是开了debug日志). 本来就想谢谢解决方案的,结果写了这么多,吃饭了.