zuul是spring cloud 微服務(wù)體系中的網(wǎng)關(guān)望迎,可以路由請(qǐng)求到具體的服務(wù)障癌,同時(shí)做一些驗(yàn)簽混弥,解密等的與業(yè)務(wù)無(wú)關(guān)的事情晾捏。今天我們從一個(gè)注解@EnableZuulProxy開(kāi)始講述惦辛。這個(gè)注解導(dǎo)入了一個(gè)配置文件ZuulProxyConfiguration胖齐,他繼承了ZuulConfiguration,整個(gè)zuul的流程的定義就在這兩個(gè)類中剿另。
@Configuration
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass(ZuulServlet.class)
// Make sure to get the ServerProperties from the same place as a normal web app would
@Import(ServerPropertiesAutoConfiguration.class)
public class ZuulConfiguration {
@Autowired
protected ZuulProperties zuulProperties;
@Autowired
protected ServerProperties server;
@Autowired(required = false)
private ErrorController errorController;
@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;
}
@Bean
@ConditionalOnMissingBean(name = "zuulServlet")
public ServletRegistrationBean zuulServlet() {
ServletRegistrationBean servlet = new ServletRegistrationBean(new ZuulServlet(),
this.zuulProperties.getServletPattern());
// The whole point of exposing this servlet is to provide a route that doesn't
// buffer requests.
servlet.addInitParameter("buffer-requests", "false");
return servlet;
}
// pre filters
@Bean
public ServletDetectionFilter servletDetectionFilter() {
return new ServletDetectionFilter();
}
@Bean
public FormBodyWrapperFilter formBodyWrapperFilter() {
return new FormBodyWrapperFilter();
}
@Bean
public DebugFilter debugFilter() {
return new DebugFilter();
}
@Bean
public Servlet30WrapperFilter servlet30WrapperFilter() {
return new Servlet30WrapperFilter();
}
// post filters
@Bean
public SendResponseFilter sendResponseFilter() {
return new SendResponseFilter();
}
@Bean
public SendErrorFilter sendErrorFilter() {
return new SendErrorFilter();
}
@Bean
public SendForwardFilter sendForwardFilter() {
return new SendForwardFilter();
}
...
}
zuulProperties 是用戶通過(guò)配置文件編寫的服務(wù)路由等信息阳准,表明請(qǐng)求是哪種模式時(shí)需要跳轉(zhuǎn)到哪個(gè)具體的服務(wù)讼稚,這是主要的內(nèi)容乱灵,當(dāng)然還有一些其他的信息。具體的看一下這個(gè)類的內(nèi)容就可以了蝉稳,都可以配置耘戚,這是我們定制化zuul的一個(gè)基礎(chǔ)配置文件。剩下的zuulController,zuulHandlerMapping长捧,zuulServlet就是spring mvc 的組件了串结,通過(guò)zuulHandlerMapping可以發(fā)現(xiàn)所有的請(qǐng)求都交給了zuulController來(lái)處理,它里面包裝的Servlet就是zuulServlet把敞,由他的service方法來(lái)處理先巴。這個(gè)下面細(xì)說(shuō)摩渺。例外還配置了ZuulFilter的過(guò)濾器横侦,著重看一下pre類型的servletDetectionFilter與post類型的sendResponseFilter枉侧。
@Configuration
public class ZuulProxyConfiguration extends ZuulConfiguration {
@Autowired(required = false)
private TraceRepository traces;
@Autowired
private SpringClientFactory clientFactory;
@Autowired
private DiscoveryClient discovery;
@Autowired
private ServiceRouteMapper serviceRouteMapper;
@Bean
@Override
@ConditionalOnMissingBean(RouteLocator.class)
public DiscoveryClientRouteLocator routeLocator() {
return new DiscoveryClientRouteLocator(this.server.getServletPrefix(),
this.discovery, this.zuulProperties, this.serviceRouteMapper);
}
@Bean
@ConditionalOnMissingBean
public RibbonCommandFactory<?> ribbonCommandFactory() {
return new RestClientRibbonCommandFactory(this.clientFactory);
}
// pre filters
@Bean
public PreDecorationFilter preDecorationFilter(RouteLocator routeLocator,
ProxyRequestHelper proxyRequestHelper) {
return new PreDecorationFilter(routeLocator,
this.server.getServletPrefix(),
this.zuulProperties,
proxyRequestHelper);
}
// route filter
@Bean
public RibbonRoutingFilter ribbonRoutingFilter(ProxyRequestHelper helper,
RibbonCommandFactory<?> ribbonCommandFactory) {
RibbonRoutingFilter filter = new RibbonRoutingFilter(helper,
ribbonCommandFactory);
return filter;
}
@Bean
public SimpleHostRoutingFilter simpleHostRoutingFilter(ProxyRequestHelper helper,
ZuulProperties zuulProperties) {
return new SimpleHostRoutingFilter(helper, zuulProperties);
}
@Bean
public ProxyRequestHelper proxyRequestHelper() { //作為一個(gè)幫助類帜矾,主要用來(lái)設(shè)置值
ProxyRequestHelper helper = new ProxyRequestHelper();
if (this.traces != null) {
helper.setTraces(this.traces);
}
helper.setIgnoredHeaders(this.zuulProperties.getIgnoredHeaders());
helper.setTraceRequestBody(this.zuulProperties.isTraceRequestBody());
return helper;
}
@Bean
@ConditionalOnMissingBean(ServiceRouteMapper.class)
public ServiceRouteMapper serviceRouteMapper() {
return new SimpleServiceRouteMapper();
}
@Configuration
@ConditionalOnClass(Endpoint.class)
protected static class RoutesEndpointConfiguration {
@Bean
public RoutesEndpoint zuulEndpoint(RouteLocator routeLocator) {
return new RoutesEndpoint(routeLocator);
}
}
}
routeLocator是一個(gè)路由的匹配器珍剑,他的實(shí)現(xiàn)是一個(gè)
DiscoveryClientRouteLocator,我這邊使用的是Consul作為服務(wù)注冊(cè)的容器,他會(huì)從consul中拉取各個(gè)服務(wù)的信息饰序,比如我們?cè)賨uul中配置了要路由的serviceId,那么從consul中尋找菌羽,如果有對(duì)應(yīng)的serviceId,那么就是這個(gè)服務(wù)來(lái)處理這個(gè)請(qǐng)求。proxyRequestHelper是一個(gè)幫助類是晨,用來(lái)再context中set值罩缴。還有兩個(gè)route類型zuulFilter,雖然都是Bean,但不一定都啟用檬寂。simpleHostRoutingFilter再設(shè)置了url的情況下使用,ribbonRoutingFilter在設(shè)置了serviceId的情況下使用镣屹,有客戶端負(fù)載均衡的作用女蜈。還有一個(gè)pre類型的zuulFilter
preDecorationFilter,主要是填充一些數(shù)據(jù)惰许。OK汹买,準(zhǔn)備工作做的差不多了生巡,我們來(lái)詳細(xì)看一下流程。請(qǐng)求過(guò)來(lái)了须揣,交給了ZuulServlet進(jìn)行處理疯汁。
public class ZuulServlet extends HttpServlet {
private static final long serialVersionUID = -3374242278843351500L;
private ZuulRunner zuulRunner;
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
String bufferReqsStr = config.getInitParameter("buffer-requests");
boolean bufferReqs = bufferReqsStr != null && bufferReqsStr.equals("true") ? true : false;
zuulRunner = new ZuulRunner(bufferReqs);
}
@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
try {
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
// Marks this request as having passed through the "Zuul engine", as opposed to servlets
// explicitly bound in web.xml, for which requests will not have the same data attached
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
try {
preRoute();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
route();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
postRoute();
} catch (ZuulException e) {
error(e);
return;
}
} catch (Throwable e) {
error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}
/**
* executes "post" ZuulFilters
*
* @throws ZuulException
*/
void postRoute() throws ZuulException {
zuulRunner.postRoute();
}
/**
* executes "route" filters
*
* @throws ZuulException
*/
void route() throws ZuulException {
zuulRunner.route();
}
/**
* executes "pre" filters
*
* @throws ZuulException
*/
void preRoute() throws ZuulException {
zuulRunner.preRoute();
}
/**
* initializes request
*
* @param servletRequest
* @param servletResponse
*/
void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
zuulRunner.init(servletRequest, servletResponse);
}
/**
* sets error context info and executes "error" filters
*
* @param e
*/
void error(ZuulException e) {
RequestContext.getCurrentContext().setThrowable(e);
zuulRunner.error();
}
}
在這個(gè)servlet初始化的時(shí)候會(huì)調(diào)用init方法,里面有個(gè)屬性
buffer-requests用來(lái)判斷是否走spring mvc沫换。不知道大家有沒(méi)有遇到過(guò)這種情況垮兑,上傳文件走網(wǎng)關(guān)時(shí)雀哨,需要在鏈接上加上/zuul,加上就不需要走spring mvc 捌浩,這樣不會(huì)對(duì)數(shù)據(jù)的大小有限制进统,所以可以傳遞大數(shù)據(jù)量的文件。另外初始化了一個(gè)zuulRunner,他所作的事情
第一點(diǎn)是在RequestContext設(shè)置請(qǐng)求按照buffer-requests這個(gè)參數(shù)來(lái)進(jìn)行掉分,如果為真,request被HttpServletRequestWrapper包裝一下 褥民,第二點(diǎn)的作用是處理按照類型處理filter.。
/**
* executes "route" filterType ZuulFilters
*
* @throws ZuulException
*/
public void route() throws ZuulException {
FilterProcessor.getInstance().route();
}
/**
* Runs all "route" filters. These filters route calls to an origin.
*
* @throws ZuulException if an exception occurs.
*/
public void route() throws ZuulException {
try {
runFilters("route");
} catch (Throwable e) {
if (e instanceof ZuulException) {
throw (ZuulException) e;
}
throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_ROUTE_FILTER_" + e.getClass().getName());
}
}
/**
* runs all filters of the filterType sType/ Use this method within filters to run custom filters by type
*
* @param sType the filterType.
* @return
* @throws Throwable throws up an arbitrary exception
*/
public Object runFilters(String sType) throws Throwable {
if (RequestContext.getCurrentContext().debugRouting()) {
Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
}
boolean bResult = false;
List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
if (list != null) {
for (int i = 0; i < list.size(); i++) {
ZuulFilter zuulFilter = list.get(i);
Object result = processZuulFilter(zuulFilter);
if (result != null && result instanceof Boolean) {
bResult |= ((Boolean) result);
}
}
}
return bResult;
}
當(dāng)調(diào)用zuulRunner中的route方法時(shí)惫叛,就會(huì)搜索這個(gè)類型的zuulFilter,依次執(zhí)行妻熊。當(dāng)我們知道了這些,在回頭看一下zuulServlet就特別容易理解了亿胸,就是根據(jù)流程走的先pre,再route,再post,再一直走這些filter啊序仙。流程大抵知道了洋丐。我們?cè)倏匆幌拢瑪?shù)據(jù)真實(shí)來(lái)了迁客,怎么流轉(zhuǎn)。我們就從pre(PreDecorationFilter),route(SimpleHostRoutingFilter),post(SendResponseFilter) 這三個(gè)部分來(lái)看一下卜范。
pre: PreDecorationFilter
public class PreDecorationFilter extends ZuulFilter {
private RouteLocator routeLocator;
private String dispatcherServletPath;
private ZuulProperties properties;
private UrlPathHelper urlPathHelper = new UrlPathHelper();
private ProxyRequestHelper proxyRequestHelper;
public PreDecorationFilter(RouteLocator routeLocator, String dispatcherServletPath,
ZuulProperties properties, ProxyRequestHelper proxyRequestHelper) {
this.routeLocator = routeLocator;
this.properties = properties;
this.urlPathHelper
.setRemoveSemicolonContent(properties.isRemoveSemicolonContent());
this.dispatcherServletPath = dispatcherServletPath;
this.proxyRequestHelper = proxyRequestHelper;
}
...
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
final String requestURI = this.urlPathHelper
.getPathWithinApplication(ctx.getRequest());
Route route = this.routeLocator.getMatchingRoute(requestURI);
if (route != null) {
String location = route.getLocation();
if (location != null) {
ctx.put("requestURI", route.getPath());
ctx.put("proxy", route.getId());
if (!route.isCustomSensitiveHeaders()) {
this.proxyRequestHelper.addIgnoredHeaders(
this.properties.getSensitiveHeaders().toArray(new String[0]));
}
else {
this.proxyRequestHelper.addIgnoredHeaders(
route.getSensitiveHeaders().toArray(new String[0]));
}
if (route.getRetryable() != null) {
ctx.put("retryable", route.getRetryable());
}
if (location.startsWith("http:") || location.startsWith("https:")) {
ctx.setRouteHost(getUrl(location));
ctx.addOriginResponseHeader("X-Zuul-Service", location);
}
else if (location.startsWith("forward:")) {
ctx.set("forward.to", StringUtils.cleanPath(
location.substring("forward:".length()) + route.getPath()));
ctx.setRouteHost(null);
return null;
}
else {
// set serviceId for use in filters.route.RibbonRequest
ctx.set("serviceId", location);
ctx.setRouteHost(null);
ctx.addOriginResponseHeader("X-Zuul-ServiceId", location);
}
if (this.properties.isAddProxyHeaders()) {
ctx.addZuulRequestHeader("X-Forwarded-Host",
ctx.getRequest().getServerName());
ctx.addZuulRequestHeader("X-Forwarded-Port",
String.valueOf(ctx.getRequest().getServerPort()));
ctx.addZuulRequestHeader(ZuulHeaders.X_FORWARDED_PROTO,
ctx.getRequest().getScheme());
if (StringUtils.hasText(route.getPrefix())) {
String existingPrefix = ctx.getRequest()
.getHeader("X-Forwarded-Prefix");
StringBuilder newPrefixBuilder = new StringBuilder();
if (StringUtils.hasLength(existingPrefix)) {
if (existingPrefix.endsWith("/")
&& route.getPrefix().startsWith("/")) {
newPrefixBuilder.append(existingPrefix, 0,
existingPrefix.length() - 1);
}
else {
newPrefixBuilder.append(existingPrefix);
}
}
newPrefixBuilder.append(route.getPrefix());
ctx.addZuulRequestHeader("X-Forwarded-Prefix",
newPrefixBuilder.toString());
}
String xforwardedfor = ctx.getRequest().getHeader("X-Forwarded-For");
String remoteAddr = ctx.getRequest().getRemoteAddr();
if (xforwardedfor == null) {
xforwardedfor = remoteAddr;
}
else if (!xforwardedfor.contains(remoteAddr)) { // Prevent duplicates
xforwardedfor += ", " + remoteAddr;
}
ctx.addZuulRequestHeader("X-Forwarded-For", xforwardedfor);
}
}
}
else {
log.warn("No route found for uri: " + requestURI);
String fallBackUri = requestURI;
String fallbackPrefix = this.dispatcherServletPath; // default fallback
// servlet is
// DispatcherServlet
if (RequestUtils.isZuulServletRequest()) {
// remove the Zuul servletPath from the requestUri
log.debug("zuulServletPath=" + this.properties.getServletPath());
fallBackUri = fallBackUri.replaceFirst(this.properties.getServletPath(),
"");
log.debug("Replaced Zuul servlet path:" + fallBackUri);
}
else {
// remove the DispatcherServlet servletPath from the requestUri
log.debug("dispatcherServletPath=" + this.dispatcherServletPath);
fallBackUri = fallBackUri.replaceFirst(this.dispatcherServletPath, "");
log.debug("Replaced DispatcherServlet servlet path:" + fallBackUri);
}
if (!fallBackUri.startsWith("/")) {
fallBackUri = "/" + fallBackUri;
}
String forwardURI = fallbackPrefix + fallBackUri;
forwardURI = forwardURI.replaceAll("http://", "/");
ctx.set("forward.to", forwardURI);
}
return null;
}
private URL getUrl(String target) {
try {
return new URL(target);
}
catch (MalformedURLException ex) {
throw new IllegalStateException("Target URL is malformed", ex);
}
}
}
這個(gè)pre就是在上下文中set一系列的值冈绊。
route: SimpleHostRoutingFilter
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
MultiValueMap<String, String> headers = this.helper
.buildZuulRequestHeaders(request);
MultiValueMap<String, String> params = this.helper
.buildZuulRequestQueryParams(request);
String verb = getVerb(request);
InputStream requestEntity = getRequestBody(request);
if (request.getContentLength() < 0) {
context.setChunkedRequestBody();
}
String uri = this.helper.buildZuulRequestURI(request);
this.helper.addIgnoredHeaders();
try {
HttpResponse response = forward(this.httpClient, verb, uri, request, headers,
params, requestEntity);
setResponse(response);
setErrorCodeFor4xx(context, response);
}
catch (Exception ex) {
context.set(ERROR_STATUS_CODE,
HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
context.set("error.exception", ex);
}
return null;
}
private void setResponse(HttpResponse response) throws IOException {
this.helper.setResponse(response.getStatusLine().getStatusCode(),
response.getEntity() == null ? null : response.getEntity().getContent(),
revertHeaders(response.getAllHeaders()));
}
利用httpClient調(diào)用服務(wù)拿到數(shù)據(jù),里面有個(gè)setReponse,就是借助helper,還記得我們有個(gè)Helper的Bean,不記得畏线,請(qǐng)看上文寝殴,把response設(shè)置到了上下文中市咽。
post :SendResponseFilter
@Override
public Object run() {
try {
addResponseHeaders();
writeResponse();
}
catch (Exception ex) {
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
private void writeResponse() throws Exception {
RequestContext context = RequestContext.getCurrentContext();
// there is no body to send
if (context.getResponseBody() == null
&& context.getResponseDataStream() == null) {
return;
}
HttpServletResponse servletResponse = context.getResponse();
if (servletResponse.getCharacterEncoding() == null) { // only set if not set
servletResponse.setCharacterEncoding("UTF-8");
}
OutputStream outStream = servletResponse.getOutputStream();
InputStream is = null;
try {
if (RequestContext.getCurrentContext().getResponseBody() != null) {
String body = RequestContext.getCurrentContext().getResponseBody();
writeResponse(
new ByteArrayInputStream(
body.getBytes(servletResponse.getCharacterEncoding())),
outStream);
return;
}
boolean isGzipRequested = false;
final String requestEncoding = context.getRequest()
.getHeader(ZuulHeaders.ACCEPT_ENCODING);
if (requestEncoding != null
&& HTTPRequestUtils.getInstance().isGzipped(requestEncoding)) {
isGzipRequested = true;
}
is = context.getResponseDataStream();
InputStream inputStream = is;
if (is != null) {
if (context.sendZuulResponse()) {
// if origin response is gzipped, and client has not requested gzip,
// decompress stream
// before sending to client
// else, stream gzip directly to client
if (context.getResponseGZipped() && !isGzipRequested) {
// If origin tell it's GZipped but the content is ZERO bytes,
// don't try to uncompress
final Long len = context.getOriginContentLength();
if (len == null || len > 0) {
try {
inputStream = new GZIPInputStream(is);
}
catch (java.util.zip.ZipException ex) {
log.debug(
"gzip expected but not "
+ "received assuming unencoded response "
+ RequestContext.getCurrentContext()
.getRequest().getRequestURL()
.toString());
inputStream = is;
}
}
else {
// Already done : inputStream = is;
}
}
else if (context.getResponseGZipped() && isGzipRequested) {
servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING, "gzip");
}
writeResponse(inputStream, outStream);
}
}
}
finally {
try {
if (is != null) {
is.close();
}
outStream.flush();
// The container will close the stream for us
}
catch (IOException ex) {
}
}
}
private void writeResponse(InputStream zin, OutputStream out) throws Exception {
byte[] bytes = new byte[INITIAL_STREAM_BUFFER_SIZE.get()];
int bytesRead = -1;
while ((bytesRead = zin.read(bytes)) != -1) {
try {
out.write(bytes, 0, bytesRead);
out.flush();
}
catch (IOException ex) {
// ignore
}
// doubles buffer size if previous read filled it
if (bytesRead == bytes.length) {
bytes = new byte[bytes.length * 2];
}
}
}
private void addResponseHeaders() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletResponse servletResponse = context.getResponse();
List<Pair<String, String>> zuulResponseHeaders = context.getZuulResponseHeaders();
@SuppressWarnings("unchecked")
List<String> rd = (List<String>) RequestContext.getCurrentContext()
.get("routingDebug");
if (rd != null) {
StringBuilder debugHeader = new StringBuilder();
for (String it : rd) {
debugHeader.append("[[[" + it + "]]]");
}
if (INCLUDE_DEBUG_HEADER.get()) {
servletResponse.addHeader("X-Zuul-Debug-Header", debugHeader.toString());
}
}
if (zuulResponseHeaders != null) {
for (Pair<String, String> it : zuulResponseHeaders) {
servletResponse.addHeader(it.first(), it.second());
}
}
RequestContext ctx = RequestContext.getCurrentContext();
Long contentLength = ctx.getOriginContentLength();
// Only inserts Content-Length if origin provides it and origin response is not
// gzipped
if (SET_CONTENT_LENGTH.get()) {
if (contentLength != null && !ctx.getResponseGZipped()) {
servletResponse.setContentLength(contentLength.intValue());
}
}
}
重點(diǎn)看writeResponse,從上下文中找到response,將數(shù)據(jù)利用輸出流寫出去就行了押蚤。這個(gè)流程就結(jié)束了