这段时间本意是想要研究一下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 }
View Code
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 }
View Code
1 package com.train.simulate.web.server;
2
3 public interface IServer {
4 void start();
5 void stop();
6 }
View Code
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工作原理啦!
每天都是崭新的开始 ——Mr.司满(214382122)[]~( ̄▽ ̄)~*
一、内嵌的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 }
View Code
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 }
View Code
1 package com.train.simulate.web.server;
2
3 public interface IServer {
4 void start();
5 void stop();
6 }
View Code
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工作原理啦!