SpringMVC 原始碼解析
前言
年初面試時接觸到一道面試題,在聊到SpringMVC時提到了SpringMVC的開發者為何要設計父子容器呢,又或者說是父子容器的設計有什麼更實際的作用呢? 首先要理解對於一個web應用,當期部署在web容器上時,容器會為其提供一個全域性上下文環境ServletContext,這個上下文環境將為後續的Spring提供宿主環境。
SpringMVC工作流程
DispatcherServlet上下文繼承關係
SpringMVC設計的父子容器
父子容器配置檔案
--在web.xml中配置,兩個重要的xml:applicationContext.xml和SpringMVC-conf.xml
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:applictionContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>dispatcher-servlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:springMVC-conf.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher-servlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
複製程式碼
父子容器的設計目的
根據SpringMVC的官方解釋,父(根)容器主要包括一些基礎腳手架的bean,比如Pool、DataSource、Dao、Service。目的是在不同的Servlet例項之間共享。這些不同的bean可以在子容器中重寫。 而子容器主要包括一些Controller、View等一些web相關的bean。
DispatcherServlet原始碼分析
既然SpringMVC中同時包含Spring容器和SpringMVC容器,那麼這兩個容器都是在什麼時候初始化呢?
根容器初始化
首先,根容器是通過ServletContext監聽器進行建立,預設的監聽器為ContextLoaderListener,當web應用啟動時,會呼叫監聽器的contextInitialized
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
public ContextLoaderListener() {
}
public ContextLoaderListener(WebApplicationContext context) {
super(context);
}
//===初始化root WebApplicationContext===
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}
@Override
public void contextDestroyed(ServletContextEvent event) {
closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
}
複製程式碼
//ContextLoader.java
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
//初始化Spring容器時如果發現servlet 容器中已存在根Spring容根器則丟擲異常,證明rootWebApplicationContext只能有一個。
if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
throw new IllegalStateException(
"Cannot initialize context because there is already a root application context present - " +
"check whether you have multiple ContextLoader* definitions in your web.xml!");
}
try {
//建立webApplicationContext例項
if (this.context == null) {
this.context = createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
//配置WebApplicationContext
configureAndRefreshWebApplicationContext(cwac,servletContext);
}
}
/**
把生成的webApplicationContext設定成root WebApplicationContext。儲存在ServletContext上下文中。
下一步初始化MVC ApplicationContext時需要從ServletContext取出根上下文作為其父上下文。
**/
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,this.context);
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
}
else if (ccl != null) {
currentContextPerThread.put(ccl,this.context);
}
return this.context;
}
catch (RuntimeException | Error ex) {
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,ex);
throw ex;
}
}
複製程式碼
以上程式碼主要完成兩個功能:建立例項WebApplicationContext例項、把所建立的WebApplicationContext設定為根上下文,也就是設定成為ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE的值。
MVC容器初始化
大家知道Servlet生命週期都是從init方法開始,desctory方法結束,由jvm負責垃圾回收。而DispatcherServlet也是一個普通的Servlet,先看一下DispatcherServlet的繼承關係圖,對整個繼承關係有個瞭解。
既然說起Servlet,那就從Servlet的初始化(init)方法入手//HttpServletBean.java
@Override
public final void init() throws ServletException {
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(),this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class,new ResourceEditor(resourceLoader,getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs,true);
}
catch (BeansException ex) {
throw ex;
}
}
//交給子類重寫
initServletBean();
}
//FrameworkServlet.java
@Override
protected final void initServletBean() throws ServletException {
try {
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
catch (ServletException | RuntimeException ex) {
throw ex;
}
}
//FrameworkServlet.java
//初始化MVC容器
protected WebApplicationContext initWebApplicationContext() {
//從ServletContext取出根上下文
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
cwac.setParent(rootContext);
}
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
wac = findWebApplicationContext();
}
//如果還沒有webApplicatioinContext,建立webApplicationContext
if (wac == null) {
wac = createWebApplicationContext(rootContext);
}
//子類自定義對servlet子上下文後續操作,在DispatcherServlet中實現
if (!this.refreshEventReceived) {
synchronized (this.onRefreshMonitor) {
//執行子類擴充套件方法onRefresh,在DispatcherServlet內初始化所有web相關元件
onRefresh(wac);
}
}
//釋出servlet子上下文到ServletContext
if (this.publishContext) {
String attrName = getServletContextAttributeName();
//將servlet子上下文以org.springframework.web.servlet.FrameworkServlet.CONTEXT. + servletName的屬性名稱註冊到ServletContext中
getServletContext().setAttribute(attrName,wac);
}
return wac;
}
protected WebApplicationContext createWebApplicationContext(@Nullable WebApplicationContext parent) {
return createWebApplicationContext((ApplicationContext) parent);
}
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
//獲取WebApplicationContext實現類,此處其實就是XmlWebApplicationContext
Class<?> contextClass = getContextClass();
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("Fatal initialization error in servlet with name '" + getServletName() +
"': custom WebApplicationContext class [" + contextClass.getName() +
"] is not of type ConfigurableWebApplicationContext");
}
//生成XmlWebApplicationContext例項
ConfigurableWebApplicationContext wac =
(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
wac.setEnvironment(getEnvironment());
//設定根容器為父容器
wac.setParent(parent);
String configLocation = getContextConfigLocation();
if (configLocation != null) {
//設定配置檔案
wac.setConfigLocation(configLocation);
}
//配置webApplicationContext
configureAndRefreshWebApplicationContext(wac);
return wac;
}
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
if (this.contextId != null) {
wac.setId(this.contextId);
}
else {
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
}
}
wac.setServletContext(getServletContext());
wac.setServletConfig(getServletConfig());
wac.setNamespace(getNamespace());
wac.addApplicationListener(new SourceFilteringListener(wac,new ContextRefreshListener()));
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(),getServletConfig());
}
postProcessWebApplicationContext(wac);
applyInitializers(wac);
//開始處理bean
wac.refresh();
}
複製程式碼
上面的關鍵程式碼都在FrameworkServlet類中,有幾個關鍵點:取除根上下文,建立子上下文並設定父上下文,完成重新整理,把子上下文釋出到ServletContext中。 到這裡可以說子容器(子上下文)已經建立完成。 並把其他初始化web元件的相關工作交給onRefresh方法完成,由DispatcherServlet來重寫onRefresh方法,這就又回到了我們熟悉的initStrategies方法。
web元件初始化
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {
//檔案上傳解析器
initMultipartResolver(context);
//本地化解析器
initLocaleResolver(context);
//主題解析器
initThemeResolver(context);
//處理器對映器(url和Controller方法的對映)
initHandlerMappings(context);
//處理器介面卡(實際執行Controller方法)
initHandlerAdapters(context);
//處理器異常解析器
initHandlerExceptionResolvers(context);
//RequestToViewName解析器
initRequestToViewNameTranslator(context);
//檢視解析器(檢視的匹配和渲染)
initViewResolvers(context);
//FlashMap管理者
initFlashMapManager(context);
}
複製程式碼
這裡我們主要關注一下三個重要元件:HandlerMapping、HandlerAdapter、ViewResolver。分析這3個元件之前,我們先看一下我們的springMVC-conf.xml配置檔案,mvc的配置檔案中,我們配置了兩行程式碼:
<context:component-scan base-package="com.zhangfei"/>
<mvc:annotation-driven>
複製程式碼
第二行程式碼主要是添加了預設的HandleMapping,ViewResolver,HandleAdapter。我們看看annotation-driven的原始碼定義,根據spring自定義schema定義,我們找到如下程式碼,如圖所示:
該檔案就一行程式碼:http\://www.springframework.org/schema/mvc=org.springframework.web.servlet.config.MvcNamespaceHandler
複製程式碼
//MVC所有的標籤解析器都定義在此
public class MvcNamespaceHandler extends NamespaceHandlerSupport {
@Override
public void init() {
registerBeanDefinitionParser("annotation-driven",new AnnotationDrivenBeanDefinitionParser());
registerBeanDefinitionParser("default-servlet-handler",new DefaultServletHandlerBeanDefinitionParser());
registerBeanDefinitionParser("interceptors",new InterceptorsBeanDefinitionParser());
registerBeanDefinitionParser("resources",new ResourcesBeanDefinitionParser());
registerBeanDefinitionParser("view-controller",new ViewControllerBeanDefinitionParser());
registerBeanDefinitionParser("redirect-view-controller",new ViewControllerBeanDefinitionParser());
registerBeanDefinitionParser("status-controller",new ViewControllerBeanDefinitionParser());
registerBeanDefinitionParser("view-resolvers",new ViewResolversBeanDefinitionParser());
registerBeanDefinitionParser("tiles-configurer",new TilesConfigurerBeanDefinitionParser());
registerBeanDefinitionParser("freemarker-configurer",new FreeMarkerConfigurerBeanDefinitionParser());
registerBeanDefinitionParser("groovy-configurer",new GroovyMarkupConfigurerBeanDefinitionParser());
registerBeanDefinitionParser("script-template-configurer",new ScriptTemplateConfigurerBeanDefinitionParser());
registerBeanDefinitionParser("cors",new CorsBeanDefinitionParser());
}
}
複製程式碼
那麼通過分析AnnotationDrivenBeanDefinitionParser類,主要完成以下三大元件的裝配工作:
初始化處理器對映器
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
//這裡detectAllHandlerMappings預設值為true,可以通過配置檔案設定為false
if (this.detectAllHandlerMappings) {
//從上下文(包含父上下文)中查詢所有HandlerMapping實現類
Map<String,HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context,HandlerMapping.class,true,false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<>(matchingBeans.values());
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
try {
//這裡只取固定的bean
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME,HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
}
}
/***
確保至少有一個HandlerMapping,如果沒能找到,註冊一個預設的
預設規則在DispatcherServlet.properties中,這裡也就是取BeanNameUrlHandlerMapping、RequestMappingHandlerMapping
***/
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context,HandlerMapping.class);
}
}
複製程式碼
初始化處理器介面卡
private void initHandlerAdapters(ApplicationContext context) {
this.handlerAdapters = null;
if (this.detectAllHandlerAdapters) {
//從上下文(包括父上下文)中查詢所有HandlerAdapter實現類
Map<String,HandlerAdapter> matchingBeans =BeanFactoryUtils.beansOfTypeIncludingAncestors(context,HandlerAdapter.class,false);
if (!matchingBeans.isEmpty()) {
this.handlerAdapters = new ArrayList<>(matchingBeans.values());
AnnotationAwareOrderComparator.sort(this.handlerAdapters);
}
}
else {
try {
//這裡取bean名字為handlerAdapter,型別為HandlerAdapter的處理器介面卡
HandlerAdapter ha = context.getBean(HANDLER_ADAPTER_BEAN_NAME,HandlerAdapter.class);
this.handlerAdapters = Collections.singletonList(ha);
}
catch (NoSuchBeanDefinitionException ex) {
}
}
/**
如果沒找到,則從預設規則裡取出指定的三個實現類:HttpRequestHandlerAdapter、SimpleControllerHandlerAdapter、RequestMappingHandlerAdapter
**/
if (this.handlerAdapters == null) {
this.handlerAdapters = getDefaultStrategies(context,HandlerAdapter.class);
}
}
複製程式碼
初始化試圖解析器
private void initViewResolvers(ApplicationContext context) {
this.viewResolvers = null;
if (this.detectAllViewResolvers) {
//從上下文(包括父上下文)中查詢所有ViewResolver實現類
Map<String,ViewResolver> matchingBeans =BeanFactoryUtils.beansOfTypeIncludingAncestors(context,ViewResolver.class,false);
if (!matchingBeans.isEmpty()) {
this.viewResolvers = new ArrayList<>(matchingBeans.values());
AnnotationAwareOrderComparator.sort(this.viewResolvers);
}
}
else {
try {
ViewResolver vr = context.getBean(VIEW_RESOLVER_BEAN_NAME,ViewResolver.class);
this.viewResolvers = Collections.singletonList(vr);
}
catch (NoSuchBeanDefinitionException ex) {
}
}
/**
如果沒找到,則從預設規則裡取出指定的實現類:InternalResourceViewResolver
**/
if (this.viewResolvers == null) {
this.viewResolvers = getDefaultStrategies(context,ViewResolver.class);
}
}
複製程式碼
三大元件的初始化最後判斷為NULL時都會呼叫getDefaultStrategies方法,也就是從DispatcherServlet.properties中取出指定預設值。
protected <T> List<T> getDefaultStrategies(ApplicationContext context,Class<T> strategyInterface) {
String key = strategyInterface.getName();
String value = defaultStrategies.getProperty(key);
if (value != null) {
String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
List<T> strategies = new ArrayList<>(classNames.length);
for (String className : classNames) {
try {
Class<?> clazz = ClassUtils.forName(className,DispatcherServlet.class.getClassLoader());
Object strategy = createDefaultStrategy(context,clazz);
strategies.add((T) strategy);
}
catch (ClassNotFoundException ex) {
throw new BeanInitializationException("Could not find DispatcherServlet's default strategy class [" + className +"] for interface [" + key + "]",ex);
}
catch (LinkageError err) {
throw new BeanInitializationException("Unresolvable class definition for DispatcherServlet's default strategy class [" +className + "] for interface [" + key + "]",err);
}
}
return strategies;
}
else {
return new LinkedList<>();
}
}
複製程式碼
DispatcherServlet請求處理過程
提到請求處理過程,我們再來回顧一下Servlet生命週期,處理請求都放在service方法中處理,那麼也從DispatcherServlet的service方法入手。DispatcherServlet繼承FrameworkServlet,在FrameworkServlet中重寫了service、doGet、doPost、doPut、doDelete方法。
//FrameworkServlet.java
@Override
protected void service(HttpServletRequest request,HttpServletResponse response)
throws ServletException,IOException {
HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
processRequest(request,response);
}
else {
super.service(request,response);
}
}
@Override
protected final void doGet(HttpServletRequest request,IOException {
processRequest(request,response);
}
@Override
protected final void doPost(HttpServletRequest request,response);
}
@Override
protected final void doPut(HttpServletRequest request,response);
}
@Override
protected final void doDelete(HttpServletRequest request,response);
}
protected final void processRequest(HttpServletRequest request,HttpServletResponse response)
throws ServletException,IOException {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request,response,previousAttributes);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(),new RequestBindingInterceptor());
//把新構造的LocaleContext物件和ServletRequestAttributes物件和當前請求執行緒繫結(後面要解除繫結)
initContextHolders(request,localeContext,requestAttributes);
try {
//抽象方法,交給DispatcherServlet方法實現
doService(request,response);
}
catch (ServletException | IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed",ex);
}
finally {
//重置LocaleContext和RequestAttributes物件,也就是解除LocaleContext物件和ServletRequestAttributes物件和當前請求執行緒的繫結
resetContextHolders(request,previousLocaleContext,previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
//釋出ServletRequestHandledEvent事件
publishRequestHandledEvent(request,startTime,failureCause);
}
}
複製程式碼
//DispatcherServlet.java
@Override
protected void doService(HttpServletRequest request,HttpServletResponse response) throws Exception {
Map<String,Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = new HashMap<>();
Enumeration<?> attrNames = request.getAttributeNames();
while (attrNames.hasMoreElements()) {
String attrName = (String) attrNames.nextElement();
if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
attributesSnapshot.put(attrName,request.getAttribute(attrName));
}
}
}
//在當前request物件中填充4個屬性
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE,getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE,this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE,this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE,getThemeSource());
if (this.flashMapManager != null) {
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request,response);
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE,Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE,new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE,this.flashMapManager);
}
try {
//主要處理分發請求
doDispatch(request,response);
}
finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request,attributesSnapshot);
}
}
}
}
protected void doDispatch(HttpServletRequest request,HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
//呼叫handlerMapping獲取handlerChain
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest,response);
return;
}
//獲取支援該handler解析的HandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request,mappedHandler.getHandler());
if (new ServletWebRequest(request,response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest,response)) {
return;
}
//使用HandlerAdapter完成handler處理
mv = ha.handle(processedRequest,mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
//檢視處理(頁面渲染)
applyDefaultViewName(processedRequest,mv);
mappedHandler.applyPostHandle(processedRequest,mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
dispatchException = new NestedServletException("Handler dispatch failed",err);
}
processDispatchResult(processedRequest,mappedHandler,mv,dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest,ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest,new NestedServletException("Handler processing failed",err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest,response);
}
}
else {
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
複製程式碼
DispatcherServlet的doDispatch方法概括起來大致就是以下幾點:首先根據當前請求路徑找到對應的HandlerMethod,一個HandlerMethod和若干個攔截器構造一個HandlerExecutionChain.通過HandlerExecutionChain得到HandlerAdapter物件**,通過執行HandlerAdapter的handle方法得到ModelAndView**物件,呼叫ModelAndView解析檢視,渲染檢視,Response結束。
參考
juejin.im/post/5cb89d… juejin.im/post/5cbc10… www.cnblogs.com/fangjian042…