源码级的跟踪和探讨
本篇文章记录的是笔者在学习Zuul时对四大Filters的疑惑及求解过程。文章结尾有总结结论,可以直接看结论(所有内容均来自于源码跟踪后总结)。
四大过滤器类型及生命周期
Zuul的过滤器类型有四种:pre、route、post和error。
其中pre、route、post是正常的请求响应时必走的三大流程。并且,在整个请求响应过程中如果出现了异常没有被及时处理,那么异常被抛到了Zuul后,会由error类型过滤器进行处理。
此处就有了第一个分歧点。
网上很多课程or资料都说的是,post过滤器是负责将响应返回给客户端的,因此在error类型过滤器处理完后,会继续交给post过滤器进行处理并返回最终结果给客户端。这个观点,只说对了一半。
事实上,根据源码,在pre、route类型的过滤器执行过程中若出现了异常,确实是会先经由error类型过滤器处理,然后再交给post过滤器处理。
但若是post类型过滤器执行过程中出现了异常,那么就只会由error类型过滤器进行后续处理,并直接返回响应内容给客户端,不会再次经过post类型过滤器(想想也觉得合理,post都抛异常了,再经过post不是又要抛异常吗?意义何在)。
整个生命周期的图示如下(重点):
对于该生命周期结论的获取,由ZuulServlet.service()中的源码所得:
public class ZuulServlet extends HttpServlet {
private static final long serialVersionUID = -3374242278843351500L;
private ZuulRunner zuulRunner;
public ZuulServlet() {
}
public void init(ServletConfig config) throws ServletException {
super.init(config);
String bufferReqsStr = config.getInitParameter("buffer-requests");
boolean bufferReqs = bufferReqsStr != null && bufferReqsStr.equals("true");
this.zuulRunner = new ZuulRunner(bufferReqs);
}
// !!!重点在这里!!!
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
try {
this.init((HttpServletRequest)servletRequest, (HttpServletResponse)servletResponse);
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
// 执行 pre过滤器,如果抛异常,则在catch中执行 error过滤器 和 post过滤器
try {
this.preRoute();
} catch (ZuulException var13) {
this.error(var13);
this.postRoute();
return;
}
// 执行 route过滤器,如果抛异常,则在catch中执行 error过滤器 和 post过滤器
try {
this.route();
} catch (ZuulException var12) {
this.error(var12);
this.postRoute();
return;
}
// 执行 post过滤器,如果抛异常,则在catch中执行 error过滤器
// 注意!此处的catch中并没有再次执行 post过滤器!
try {
this.postRoute();
} catch (ZuulException var11) {
this.error(var11);
}
} catch (Throwable var14) {
this.error(new ZuulException(var14, 500, "UNHANDLED_EXCEPTION_" + var14.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}
void postRoute() throws ZuulException {
this.zuulRunner.postRoute();
}
void route() throws ZuulException {
this.zuulRunner.route();
}
void preRoute() throws ZuulException {
this.zuulRunner.preRoute();
}
void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
this.zuulRunner.init(servletRequest, servletResponse);
}
void error(ZuulException e) {
RequestContext.getCurrentContext().setThrowable(e);
this.zuulRunner.error();
}
}
通过上述的源码,我们可以看到,Zuul本质上也是个Servlet,在重写Service()中实现了对请求的相关处理。
其中,通过三个try-catch分别对pre、route和post类型的过滤器进行调用,在调用过程中若出现了异常,则由catch中显式调用的过滤器(如post、error)做相应的后续处理。其中,post过滤器执行过程中若抛异常,则只会由error过滤器进行处理,这点与pre过滤器、route过滤器不同。
同一类型的过滤器的执行细节
对于同一类型的过滤器,如多个pre类型的过滤器,通过上述代码我们也可以看出,Zuul会先执行完所有的pre类型过滤器,然后再执行route类型过滤器,以此类推。
在同类型的过滤器链中,根据过滤器中的:
public abstract int filterOrder();
该方法的返回值来决定执行顺序,数字越小,优先级越高,越先执行。
那么,疑问来了。对于同一类型的过滤器形成的过滤器链,当其中某一个过滤器抛异常了,剩余的过滤器还会接着执行吗?
还是根据源码来分析,此处我们以pre类型过滤器链为例:
- 先从ZuulServlet.service()中的preRoute()进入
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
try {
this.init((HttpServletRequest)servletRequest, (HttpServletResponse)servletResponse);
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
try {
// 第1步
this.preRoute();
} catch (ZuulException var13) {
this.error(var13);
this.postRoute();
return;
}
// 其余无关源码已删除省略
......
}
// 调用了zuulRunner
void preRoute() throws ZuulException {
this.zuulRunner.preRoute();
}
- 调用(Zuul运行器)ZuulRunner.preRoute(),该方法源码如下:
public void preRoute() throws ZuulException {
FilterProcessor.getInstance().preRoute();
}
- 调用(过滤器执行器)FilterProcessor.preRoute(),该方法源码如下:
public void preRoute() throws ZuulException {
try {
// 重点看这一个方法
this.runFilters("pre");
} catch (ZuulException var2) {
throw var2;
} catch (Throwable var3) {
throw new ZuulException(var3, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + var3.getClass().getName());
}
}
继续调用该类中的runFilters(),传入的参数为"pre",代表本次执行的pre类型的过滤器:
public Object runFilters(String sType) throws Throwable {
if (RequestContext.getCurrentContext().debugRouting()) {
Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
}
boolean bResult = false;
// 根据传入的参数值,利用过滤器加载器 FilterLoader 获取指定类型("pre")的所有过滤器
List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
// 对获取的过滤器列表进行循环遍历
if (list != null) {
for(int i = 0; i < list.size(); ++i) {
ZuulFilter zuulFilter = (ZuulFilter)list.get(i);
// 执行列表中的每一个过滤器
Object result = this.processZuulFilter(zuulFilter);
if (result != null && result instanceof Boolean) {
bResult |= (Boolean)result;
}
}
}
return bResult;
}
- FilterProcessor.processZuulFilter()执行列表中的每一个过滤器
public Object processZuulFilter(ZuulFilter filter) throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
boolean bDebug = ctx.debugRouting();
String metricPrefix = "zuul.filter-";
long execTime = 0L;
String filterName = "";
try {
long ltime = System.currentTimeMillis();
filterName = filter.getClass().getSimpleName();
RequestContext copy = null;
Object o = null;
Throwable t = null;
if (bDebug) {
Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName);
copy = ctx.copy();
}
// 调用具体的Filter的runFilter(),并获取结果
ZuulFilterResult result = filter.runFilter();
// 获取此次结果的状态,有 FAILED 和 SUCCESS两种
ExecutionStatus s = result.getStatus();
execTime = System.currentTimeMillis() - ltime;
switch (s) {
case FAILED:
// 过滤器执行有问题,抛出异常,将异常赋予变量t
t = result.getException();
ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
break;
case SUCCESS:
o = result.getResult();
ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime);
if (bDebug) {
Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms");
Debug.compareContextState(filterName, copy);
}
}
// 当变量t 确实保存了异常对象时,将其抛出给调用者
if (t != null) {
throw t;
} else {
this.usageNotifier.notify(filter, s);
return o;
}
} catch (Throwable var15) {
if (bDebug) {
Debug.addRoutingDebug("Running Filter failed " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + var15.getMessage());
}
this.usageNotifier.notify(filter, ExecutionStatus.FAILED);
if (var15 instanceof ZuulException) {
throw (ZuulException)var15;
} else {
ZuulException ex = new ZuulException(var15, "Filter threw Exception", 500, filter.filterType() + ":" + filterName);
ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
throw ex;
}
}
}
- 具体的Filter的公共runFilter()如下:
该方法属于ZuulFilter,而所有的Filter都需要继承ZuulFilter,因此这是所有Filter的通用流程。
public ZuulFilterResult runFilter() {
ZuulFilterResult zr = new ZuulFilterResult();
if (!this.isFilterDisabled()) {
// 该过滤器需要执行时的逻辑如下
if (this.shouldFilter()) {
Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName());
try {
// 调用具体的过滤器重写的run(),并获取结果
Object res = this.run();
// 如果run()正常执行,那么说明过滤器执行没有问题,返回一个 SUCCESS状态的结果
zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS);
} catch (Throwable var7) {
t.setName("ZUUL::" + this.getClass().getSimpleName() + " failed");
// 如果run()执行过程中抛出了异常,那么返回一个FAILED状态的结果
zr = new ZuulFilterResult(ExecutionStatus.FAILED);
// 同时将异常封装进结果中
zr.setException(var7);
} finally {
t.stopAndLog();
}
} else {
// 该过滤器不需要执行时,直接返回一个 标记为 SKIPPED状态的结果
zr = new ZuulFilterResult(ExecutionStatus.SKIPPED);
}
}
return zr;
}
通过以上5步的源码,我们可以看出,从最初的ZuulServlet.service()开始不断调用,最后到了具体的Filter.runFilter(),这就是每一个过滤器的详细调用流程。
那么,再次回到我们的问题,通过第3步的源码我们已经知道,Zuul会先获取同一类型的所有过滤器形成过滤器列表,并对列表进行循环遍历调用。 那么,如果同一类型的某个Filter运行出现异常,那么同类型的其他过滤器是否还会遍历执行?
答案是:
当出现异常后,同一类型的剩余过滤器不会被遍历执行,该类型的过滤器调用链会被中断,直接返回到ZuulServlet.service()中,由各个类型的catch{}进行后续的异常处理。
根据源码做如下解释:
- 如果在ZuulFilter.runFilter()中调用run()出了异常,那么该异常会被catch,在catch中会封装一个状态为FAILED的结果,并将异常封装在结果中;
- 第1步的调用结果会返回到FilterProcessor.processZuulFilter()中,FilterProcessor获取到一个状态为FAILED的结果,在switch中符合第一种情况(case FAILED),并做如下处理:
switch (s) {
case FAILED:
// 过滤器执行有问题,将异常赋予变量t
t = result.getException();
ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
break;
case SUCCESS:
o = result.getResult();
ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime);
if (bDebug) {
Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms");
Debug.compareContextState(filterName, copy);
}
}
并在switch代码块之后,对变量t进行空值判断,若有值,代表获取到了异常,说明本次调用失败,所以继续将该异常抛出给调用者。
// 当变量t 确实保存了异常对象时,将其抛出给调用者
if (t != null) {
throw t;
} else {
this.usageNotifier.notify(filter, s);
return o;
}
- 于是调用栈回到了FilterProcessor.runFilters()的循环遍历位置:
if (list != null) {
for(int i = 0; i < list.size(); ++i) {
ZuulFilter zuulFilter = (ZuulFilter)list.get(i);
// 调用栈回到了此处,收到了个异常
Object result = this.processZuulFilter(zuulFilter);
if (result != null && result instanceof Boolean) {
bResult |= (Boolean)result;
}
}
}
- 由于该方法runFilters()并没有做try-catch处理,因此异常会导致该方法的执行被迫中断,因此对list的遍历也就中断了。 所以到此,我们的结论也就被源码给实锤了。
- 接着FilterProcessor.preRoute()虽然捕获了runFilters()的异常,但也在catch中对它进行再次抛出,一直往上回调到ZuulServlet.service()中,被catch给捕获并进行处理:
try {
// 抛出异常
this.preRoute();
} catch (ZuulException var13) {
// 捕获异常,交给error过滤器链和post过滤器链处理
this.error(var13);
this.postRoute();
// 并在处理结束后返回
return;
}
如果error类型过滤器抛异常了,谁兜底呢?
答案是:没有人兜底。因为FilterProcessor在调用error类型过滤器时会自己把异常给消化掉,不再向上抛出给调用者。
从刚刚的第二大结论的探讨中,我们发现ZuulServlet在调用过滤器的时候,会先经过FilterProcessor,由它根据不同的过滤器类型去获取不同的过滤器列表并调用执行(this.runFilters(过滤器类型))。
在FilterProcessor中的相关源码如下:
public class FilterProcessor {
static FilterProcessor INSTANCE = new FilterProcessor();
protected static final Logger logger = LoggerFactory.getLogger(FilterProcessor.class);
private FilterUsageNotifier usageNotifier = new BasicFilterUsageNotifier();
// 省略部分无关方法代码
.......
public void postRoute() throws ZuulException {
try {
this.runFilters("post");
} catch (ZuulException var2) {
throw var2;
} catch (Throwable var3) {
throw new ZuulException(var3, 500, "UNCAUGHT_EXCEPTION_IN_POST_FILTER_" + var3.getClass().getName());
}
}
public void error() {
try {
this.runFilters("error");
} catch (Throwable var2) {
// 对error类型过滤器的执行过程中抛出的异常
// 只进行捕获并打印日志,但不向上抛出
logger.error(var2.getMessage(), var2);
}
}
public void route() throws ZuulException {
try {
this.runFilters("route");
} catch (ZuulException var2) {
throw var2;
} catch (Throwable var3) {
throw new ZuulException(var3, 500, "UNCAUGHT_EXCEPTION_IN_ROUTE_FILTER_" + var3.getClass().getName());
}
}
public void preRoute() throws ZuulException {
try {
this.runFilters("pre");
} catch (ZuulException var2) {
throw var2;
} catch (Throwable var3) {
throw new ZuulException(var3, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + var3.getClass().getName());
}
}
// 省略部分无关方法代码
.......
}
从源码可以看出,FilterProcessor的xxxRoute()针对不同的过滤器类型有不同的方法实现体,其中对异常的处理并不全都相同。
针对pre、route、post类型过滤器的执行调用,若runFilters()过程中出现异常,那么FilterProcessor会对异常进行捕获,但不进行消化处理,而是在catch中进行二次抛出。从而将异常向上传递,抛给调用者,最终传递到ZuulServlet.service()中。
而对于error类型过滤器,若是在执行runFilters(“error”)的过程中出现了异常,那么会对该异常进行catch,并调用日志打印器logger打印相关的异常信息,然后就结束了本次异常的处理。也就是说,不会将异常进行二次抛出,所以调用者如ZuulServlet对该异常是不知情的,因此也就没有相应的后续处理。
因此,若是error过滤器链在执行过程中出现了异常,是会被FilterProcessor自我消化的,不需要其他的组件来兜底。
异常时的输出到客户端的信息,是由谁来负责输出的?默认的post过滤器起到了什么作用?
Zuul的生命周期过程中出现异常时,
默认的error类型过滤器链只有一个过滤器,是:SendErrorFilter;
默认的post类型过滤器链也只有一个,是:SendResponseFilter。
整个请求响应的过程中若出现了异常,无论是pre、route还是post过滤器链执行出现了异常,最终都会由error过滤器链对异常响应信息进行封装处理并写入到response的writer(类型为:CoyoteWriter)中。
并且SendErrorFilter写完后会强制关闭writer,也就是说后续的其他error过滤器或post过滤器,包括我们自定义的这两种类型的过滤器,都无法对响应内容进行修改。
因为在Java中,一旦流被关闭,就意味着它已经不可用了,任何试图使用已关闭的流进行写操作都会抛出异常。因此,一旦CoyoteWriter被关闭,它通常就不能再被利用来向客户端输出响应数据。
这也就是为什么说,如果我们要自定义异常信息,即使我们自定义了error类型过滤器,它也无法将我们自定义的异常信息响应给客户端,也就是说无法发挥作用。
原因就在于Zuul默认的error过滤器SendErrorFilter在error过滤器链中的优先级最高,且它会强制关闭RequestContext.getCurrentContext().response的writer输出流对象,导出后续的过滤器都无法对响应内容进行再次修改,源码如下:
public class SendErrorFilter extends ZuulFilter {
private static final Log log = LogFactory.getLog(SendErrorFilter.class);
protected static final String SEND_ERROR_FILTER_RAN = "sendErrorFilter.ran";
@Value("${error.path:/error}")
private String errorPath;
public SendErrorFilter() {
}
// error类型
public String filterType() {
return "error";
}
// 优先级最高
public int filterOrder() {
return 0;
}
// 其他方法省略
......
}
因此,如果要让我们自定义的error类型过滤器发挥作用,实现能够对异常响应内容进行自定义,那我们就必须先禁用了Zuul默认的error过滤器也就是SendErrorFilter。
配置如下:
zuul:
SendErrorFilter: # 过滤器类名
error: # 过滤器类型
disable: true # 禁用该过滤器
注意!这里禁用Zuul默认的SendErrorFilter过滤器,是为了实现自定义异常响应内容(因为SendErrorFilter会把writer输出流给close)。 若是没有这方面的需求,是不需要开启这个配置的。即, SendErrorFilter的启用与否,并不会影响到我们自定义的error类型过滤器的执行,它只是影响到自定义的异常响应内容能否顺利响应到客户端。
再回到本话题的结论,SendErrorFilter会强制关闭response的writer,源码如何追踪?
由于这部分涉及的源码内容比较多,就不贴出来了,大家有兴趣的可以自定义一个过滤器并在run()抛出异常,然后debug,需要重点留意的位置(类名.方法名)是:
- SendErrorFilter.run():
// 方法中的核心代码:
RequestDispatcher dispatcher = request.getRequestDispatcher(this.errorPath);
if (dispatcher != null) {
ctx.set("sendErrorFilter.ran", true);
if (!ctx.getResponse().isCommitted()) {
ctx.setResponseStatusCode(exception.getStatusCode());
// 对请求进行转发,转发到/error端点
dispatcher.forward(request, ctx.getResponse());
}
}
- ApplicationDispatcher.doForward():
// 方法中的核心代码:
if (response instanceof ResponseFacade) {
((ResponseFacade) response).finish();
} else {
// Servlet SRV.6.2.2. The Request/Response may have been wrapped
// and may no longer be instance of RequestFacade
if (wrapper.getLogger().isDebugEnabled()){
wrapper.getLogger().debug( " The Response is vehiculed using a wrapper: "
+ response.getClass().getName() );
}
// Close anyway
// !!!关键点在这里!!!
try {
// 在这一步之前,会通过各种逻辑,将响应信息写入到response.writer中,
// 到了这一步后,就会强制将writer进行close(),
// 也就是关闭本次请求的最终响应的输出流对象,那么后续就无法再对响应内容进行任何修改操作
PrintWriter writer = response.getWriter();
writer.close();
} catch (IllegalStateException e) {
try {
ServletOutputStream stream = response.getOutputStream();
stream.close();
} catch (IllegalStateException f) {
// Ignore
} catch (IOException f) {
// Ignore
}
} catch (IOException e) {
// Ignore
}
}
在关闭了response.writer后,SendErrorFilter的处理逻辑也就大体上都结束了,会逐层返回,最终回到ZuulServlet.service()中。
那么,对于pre、route类型的过滤器,既然SendErrorFilter已经将异常请求的响应内容设置好了,那么post类型过滤器在这里起到了什么作用呢?
答案是:默认的post类型过滤器(即SendResponseFilter)在这里起到了造型上的作用。
不信?那你往下看:
当调用ZuulFilter.runFilter()时,会对具体的过滤器进行开关判断,即判断该过滤器是否启用,源码如下:
public abstract class ZuulFilter implements IZuulFilter, Comparable<ZuulFilter> {
// 省略其他无关代码
......
public String disablePropertyName() {
// 在配置文件中的disable属性设置
return "zuul." + this.getClass().getSimpleName() + "." + this.filterType() + ".disable";
}
public boolean isFilterDisabled() {
this.filterDisabledRef.compareAndSet((Object)null, DynamicPropertyFactory.getInstance().getBooleanProperty(this.disablePropertyName(), false));
return ((DynamicBooleanProperty)this.filterDisabledRef.get()).get();
}
public ZuulFilterResult runFilter() {
ZuulFilterResult zr = new ZuulFilterResult();
// 判断该过滤器是否在配置文件中被设置disable: true
if (!this.isFilterDisabled()) {
// 判断该过滤器重写的shouldFilter()是否返回true
if (this.shouldFilter()) {
Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName());
try {
Object res = this.run();
zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS);
} catch (Throwable var7) {
t.setName("ZUUL::" + this.getClass().getSimpleName() + " failed");
zr = new ZuulFilterResult(ExecutionStatus.FAILED);
zr.setException(var7);
} finally {
t.stopAndLog();
}
} else {
zr = new ZuulFilterResult(ExecutionStatus.SKIPPED);
}
}
return zr;
}
// 省略其他无关方法
......
}
其中,对于异常发生时,catch中调用post过滤器链,默认是调用了SendResponseFilter,而该过滤器的shouldFilter()此时会是如下的情况:
此时由于生命周期中有异常被捕获并放置在请求上下文(RequestContext)中,因此&&的前置条件是false,所以该方法会返回false,也就是不执行,所以ZuulFilter.runFilter()会进入else分支并最终返回一个状态为SKIPPED(译:跳过)的结果对象(return new ZuulFilterResult(ExecutionStatus.SKIPPED));
error类型过滤器的管辖范围
Zuul中的error类型过滤器,只会对pre、route和post类型过滤器的执行流程中的异常进行管理。
若是在调用远程服务API时,API的执行过程中出现了异常,那么Zuul是无法对此异常进行管理的(当然包括自定义的error过滤器的异常处理也同样不起作用),业务层的异常会交由Spring来管理。
如下图所示,是Zuul过滤器的整个生命周期流程图,其中虚线部分所圈住的范围,就是error类型过滤器的管辖范围。很明显,Origin Server中出现的异常,不归error类型过滤器管控和处理。
核心总结
- pre、route、post类型过滤器链执行过程中出现异常,都会由error过滤器链进行异常处理,但只有pre、route类型产生的异常会在error类型过滤器链处理后,再交由post类型过滤器链进行二次处理。
- FilterProcessor在调用pre、route、post三种类型的过滤器时,若出现异常,会将异常向上抛出,不消化异常。但在调用error类型过滤器链时,若出现异常,则直接进行catch打印相关日志信息,并不再向上抛出。
- 同一类型的过滤器链中,有一个过滤器执行时出现了异常,那么该类型过滤器链中的剩余过滤器,都不会再继续往下执行,而是将异常抛出直到ZuulServlet.service()的catch{}中。
- 请求过程中出现异常,那么本次请求所对应的响应中的异常信息,由error过滤器链进行控制和输出到response中,给到客户端。默认的post过滤器链会被跳过,不做任何操作。
- 自定义的异常处理error类型过滤器,若是涉及了自定义异常响应内容这方面的处理,必须先禁用了默认的SendErrorFilter才能发挥作用。
- error类型过滤器,只对Zuul的过滤器(包括pre、route和post三种类型的过滤器)执行过程中产生的异常有作用,无法处理在Origin Server即远程业务层服务的异常。
好了,以上就是我个人对本次内容的理解与解析,如果有什么不恰当的地方,还望各位兄弟在评论区指出哦。
如果这篇文章对你有帮助的话,不妨点个关注吧~
期待下次我们共同讨论,一起进步~