使用servlet3.0非同步特性改造spring-cloud-zuul
我們知道spring-cloud-zuul是依賴springMVC來註冊路由的,而springMVC又是在建立在servlet之上的(這裡微服務專家楊波老師寫過一篇文章講述其網路模型,可以參考看看),在servlet3.0之前使用的是thread per connection方式處理請求,就是每一個請求需要servlet容器為其分配一個執行緒來處理,直到響應完使用者請求,才被釋放回容器執行緒池,如果後端業務處理比較耗時,那麼這個執行緒將會被一直阻塞,不能幹其他事情,如果耗時請求比較多時,servlet容器執行緒將被耗盡,也就無法處理新的請求了,所以Netflix還專門開發了一個熔斷的元件Hystrix
我們先來建立一個zuul的maven專案,就叫async-zuul
吧,具體程式碼我放在github上了。專案依賴於consul做註冊中心,啟動時先要在本地啟動consul,為了能看到效果我們先來新建一個zuul的filter類:
@Component public class TestFilter extends ZuulFilter { //忽略無關程式碼,具體看github上的原始碼 @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); System.out.println("==============執行緒名稱:" + Thread.currentThread().getName() + ",訪問url:" + request.getRequestURI() + "================"); return null; } }
主要就是列印下執行緒的名稱,這個filter是zuul的前置過濾器,我們主要就是看下在zuul在執行路由時是由什麼執行緒執行的。好了我們來啟動下main方法,不過我們還需要一個後端服務,很簡單,建立一個springcloud專案名叫book
即可,並提供一個url:/book/borrow
,啟動後把服務註冊到consul上,成功後我們通過zuul的代理來訪問下book服務:
http://localhost:8080/book/book/borrow
輸出:
==========執行緒名稱:http-nio-8080-exec-10,訪問url:/book/book/borrow=======
很清楚的看到執行filter的執行緒是servlet容器執行緒,等下我們改造成非同步後再做一下對比。
還記得在文章spring-cloud-zuul原理解析(一)中我們分析到,spring-cloud-zuul的路由對映使用到springMVC的兩大元件ZuulHandlerMapping
和ZuulController
,目前肯定是無法支援非同步servlet的。那麼這兩個類在哪裡被載入的呢?答案就是ZuulServerAutoConfiguration
,此類是spring-cloud-zuul自動配置類,原始碼如下:
@Configuration
@ConditionalOnBean(annotation=EnableZuulProxy.class)
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass(ZuulServlet.class)
@Import(ServerPropertiesAutoConfiguration.class)
public class ZuulServerAutoConfiguration {
//無關程式碼省略..........
@Bean
public ZuulController zuulController() {
return new ZuulController();
}
@Bean
public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) {
ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController());
mapping.setErrorController(this.errorController);
return mapping;
}
//無關程式碼省略..........
}
可以看到這兩個類在spring-cloud-zuul中並沒有為我們提供擴充套件,沒法替換它們來實現servlet的非同步邏輯,那該怎麼辦呢?spring-cloud-zuul還有一個自動配置配ZuulProxyAutoConfiguration
繼承自ZuulServerAutoConfiguration
,我們把這兩個配置類全部替換掉,換成我們自己的不就可以了麼?是的,不過首先我們得先排除載入這兩個自動配置類,springboot為我們提供這樣的設定:
@EnableZuulProxy
//排除ZuulProxyAutoConfiguration配置類
@SpringBootApplication(exclude=ZuulProxyAutoConfiguration.class)
public class Startup {
public static void main(String[] args) {
SpringApplication.run(Startup.class, args);
}
}
之後,我們建立兩個自己的配置配,完全拷貝ZuulServerAutoConfiguration
和ZuulProxyAutoConfiguration
這兩個類,不過光這兩個類還是不行,這兩個類使用到了類RibbonCommandFactoryConfiguration
,裡面的內部類是protected
的,我們沒法使用,也得自己建立,也是拷貝自RibbonCommandFactoryConfiguration
,然後我們還需修改ZuulController
的邏輯改成非同步方式,所以再新建一個類繼承ZuulController,這樣我們就新建了自己的三個配置類和一個自己的
ZuulController`類,如下:
public class MyZuulController extends ZuulController{
private final AsyncTaskExecutor asyncTaskExecutor;
public MyZuulController(AsyncTaskExecutor asyncTaskExecutor) {
super();
this.asyncTaskExecutor = asyncTaskExecutor;
}
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
//真正的非同步化邏輯
final AsyncContext asyncCtx = request.startAsync();
this.asyncTaskExecutor.execute(new Runnable() {
@Override
public void run() {
try {
MyZuulController.this.handleRequestInternal((HttpServletRequest)asyncCtx.getRequest(),
(HttpServletResponse)asyncCtx.getResponse());
}catch (Exception e) {
e.printStackTrace();
}finally {
asyncCtx.complete();
RequestContext.getCurrentContext().unset();
}
}
});
return null;
}
}
@Configuration
@ConditionalOnBean(annotation=EnableZuulProxy.class)
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass(ZuulServlet.class)
@Import(ServerPropertiesAutoConfiguration.class)
public class MyZuulServerAutoConfiguration {
//省略程式碼,完全拷貝自ZuulServerAutoConfiguration
/**
* 自定義執行緒池
* @return
*/
@Bean
public AsyncTaskExecutor zuulAsyncPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("zuul-async-");
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(50);
return executor;
}
//這裡換成我們自己的MyZuulController類,並且傳入一個我們自定義的執行緒池
@Bean
public ZuulController zuulController(AsyncTaskExecutor asyncTaskExecutor) {
return new MyZuulController(asyncTaskExecutor);
}
@Bean
public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes,AsyncTaskExecutor asyncTaskExecutor) {
ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController(asyncTaskExecutor));
mapping.setErrorController(this.errorController);
return mapping;
}
}
@Configuration
@Import({ MyRibbonCommandFactoryConfiguration.RestClientRibbonConfiguration.class,
MyRibbonCommandFactoryConfiguration.OkHttpRibbonConfiguration.class,
MyRibbonCommandFactoryConfiguration.HttpClientRibbonConfiguration.class })
@ConditionalOnBean(annotation=EnableZuulProxy.class)
public class MyZuulProxyAutoConfiguration extends MyZuulServerAutoConfiguration {
//省略程式碼,完全拷貝自ZuulProxyAutoConfiguration
}
public class MyRibbonCommandFactoryConfiguration {
//省略程式碼,完全拷貝自RibbonCommandFactoryConfiguration
}
這裡我們稍作了一點修改
@ConditionalOnBean(ZuulProxyMarkerConfiguration.Marker.class)
//替換成:
@ConditionalOnBean(annotation=EnableZuulProxy.class)
這樣做的目的主要是配合註解@EnableZuulProxy
使用,只有開啟了此註解才載入配置類。我們還替換ZuulController
成我們自定義的MyZuulController
了,這裡是非同步化的主要邏輯,其實也非常簡單,就是使用了serv3.0為我們提供的api來開啟非同步化。萬事已經具備啦,我們再次啟動zuul,訪問上面的url,輸出:
==========執行緒名稱:zuul-async-1,訪問url:/book/book/borrow==========
哈哈,執行filter的執行緒變成我們自定義的執行緒名稱了,達到了我們的需求,servlet已經變成非同步的了。
這是我對spring-cloud-zuul實現非同步servlet的想法,記錄下來,可能不是最好的實現方式,如果您有更好的方法歡迎留言給我一起探討下!