這段時間本意是想要研究一下Netty的多線程異步NIO通訊框架,看完原理想要做下源碼分析。查找資料發現Jetty框架底層支持用Netty做web請求的多線程分發處理,於是就籌備着將Jetty框架內嵌到手頭的一個測試項目中,調試源碼分析實現原理。結果這集成一搞就是兩天,有些細節部分還是要真正接觸之后才會了解,為此特意整理博客一篇,就集成過程中的問題做一下總結。
項目說明:Maven多模塊,springmvc,spring-security;
參考項目:JFinal(國產優秀的mvc開發框架);
問題說明:1、設置webapp根目錄后,項目根本無法啟動;2、解決項目根路徑問題后,spring-security過濾器鏈不執行;
一、內嵌的Jetty服務最終實現
1.1、Jetty服務的站點根目錄與監聽端口號設置代碼實現

1 package com.train.simulate.web.server; 2 3 public class JettyServerRun { 4 public static final int DEFAULT_PORT = 8898; 5 public static final String DEFAULT_CONTEXT_PATH = "/train"; 6 private static final String DEFAULT_APP_CONTEXT_PATH = "src/main/webapp"; 7 8 9 public static void main(String[] args) { 10 11 runJettyServer(DEFAULT_PORT, DEFAULT_CONTEXT_PATH); 12 13 } 14 15 public static void runJettyServer(int port, String contextPath) { 16 17 new JettyServerForIDEA(DEFAULT_APP_CONTEXT_PATH,port,contextPath).start(); 18 19 } 20 }

1 /** 2 * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com). 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.train.simulate.web.server; 18 19 import java.io.File; 20 import java.io.IOException; 21 import java.net.DatagramSocket; 22 import java.net.ServerSocket; 23 import java.util.EnumSet; 24 25 import com.train.simulate.common.utils.*; 26 import org.eclipse.jetty.server.DispatcherType; 27 import org.eclipse.jetty.server.Server; 28 import org.eclipse.jetty.server.SessionManager; 29 import org.eclipse.jetty.server.nio.SelectChannelConnector; 30 import org.eclipse.jetty.server.session.HashSessionManager; 31 import org.eclipse.jetty.server.session.SessionHandler; 32 import org.eclipse.jetty.servlet.FilterHolder; 33 import org.eclipse.jetty.webapp.WebAppContext; 34 import org.springframework.web.filter.DelegatingFilterProxy; 35 36 /** 37 * IDEA 專用於在 IDEA 之下用 main 方法啟動,原啟動方式在 IDEA 下會報異常 38 * 注意:用此方法啟動對熱加載支持不完全,只支持方法內部修改的熱加載,不支持添加、刪除方法 39 * 不支持添加類文件熱加載,建議開發者在 IDEA 下使用 jrebel 或者 maven 下的 jetty 40 * 插件支持列為完全的熱加載 41 */ 42 public class JettyServerForIDEA implements IServer { 43 44 private String webAppDir; 45 private int port; 46 private String context; 47 // private int scanIntervalSeconds; 48 private boolean running = false; 49 private Server server; 50 private WebAppContext webApp; 51 52 public JettyServerForIDEA(String webAppDir, int port, String context) { 53 if (webAppDir == null) { 54 throw new IllegalStateException("Invalid webAppDir of web server: " + webAppDir); 55 } 56 if (port < 0 || port > 65535) { 57 throw new IllegalArgumentException("Invalid port of web server: " + port); 58 } 59 if (StrKit.isBlank(context)) { 60 throw new IllegalStateException("Invalid context of web server: " + context); 61 } 62 63 this.webAppDir = webAppDir; 64 this.port = port; 65 this.context = context; 66 // this.scanIntervalSeconds = scanIntervalSeconds; 67 } 68 69 public void start() { 70 if (!running) { 71 try { 72 running = true; 73 doStart(); 74 } catch (Exception e) { 75 System.err.println(e.getMessage()); 76 //LogKit.error(e.getMessage(), e); 77 } 78 } 79 } 80 81 public void stop() { 82 if (running) { 83 try {server.stop();} catch (Exception e) { 84 //LogKit.error(e.getMessage(), e); 85 System.err.println(e.getMessage()); 86 } 87 running = false; 88 } 89 } 90 91 private void doStart() { 92 if (!available(port)) { 93 throw new IllegalStateException("port: " + port + " already in use!"); 94 } 95 deleteSessionData(); 96 System.out.println("Starting Jetty ...... "); 97 server = new Server(); 98 SelectChannelConnector connector = new SelectChannelConnector(); 99 connector.setPort(port); 100 server.addConnector(connector); 101 webApp = new WebAppContext(); 102 webApp.setThrowUnavailableOnStartupException(true); // 在啟動過程中允許拋出異常終止啟動並退出 JVM 103 webApp.setContextPath(context); 104 String rootPath = PathKit.getWebRootPath(); 105 System.out.println(rootPath); 106 String webDir = rootPath + "/" + webAppDir; 107 //webApp.setDescriptor(webDir + "/WEB-INF/web.xml"); 108 webApp.setResourceBase(webDir); // webApp.setWar(webAppDir); 109 //postStart(webApp); 110 webApp.setWelcomeFiles(new String[]{"/index.do"}); 111 webApp.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false"); 112 webApp.setInitParameter("org.eclipse.jetty.servlet.Default.useFileMappedBuffer", "false"); // webApp.setInitParams(Collections.singletonMap("org.mortbay.jetty.servlet.Default.useFileMappedBuffer", "false")); 113 persistSession(webApp); 114 115 server.setHandler(webApp); 116 try { 117 System.out.println("Starting web server on port: " + port); 118 server.start(); 119 System.out.println("Starting Complete. Welcome To The Jetty :)"); 120 server.join(); 121 } catch (Exception e) { 122 //LogKit.error(e.getMessage(), e); 123 System.err.println(e.getMessage()); 124 System.exit(100); 125 } 126 return; 127 } 128 private void postStart(WebAppContext root){ 129 /**spring內部過濾器代理 里面包含了默認的11個過濾器 這里的初始化參數可以直接些spring的bean名稱*/ 130 FilterHolder filterHolder=new FilterHolder(DelegatingFilterProxy.class); 131 filterHolder.setName("springSecurityFilterChain"); 132 root.addFilter(filterHolder, "/*", EnumSet.of(DispatcherType.REQUEST)); 133 } 134 135 private void deleteSessionData() { 136 try { 137 FileKit.delete(new File(getStoreDir())); 138 } 139 catch (Exception e) { 140 //LogKit.logNothing(e); 141 System.err.println(e.getMessage()); 142 } 143 } 144 145 private String getStoreDir() { 146 String storeDir = PathKit.getWebRootPath() + "/../../session_data" + context; 147 if ("\\".equals(File.separator)) { 148 storeDir = storeDir.replaceAll("/", "\\\\"); 149 } 150 return storeDir; 151 } 152 153 private void persistSession(WebAppContext webApp) { 154 String storeDir = getStoreDir(); 155 156 SessionManager sm = webApp.getSessionHandler().getSessionManager(); 157 if (sm instanceof HashSessionManager) { 158 try { 159 ((HashSessionManager)sm).setStoreDirectory(new File(storeDir)); 160 } catch (Exception e) { 161 e.printStackTrace(); 162 } 163 return ; 164 } 165 166 HashSessionManager hsm = new HashSessionManager(); 167 try { 168 hsm.setStoreDirectory(new File(storeDir)); 169 } catch (Exception e) { 170 e.printStackTrace(); 171 } 172 SessionHandler sh = new SessionHandler(); 173 sh.setSessionManager(hsm); 174 webApp.setSessionHandler(sh); 175 } 176 177 private static boolean available(int port) { 178 if (port <= 0) { 179 throw new IllegalArgumentException("Invalid start port: " + port); 180 } 181 182 ServerSocket ss = null; 183 DatagramSocket ds = null; 184 try { 185 ss = new ServerSocket(port); 186 ss.setReuseAddress(true); 187 ds = new DatagramSocket(port); 188 ds.setReuseAddress(true); 189 return true; 190 } catch (IOException e) { 191 //LogKit.logNothing(e); 192 System.err.println(e.getMessage()); 193 } finally { 194 if (ds != null) { 195 ds.close(); 196 } 197 198 if (ss != null) { 199 try { 200 ss.close(); 201 } catch (IOException e) { 202 // should not be thrown, just detect port available. 203 // LogKit.logNothing(e); 204 System.err.println(e.getMessage()); 205 } 206 } 207 } 208 return false; 209 } 210 }

1 package com.train.simulate.web.server; 2 3 public interface IServer { 4 void start(); 5 void stop(); 6 }
1.2、Jetty架包的maven配置
<dependency> <groupId>org.eclipse.jetty.aggregate</groupId> <artifactId>jetty-all</artifactId> <version>7.6.0.v20120127</version> <exclusions> <exclusion> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> </exclusion> </exclusions> </dependency>
二、關於站點根目錄設置后,站點無法啟動的問題處理
2.1、在Jfinal中調用org.eclipse.jetty.webapp.WebAppContext.setResourceBase("src/main/webapp") 這樣的相對路徑方式。結果站點無法啟動后,我去跟蹤源碼,發現setResourceBase方法底層使用File.getCanonicalFile方法來獲取站點的絕對根路徑,而這個方法最終調用System.getProperty("user.dir")來獲取項目路徑。具體這個系統變量是如何賦的值,這塊我還沒有具體搞明白,但是跟蹤結果告訴我這個路徑指向的是我的Maven多模塊項目的頂級項目目錄,而不是我想要的web子級項目。因為路徑錯誤,所以無法啟動。
2.2、既然問題已經定位,接下來就有兩種處理方案。一是設置資源根路徑時從web項目名稱開始設置;二是直接讀取web站點的絕對根路徑,將絕對路徑設置到org.eclipse.jetty.webapp.WebAppContext.setResourceBase方法中。我上面的源碼使用的就是直接指明絕對路徑的方式。
三、啟用Spring-Security組件,過濾鏈卻不生效的問題
3.1、因為我在登錄頁面啟用了crsf功能,需要crsfFilter為登錄請求輸出crsf-token碼。結果站點啟動執行登錄時,發現這個值始終為null。異常日志顯示請求也僅僅是進入了servlet的處理器,未執行Filter代碼。經過一番的跟蹤分析,最終確定 Tomcat模式下請求由org.springframework.web.filter.DelegatingFilterProxy進行攔截。從結果推斷apache應該是直接內置了該過濾功能,不需要手動設置,但是jetty沒有這個實現。
3.2、識別這個問題后,相應的也有兩種處理方案(兩種方案二選一)
- 第一種,在web.xml中顯示指明這個Filter(推薦做法)
<filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
- 第二種,在代碼中org.eclipse.jetty.webapp.WebAppContext中添加這個Filter,示例代碼如下
private void postStart(WebAppContext root){ /**spring內部過濾器代理 里面包含了默認的11個過濾器 這里的初始化參數可以直接些spring的bean名稱*/ FilterHolder filterHolder=new FilterHolder(DelegatingFilterProxy.class); filterHolder.setName("springSecurityFilterChain"); root.addFilter(filterHolder, "/*", EnumSet.of(DispatcherType.REQUEST)); }
小結:
紙上得來終覺淺,絕知此事要躬行。折騰之后好在出了結果,得到了一點安慰。接下來就准備從jetty切入分析Netty的nio工作原理啦!