本文摘自于 《Spring Cloud微服务 入门 实战与进阶》 一书。
1. /routes 端点
当@EnableZuulProxy与Spring Boot Actuator配合使用时,Zuul会暴露一个路由管理端点/routes。
借助这个端点,可以方便、直观地查看以及管理Zuul的路由。
将所有端点都暴露出来,增加下面的配置:
management.endpoints.web.exposure.include=*
访问 http://localhost:2103/actuator/routes 可以显示所有路由信息:
{
"/cxytiandi/**"
:
"http://cxytiandi.com"
,
"/hystrix-api/**"
:
"hystrix-feign-demo"
,
"/api/**"
:
"forward:/local"
,
"/hystrix-feign-demo/**"
:
"hystrix-feign-demo"
}
2. /filters 端点
/fliters端点会返回Zuul中所有过滤器的信息。可以清楚的了解Zuul中目前有哪些过滤器,哪些被禁用了等详细信息。
访问 http://localhost:2103/actuator/filters 可以显示所有过滤器信息:
{
"error"
:
[
{
"class"
:
"com.cxytiandi.zuul_demo.filter.ErrorFilter"
,
"order"
:
100
,
"disabled"
:
false
,
"static"
:
true
}
],
"post"
:
[
{
"class"
:
"org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter"
,
"order"
:
1000
,
"disabled"
:
false
,
"static"
:
true
}
],
"pre"
:
[
{
"class"
:
"com.cxytiandi.zuul_demo.filter.IpFilter"
,
"order"
:
1
,
"disabled"
:
false
,
"static"
:
true
}
],
"route"
:
[
{
"class"
:
"org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter"
,
"order"
:
10
,
"disabled"
:
false
,
"static"
:
true
}
]
}
3. 文件上传
创建一个新的Maven项目zuul-file-demo,编写一个文件上传的接口,如代码清单7-20所示。
代码清单 7-20 文件上传接口
@RestController
public
class
FileController
{
@PostMapping
(
"/file/upload"
)
public
String
fileUpload
(
@RequestParam
(
value
=
"file"
)
MultipartFile
file
)
throws
IOException
{
byte
[]
bytes
=
file
.
getBytes
();
File
fileToSave
=
new
File
(
file
.
getOriginalFilename
());
FileCopyUtils
.
copy
(
bytes
,
fileToSave
);
return
fileToSave
.
getAbsolutePath
();
}
}
将服务注册到Eureka中,服务名称为zuul-file-demo,通过PostMan来上传文件,如图7-4所示
可以看到接口正常返回了文件上传之后的路径,接下来我们换一个大一点的文件,文件大小为1.7MB。
可以看到报错了(如图7-5所示),通过Zuul上传文件,如果超过1M需要配置上传文件的大小, Zuul和上传的服务都要加上配置:
spring.servlet.multipart.max-file-size=1000Mb
spring.servlet.multipart.max-request-size=1000Mb
配置加完后重新上传就可以成功了,如图7-6所示。
第二种解决办法是在网关的请求地址前面加上/zuul,就可以绕过Spring DispatcherServlet进行上传大文件。
# 正常的地址
http
:
//localhost:2103/zuul-file-demo/file/upload
# 绕过的地址
http
:
//localhost:2103/zuul/zuul-file-demo/file/upload
通过加上/zuul前缀可以让Zuul服务不用配置文件上传大小,但是接收文件的服务还是需要配置文件上传大小,否则文件还是会上传失败。
在上传大文件的时候,时间比较会比较长,这个时候需要设置合理的超时时间来避免超时。
ribbon.ConnectTimeout=3000
ribbon.ReadTimeout=60000
在Hystrix隔离模式为线程下zuul.ribbon-isolation-strategy=thread,需要设置Hystrix超时时间。
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=60000
4. 请求响应信息输出
系统在生产环境出现问题时,排查问题最好的方式就是查看日志了,日志的记录尽量详细,这样你才能快速定位问题。
下面带大家学习如何在Zuul中输出请求响应的信息来辅助我们解决一些问题。
熟悉Zuul的朋友都知道,Zuul中有4种类型过滤器,每种都有特定的使用场景,要想记录响应数据,那么必须是在请求路由到了具体的服务之后,返回了才有数据,这种需求就适合用post过滤器来实现了。如代码清单7-21所示。
代码清单 7-21 Zull获取请求信息
HttpServletRequest
req
=
(
HttpServletRequest
)
RequestContext
.
getCurrentContext
().
getRequest
();
System
.
err
.
println
(
"REQUEST:: "
+
req
.
getScheme
()
+
" "
+
req
.
getRemoteAddr
()
+
":"
+
req
.
getRemotePort
());
StringBuilder
params
=
new
StringBuilder
(
"?"
);
// 获取URL参数
Enumeration
<
String
>
names
=
req
.
getParameterNames
();
if
(
req
.
getMethod
().
equals
(
"GET"
)
)
{
while
(
names
.
hasMoreElements
())
{
String
name
=
(
String
)
names
.
nextElement
();
params
.
append
(
name
);
params
.
append
(
"="
);
params
.
append
(
req
.
getParameter
(
name
));
params
.
append
(
"&"
);
}
}
if
(
params
.
length
()
>
0
)
{
params
.
delete
(
params
.
length
()-
1
,
params
.
length
());
}
System
.
err
.
println
(
"REQUEST:: > "
+
req
.
getMethod
()
+
" "
+
req
.
getRequestURI
()
+
params
+
" "
+
req
.
getProtocol
());
Enumeration
<
String
>
headers
=
req
.
getHeaderNames
();
while
(
headers
.
hasMoreElements
())
{
String
name
=
(
String
)
headers
.
nextElement
();
String
value
=
req
.
getHeader
(
name
);
System
.
err
.
println
(
"REQUEST:: > "
+
name
+
":"
+
value
);
}
final
RequestContext
ctx
=
RequestContext
.
getCurrentContext
();
// 获取请求体参数
if
(!
ctx
.
isChunkedRequestBody
())
{
ServletInputStream
inp
=
null
;
try
{
inp
=
ctx
.
getRequest
().
getInputStream
();
String
body
=
null
;
if
(
inp
!=
null
)
{
body
=
IOUtils
.
toString
(
inp
);
System
.
err
.
println
(
"REQUEST:: > "
+
body
);
}
catch
(
IOException
e
)
{
e
.
printStackTrace
();
}
}
}
输出效果如下:
获取响应内容第一种方式,如代码清单7-22所示。
代码清单 7-22 获取响应内容(一)
try
{
Object
zuulResponse
=
RequestContext
.
getCurrentContext
().
get
(
"zuulResponse"
);
if
(
zuulResponse
!=
null
)
{
RibbonHttpResponse
resp
=
(
RibbonHttpResponse
)
zuulResponse
;
String
body
=
IOUtils
.
toString
(
resp
.
getBody
());
System
.
err
.
println
(
"RESPONSE:: > "
+
body
);
resp
.
close
();
RequestContext
.
getCurrentContext
().
setResponseBody
(
body
);
}
}
catch
(
IOException
e
)
{
e
.
printStackTrace
();
}
获取响应内容第二种方式,如代码清单7-23所示。
代码清单 7-23 获取响应内容(二)
InputStream
stream
=
RequestContext
.
getCurrentContext
().
getResponseDataStream
();
try
{
if
(
stream
!=
null
)
{
String
body
=
IOUtils
.
toString
(
stream
);
System
.
err
.
println
(
"RESPONSE:: > "
+
body
);
RequestContext
.
getCurrentContext
().
setResponseBody
(
body
);
}
}
catch
(
IOException
e
)
{
e
.
printStackTrace
();
}
为什么上面两种方式可以取到响应内容?
在RibbonRoutingFilter或者SimpleHostRoutingFilter中可以看到下面一段代码,如代码清单7-24所示。
代码清单 7-24 响应内容获取源码
public
Object
run
()
{
RequestContext
context
=
RequestContext
.
getCurrentContext
();
this
.
helper
.
addIgnoredHeaders
();
try
{
RibbonCommandContext
commandContext
=
buildCommandContext
(
context
);
ClientHttpResponse
response
=
forward
(
commandContext
);
setResponse
(
response
);
return
response
;
}
catch
(
ZuulException
ex
)
{
throw
new
ZuulRuntimeException
(
ex
);
}
catch
(
Exception
ex
)
{
throw
new
ZuulRuntimeException
(
ex
);
}
}
forward()方法对服务调用,拿到响应结果,通过setResponse()方法进行响应的设置,如代码清单7-25所示。
代码清单 7-25 setResponse(一)
protected
void
setResponse
(
ClientHttpResponse
resp
)
throws
ClientException
,
IOException
{
RequestContext
.
getCurrentContext
().
set
(
"zuulResponse"
,
resp
);
this
.
helper
.
setResponse
(
resp
.
getStatusCode
().
value
(),
resp
.
getBody
()
==
null
?
null
:
resp
.
getBody
(),
resp
.
getHeaders
());
}
上面第一行代码就可以解释我们的第一种获取的方法,这边直接把响应内容加到了RequestContext中。
第二种方式的解释就在helper.setResponse的逻辑里面了,如代码清单7-26所示。
代码清单 7-26 setResponse(二)
public
void
setResponse
(
int
status
,
InputStream
entity
,
MultiValueMap
<
String
,
String
>
headers
)
throws
IOException
{
RequestContext
context
=
RequestContext
.
getCurrentContext
();
context
.
setResponseStatusCode
(
status
);
if
(
entity
!=
null
)
{
context
.
setResponseDataStream
(
entity
);
}
// .....
}
- Zuul自带的Debug功能
Zuul中自带了一个DebugFilter,一开始我也没明白这个DebugFilter有什么用,看名称很容易理解,用来调试的,可是你看它源码几乎没什么逻辑,就set了两个值而已,如代码清单7-27所示。
代码清单 7-27 DebugFilter run方法
@Override
public
Object
run
()
{
RequestContext
ctx
=
RequestContext
.
getCurrentContext
();
ctx
.
setDebugRouting
(
true
);
ctx
.
setDebugRequest
(
true
);
return
null
;
}
要想让这个过滤器执行就得研究下它的shouldFilter()方法,如代码清单7-28所示。
代码清单 7-28 DebugFilter shouldFilter 方法
@Override
public
boolean
shouldFilter
()
{
HttpServletRequest
request
=
RequestContext
.
getCurrentContext
().
getRequest
();
if
(
"true"
.
equals
(
request
.
getParameter
(
DEBUG_PARAMETER
.
get
())))
{
return
true
;
}
return
ROUTING_DEBUG
.
get
();
}
只要满足两个条件中的任何一个就可以开启这个过滤器,第一个条件是请求参数中带了某个参数=true就可以开启,这个参数名是通过下面的代码获取的,如代码清单7-29所示。
代码清单 7-29 DebugFilter启用参数(一)
private
static
final
DynamicStringProperty
DEBUG_PARAMETER
=
DynamicPropertyFactory
.
getInstance
().
getStringProperty
(
ZuulConstants
.
ZUUL_DEBUG_PARAMETER
,
"debug"
);
DynamicStringProperty是Netflix的配置管理框架Archaius提供的API,可以从配置中心获取配置,由于Netflix没有开源Archaius的服务端,所以这边用的就是默认值debug,如果大家想动态去获取这个值的话可以用携程开源的Apollo来对接Archaius,这个在后面章节给大家讲解。
可以在请求地址后面追加debug=true来开启这个过滤器,参数名称debug也可以在配置文件中进行覆盖,用zuul.debug.parameter指定,否则就是从Archaius中获取,没有对接Archaius那就是默认值debug。
第二个条件代码,如代码清单7-30所示。
代码清单 7-30 DebugFilter启用参数(二)
private
static
final
DynamicBooleanProperty
ROUTING_DEBUG
=
DynamicPropertyFactory
.
getInstance
().
getBooleanProperty
(
ZuulConstants
.
ZUUL_DEBUG_REQUEST
,
false
);
是通过配置zuul.debug.request来决定的,可以在配置文件中配置zuul.debug.request=true开启DebugFilter过滤器。
DebugFilter过滤器开启后,并没什么效果,在run方法中只是设置了DebugRouting和DebugRequest两个值为true,于是继续看源码,发现在很多地方有这么一段代码,比如com.netflix.zuul.FilterProcessor.runFilters(String)中,如代码清单7-31所示。
代码清单 7-31 Debug信息添加
if
(
RequestContext
.
getCurrentContext
().
debugRouting
())
{
Debug
.
addRoutingDebug
(
"Invoking {"
+
sType
+
"} type filters"
);
}
当debugRouting为true的时候就会添加一些Debug信息到RequestContext中。现在明白了DebugFilter中为什么要设置DebugRouting和DebugRequest两个值为true。
到这步后发现还是很迷茫,一般我们调试信息的话肯定是用日志输出来的,日志级别就是Debug,但这个Debug信息只是累加起来存储到RequestContext中,没有对使用者展示。
继续看代码吧,功夫不负有心人,在org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter.addResponseHeaders()这段代码中看到了希望。如代码清单7-32所示。
代码清单 7-32 Debug信息设置响应
private
void
addResponseHeaders
()
{
RequestContext
context
=
RequestContext
.
getCurrentContext
();
HttpServletResponse
servletResponse
=
context
.
getResponse
();
if
(
this
.
zuulProperties
.
isIncludeDebugHeader
())
{
@SuppressWarnings
(
"unchecked"
)
List
<
String
>
rd
=
(
List
<
String
>)
context
.
get
(
ROUTING_DEBUG_KEY
);
if
(
rd
!=
null
)
{
StringBuilder
debugHeader
=
new
StringBuilder
();
for
(
String
it
:
rd
)
{
debugHeader
.
append
(
"[[["
+
it
+
"]]]"
);
}
servletResponse
.
addHeader
(
X_ZUUL_DEBUG_HEADER
,
debugHeader
.
toString
());
}
}
}
核心代码在于this.zuulProperties.isIncludeDebugHeader(),只有满足这个条件才会把RequestContext中的调试信息作为响应头输出,在配置文件中增加下面的配置即可:
zuul
.
include
-
debug
-
header
=
true
最后在请求的响应头中可以看到调试内容,如图7-7所示。
本文摘自于 《Spring Cloud微服务 入门 实战与进阶》 一书。
尹吉欢