Tomcat Url 映射源码学习

在Tomcat中接收到具体的http请求,请求最后被一个具体的service处理,中间有一系列操作,有service的初始化、监听、过滤器等等操作,今天主要说的是service和URL的映射以及URL的匹配规则。和所有的web框架类似,URL肯定是有一个地方设置,然后关联具体的service,其他web框架有可能是使用正则(例如Django),Tomcat却是使用web.xml关联,接下来就讲讲映射的具体细节以及如何匹配到tomcat的wrap上
以下涉及到的源码版本:java8、Tomcat8.5.4

Tomcat 基础了解

先了解下Tomcat的各个组件的关系吧


image.png

Tomcat是有个核心类叫做catalina,在启动的时候就是根据不同的参数加载catalina的不同模块的功能,例如生命周期、事件监听、组件管理等等。
Tomcat的server服务器可以包含多个service服务,每一个service服务都存在一个组件connector接收http请求,然后交给container组件去进行下一步的处理。如图包含了engine、host、context、wrapper四种组件,其中wrapper就是包含了用户实际开发的servlet服务。此外pipeline做为管道,一个组件只持有一个管道,然后管道上可以加上各种各样的阀门valve,通过动态配置valve,我们就可以实现数据的修改,监控等等各种操作。
servlet规范中有明确的规定,servlet的服务时是使用ServletContext来传递上下文,在Tomcat中是实现了ApplicationContext去绑定service和context组件的,通过这样的绑定就可以间接的绑定connector,使得整个的上下文都可以持有。

public ApplicationContext(StandardContext context) {
   super();
   this.context = context;
   this.service = ((Engine) context.getParent().getParent()).getService();
   this.sessionCookieConfig = new ApplicationSessionCookieConfig(context);

   // Populate session tracking modes
   populateSessionTrackingModes();
}
image.png

URL配置

URL配置是通过配置web.xml的URL-pattern设置URL到servlet类的映射关系的,如下样例

    <servlet>
        <servlet-name>ExactServlet</servlet-name>
        <servlet-class>org.test.ExactServlet</servlet-class>
        <!-- init参数以及load-on-startup等暂时忽略-->
    </servlet>
    <servlet-mapping>
        <servlet-name>ExactServlet</servlet-name>
        <url-pattern>/exact.action</url-pattern>
    </servlet-mapping>

先明确好具体的servlet类,其中servlet-name表示一个servlet的名称,不允许重复,和具体的servlet-mapping对应,url-pattern就是我们关心的URL,现在一个*****/exact.action的URL请求过来,会被关联到org.test.ExactServlet类上。

URL有三种配置方法

  • 完全匹配 /index.html
  • 目录匹配 /news/*
  • 后缀匹配 *.do

URL读取

在不看源码之前,如果我们实现该功能,第一步肯定是解析xml文件,找到具体的映射之间的关系,然后具体请求就根据某些规则匹配出最合适的servlet服务,如果匹配失败就会提示404错误。

现在就看看源码具体的细节是如何操作的,其实Tomcat的具体实现和我们说的也基本类似(基本上的URL映射都是这个套路吧),入口是mbeanfactory类的createStandardContext方法,其中context加上了ContextConfig监听者


image.png

然后就是StandardContext类的启动,按照链路分析,发现在startInternal方法中有进行CONFIG_START_EVENT的监听事件触发

又因为上面加入的监听者是ContextConfig类,那么最后就进入到该类的事件处理方法上


image.png

configStart这个方法的名字就很直观,表示的是属性开始配置,又来到webConfig方法


image.png

通过WebXmlParse类的即系以及WebXml数据的存储,依旧是采用了digester的方式解析xml数据,中间通过各种操作,最后把数据存储到了StandardContext的servletMappings键值对中。


image.png

通过上述操作,就完成了URL从xml文件中到context的过渡工作。下面这个图简要的介绍下上面的整个流程(有些细节还未完全处理好)

(

URL映射

这一节介绍接收到一个http请求,URL是如何从socket被解析出来,映射到具体的servlet的一整个过程,包括了URL匹配的细节

作为一个http容器,必然存在接受socket套接字的数据,我们可以看到在Tomcat的启动时候的代码


image.png
image.png

其中getConnector()方法就是创建一个connector,使用配置好的端口号,http1.1的协议,并且把这个连接器组件绑定到service上。再回看上面的框架图,肯定知道在service中包含了一个engine组件,重点是在Mapper,关于解析socket数据不在此次介绍中。

image.png

Mapper的作用就是通过一系列的规则,最后匹配到合适的servlet去执行相应功能,具体的调用是由MapperListener监听器完成。在MapperListener类中监听到容器事件

@Override
public void containerEvent(ContainerEvent event) {
   if (Container.ADD_CHILD_EVENT.equals(event.getType())) {
       Container child = (Container) event.getData();
       addListeners(child);
       // 添加子容器
       if (child.getState().isAvailable()) {
           if (child instanceof Host) {
               registerHost((Host) child);
           } else if (child instanceof Context) {
               registerContext((Context) child);
           } else if (child instanceof Wrapper) {
               // Only if the Context has started. If it has not, then it
               // will have its own "after_start" life-cycle event later.
               if (child.getParent().getState().isAvailable()) {
                   registerWrapper((Wrapper) child);
               }
           }
       }
   } else if (Container.REMOVE_CHILD_EVENT.equals(event.getType())) {
       Container child = (Container) event.getData();
       removeListeners(child);
       // No need to unregister - life-cycle listener will handle this when
       // the child stops
   } else if (Host.ADD_ALIAS_EVENT.equals(event.getType())) {
       // Handle dynamically adding host aliases
       mapper.addHostAlias(((Host) event.getSource()).getName(),
               event.getData().toString());
   } else if (Host.REMOVE_ALIAS_EVENT.equals(event.getType())) {
       // Handle dynamically removing host aliases
       mapper.removeHostAlias(event.getData().toString());
   } else if (Wrapper.ADD_MAPPING_EVENT.equals(event.getType())) {
       // Handle dynamically adding wrappers
       Wrapper wrapper = (Wrapper) event.getSource();
       Context context = (Context) wrapper.getParent();
       String contextPath = context.getPath();
       if ("/".equals(contextPath)) {
           contextPath = "";
       }
       String version = context.getWebappVersion();
       String hostName = context.getParent().getName();
       String wrapperName = wrapper.getName();
       String mapping = (String) event.getData();
       boolean jspWildCard = ("jsp".equals(wrapperName)
               && mapping.endsWith("/*"));
       mapper.addWrapper(hostName, contextPath, version, mapping, wrapper,
               jspWildCard, context.isResourceOnlyServlet(wrapperName));
   } else if (Wrapper.REMOVE_MAPPING_EVENT.equals(event.getType())) {
       // Handle dynamically removing wrappers
       Wrapper wrapper = (Wrapper) event.getSource();

       Context context = (Context) wrapper.getParent();
       String contextPath = context.getPath();
       if ("/".equals(contextPath)) {
           contextPath = "";
       }
       String version = context.getWebappVersion();
       String hostName = context.getParent().getName();

       String mapping = (String) event.getData();

       mapper.removeWrapper(hostName, contextPath, version, mapping);
   } else if (Context.ADD_WELCOME_FILE_EVENT.equals(event.getType())) {
       // Handle dynamically adding welcome files
       Context context = (Context) event.getSource();

       String hostName = context.getParent().getName();

       String contextPath = context.getPath();
       if ("/".equals(contextPath)) {
           contextPath = "";
       }

       String welcomeFile = (String) event.getData();

       mapper.addWelcomeFile(hostName, contextPath,
               context.getWebappVersion(), welcomeFile);
   } else if (Context.REMOVE_WELCOME_FILE_EVENT.equals(event.getType())) {
       // Handle dynamically removing welcome files
       Context context = (Context) event.getSource();

       String hostName = context.getParent().getName();

       String contextPath = context.getPath();
       if ("/".equals(contextPath)) {
           contextPath = "";
       }

       String welcomeFile = (String) event.getData();

       mapper.removeWelcomeFile(hostName, contextPath,
               context.getWebappVersion(), welcomeFile);
   } else if (Context.CLEAR_WELCOME_FILES_EVENT.equals(event.getType())) {
       // Handle dynamically clearing welcome files
       Context context = (Context) event.getSource();

       String hostName = context.getParent().getName();

       String contextPath = context.getPath();
       if ("/".equals(contextPath)) {
           contextPath = "";
       }

       mapper.clearWelcomeFiles(hostName, contextPath,
               context.getWebappVersion());
   }
}

仔细看这个代码没发现什么异常,可以细看会觉得有些不对劲,MapperListener作为service的监听器怎么可能接收到添加wrapper的,中间还嵌套了engine、host等容器,按照逻辑肯定是不能挂载wrapper的。回过头来再看StandardService类的启动方法

protected void startInternal() throws LifecycleException {

   if(log.isInfoEnabled())
       log.info(sm.getString("standardService.start.name", this.name));
   setState(LifecycleState.STARTING);

   // Start our defined Container first
   if (engine != null) {
       synchronized (engine) {
           engine.start();
           // 启动了engine
       }
   }

   synchronized (executors) {
       for (Executor executor: executors) {
           executor.start();
       }
   }

   mapperListener.start();
   // 当前standardservice的监听器也启动了

上述代码可知,在engine.start()的时候,engine以及engine的子容器,子容器的子容器也都顺利启动了,各个组件的嵌套关系也很明确,细看mapperListener.start()

public void startInternal() throws LifecycleException {

   setState(LifecycleState.STARTING);

   Engine engine = service.getContainer();
   if (engine == null) {
       return;
   }

   findDefaultHost();

   addListeners(engine);
   // 比较关键的一步,加上监听器,也是我们当前关注的重点

   Container[] conHosts = engine.findChildren();
   // 开始处理engine的子容器
   for (Container conHost : conHosts) {
       Host host = (Host) conHost;
       if (!LifecycleState.NEW.equals(host.getState())) {
           // Registering the host will register the context and wrappers
           registerHost(host);
           // 注册host组件
       }
   }
}

private void addListeners(Container container) {
   container.addContainerListener(this);
   // 把该mapperlistener加入到容器的监听者中
   container.addLifecycleListener(this);
   for (Container child : container.findChildren()) {
       // 针对engine而言子容器就是host,给每个host加上该mapperlistener
       // 遍历所有的容器组件,给每个组件都加上mapperlistener监听器
       addListeners(child);
   }
}

上述代码已经很清楚的告诉我们,每一个容器都持有同一个mapperlistener监听器对象,所以上述的可以添加wrapper容器也可以很好的解释了,每一个组件都可以调用该方法,自然就存在挂载wrapper的情况了。

该函数内容较多,大部分事件都是在容器内插入新的子容器,以及插入子容器后续的事情,就以addMapping为例子

    // 调用该addMapping的方法在StandardContext的addServletMapping方法内
    // fireContainerEvent("addServletMapping", decodedPattern);
    
   } else if (Wrapper.ADD_MAPPING_EVENT.equals(event.getType())) {
       // Handle dynamically adding wrappers
       Wrapper wrapper = (Wrapper) event.getSource();
       Context context = (Context) wrapper.getParent();
       String contextPath = context.getPath();
       if ("/".equals(contextPath)) {
           contextPath = "";
       }
       String version = context.getWebappVersion();
       String hostName = context.getParent().getName();
       String wrapperName = wrapper.getName();
       String mapping = (String) event.getData();
       boolean jspWildCard = ("jsp".equals(wrapperName)
               && mapping.endsWith("/*"));
       mapper.addWrapper(hostName, contextPath, version, mapping, wrapper,
               jspWildCard, context.isResourceOnlyServlet(wrapperName));
                    
                    
                    
// mapper的addwrapper方法
protected void addWrapper(ContextVersion context, String path,
       Wrapper wrapper, boolean jspWildCard, boolean resourceOnly) {

   synchronized (context) {
       if (path.endsWith("/*")) {
           // Wildcard wrapper
           String name = path.substring(0, path.length() - 2);
           MappedWrapper newWrapper = new MappedWrapper(name, wrapper,
                   jspWildCard, resourceOnly);
           MappedWrapper[] oldWrappers = context.wildcardWrappers;
           MappedWrapper[] newWrappers = new MappedWrapper[oldWrappers.length + 1];
           if (insertMap(oldWrappers, newWrappers, newWrapper)) {
               context.wildcardWrappers = newWrappers;
               int slashCount = slashCount(newWrapper.name);
               if (slashCount > context.nesting) {
                   context.nesting = slashCount;
               }
           }
       } else if (path.startsWith("*.")) {
           // Extension wrapper
           String name = path.substring(2);
           MappedWrapper newWrapper = new MappedWrapper(name, wrapper,
                   jspWildCard, resourceOnly);
           MappedWrapper[] oldWrappers = context.extensionWrappers;
           MappedWrapper[] newWrappers =
               new MappedWrapper[oldWrappers.length + 1];
           if (insertMap(oldWrappers, newWrappers, newWrapper)) {
               context.extensionWrappers = newWrappers;
           }
       } else if (path.equals("/")) {
           // Default wrapper
           MappedWrapper newWrapper = new MappedWrapper("", wrapper,
                   jspWildCard, resourceOnly);
           context.defaultWrapper = newWrapper;
       } else {
           // Exact wrapper
           final String name;
           if (path.length() == 0) {
               // Special case for the Context Root mapping which is
               // treated as an exact match
               name = "/";
           } else {
               name = path;
           }
           MappedWrapper newWrapper = new MappedWrapper(name, wrapper,
                   jspWildCard, resourceOnly);
           MappedWrapper[] oldWrappers = context.exactWrappers;
           MappedWrapper[] newWrappers = new MappedWrapper[oldWrappers.length + 1];
           if (insertMap(oldWrappers, newWrappers, newWrapper)) {
               context.exactWrappers = newWrappers;
           }
       }
   }
}

这里,我们可以看到路径分成4类,其中包含了我们上面说的三种分类情况,其实细看源码会发现,不同匹配规则会被放到不同类型的wrapper中,其中有

  • /* 放在wildcardWrappers中
  • *. 放在extensionWrappers中
  • / 放在defaultWrapper中
  • 其他 放在exactWrappers中

以上就完成了wrapper以及一系列容器的关联嵌套。

HTTP处理

一个新来的http请求,也需要找到合适的engine、host、context、wrapper进行处理,在接收到新的请求之后,在CoyoteAdapt类中调用map方法,再调用internalMap方法,这个方法中可以为mappingdata设置整个链路的容器(除了wrapper),最后的internalMapWrapper 明确最后的wrapper

// CoyoteAdapt
  connector.getService().getMapper().map(serverName, decodedURI,
          version, request.getMappingData());

private final void internalMap(CharChunk host, CharChunk uri,
  String version, MappingData mappingData) throws IOException {

if (mappingData.host != null) {
  // The legacy code (dating down at least to Tomcat 4.1) just
  // skipped all mapping work in this case. That behaviour has a risk
  // of returning an inconsistent result.
  // I do not see a valid use case for it.
  throw new AssertionError();
}

uri.setLimit(-1);

// Virtual host mapping
MappedHost[] hosts = this.hosts;
MappedHost mappedHost = exactFindIgnoreCase(hosts, host);
if (mappedHost == null) {
  if (defaultHostName == null) {
      return;
  }
  mappedHost = exactFind(hosts, defaultHostName);
  if (mappedHost == null) {
      return;
  }
}
mappingData.host = mappedHost.object;

// Context mapping
ContextList contextList = mappedHost.contextList;
MappedContext[] contexts = contextList.contexts;
int pos = find(contexts, uri);
if (pos == -1) {
  return;
}

int lastSlash = -1;
int uriEnd = uri.getEnd();
int length = -1;
boolean found = false;
MappedContext context = null;
while (pos >= 0) {
  context = contexts[pos];
  if (uri.startsWith(context.name)) {
      length = context.name.length();
      if (uri.getLength() == length) {
          found = true;
          break;
      } else if (uri.startsWithIgnoreCase("/", length)) {
          found = true;
          break;
      }
  }
  if (lastSlash == -1) {
      lastSlash = nthSlash(uri, contextList.nesting + 1);
  } else {
      lastSlash = lastSlash(uri);
  }
  uri.setEnd(lastSlash);
  pos = find(contexts, uri);
}
uri.setEnd(uriEnd);

if (!found) {
  if (contexts[0].name.equals("")) {
      context = contexts[0];
  } else {
      context = null;
  }
}
if (context == null) {
  return;
}

mappingData.contextPath.setString(context.name);

ContextVersion contextVersion = null;
ContextVersion[] contextVersions = context.versions;
final int versionCount = contextVersions.length;
if (versionCount > 1) {
  Context[] contextObjects = new Context[contextVersions.length];
  for (int i = 0; i < contextObjects.length; i++) {
      contextObjects[i] = contextVersions[i].object;
  }
  mappingData.contexts = contextObjects;
  if (version != null) {
      contextVersion = exactFind(contextVersions, version);
  }
}
if (contextVersion == null) {
  // Return the latest version
  // The versions array is known to contain at least one element
  contextVersion = contextVersions[versionCount - 1];
}
mappingData.context = contextVersion.object;
mappingData.contextSlashCount = contextVersion.slashCount;

// Wrapper mapping
if (!contextVersion.isPaused()) {
  internalMapWrapper(contextVersion, uri, mappingData);
}

}

// 根据URL匹配具体的wrapper规则
private final void internalMapWrapper(ContextVersion contextVersion,
                                     CharChunk path,
                                     MappingData mappingData) throws IOException {

   int pathOffset = path.getOffset();
   int pathEnd = path.getEnd();
   boolean noServletPath = false;

   int length = contextVersion.path.length();
   if (length == (pathEnd - pathOffset)) {
       noServletPath = true;
   }
   int servletPath = pathOffset + length;
   path.setOffset(servletPath);

   // Rule 1 -- Exact Match
   MappedWrapper[] exactWrappers = contextVersion.exactWrappers;
   internalMapExactWrapper(exactWrappers, path, mappingData);

   // Rule 2 -- Prefix Match
   boolean checkJspWelcomeFiles = false;
   MappedWrapper[] wildcardWrappers = contextVersion.wildcardWrappers;
   if (mappingData.wrapper == null) {
       internalMapWildcardWrapper(wildcardWrappers, contextVersion.nesting,
                                  path, mappingData);
       if (mappingData.wrapper != null && mappingData.jspWildCard) {
           char[] buf = path.getBuffer();
           if (buf[pathEnd - 1] == '/') {
               /*
                * Path ending in '/' was mapped to JSP servlet based on
                * wildcard match (e.g., as specified in url-pattern of a
                * jsp-property-group.
                * Force the context's welcome files, which are interpreted
                * as JSP files (since they match the url-pattern), to be
                * considered. See Bugzilla 27664.
                */
               mappingData.wrapper = null;
               checkJspWelcomeFiles = true;
           } else {
               // See Bugzilla 27704
               mappingData.wrapperPath.setChars(buf, path.getStart(),
                                                path.getLength());
               mappingData.pathInfo.recycle();
           }
       }
   }

   if(mappingData.wrapper == null && noServletPath &&
           contextVersion.object.getMapperContextRootRedirectEnabled()) {
       // The path is empty, redirect to "/"
       path.append('/');
       pathEnd = path.getEnd();
       mappingData.redirectPath.setChars
           (path.getBuffer(), pathOffset, pathEnd - pathOffset);
       path.setEnd(pathEnd - 1);
       return;
   }

   // Rule 3 -- Extension Match
   MappedWrapper[] extensionWrappers = contextVersion.extensionWrappers;
   if (mappingData.wrapper == null && !checkJspWelcomeFiles) {
       internalMapExtensionWrapper(extensionWrappers, path, mappingData,
               true);
   }

   // Rule 4 -- Welcome resources processing for servlets
   if (mappingData.wrapper == null) {
       boolean checkWelcomeFiles = checkJspWelcomeFiles;
       if (!checkWelcomeFiles) {
           char[] buf = path.getBuffer();
           checkWelcomeFiles = (buf[pathEnd - 1] == '/');
       }
       if (checkWelcomeFiles) {
           for (int i = 0; (i < contextVersion.welcomeResources.length)
                    && (mappingData.wrapper == null); i++) {
               path.setOffset(pathOffset);
               path.setEnd(pathEnd);
               path.append(contextVersion.welcomeResources[i], 0,
                       contextVersion.welcomeResources[i].length());
               path.setOffset(servletPath);

               // Rule 4a -- Welcome resources processing for exact macth
               internalMapExactWrapper(exactWrappers, path, mappingData);

               // Rule 4b -- Welcome resources processing for prefix match
               if (mappingData.wrapper == null) {
                   internalMapWildcardWrapper
                       (wildcardWrappers, contextVersion.nesting,
                        path, mappingData);
               }

               // Rule 4c -- Welcome resources processing
               //            for physical folder
               if (mappingData.wrapper == null
                   && contextVersion.resources != null) {
                   String pathStr = path.toString();
                   WebResource file =
                           contextVersion.resources.getResource(pathStr);
                   if (file != null && file.isFile()) {
                       internalMapExtensionWrapper(extensionWrappers, path,
                                                   mappingData, true);
                       if (mappingData.wrapper == null
                           && contextVersion.defaultWrapper != null) {
                           mappingData.wrapper =
                               contextVersion.defaultWrapper.object;
                           mappingData.requestPath.setChars
                               (path.getBuffer(), path.getStart(),
                                path.getLength());
                           mappingData.wrapperPath.setChars
                               (path.getBuffer(), path.getStart(),
                                path.getLength());
                           mappingData.requestPath.setString(pathStr);
                           mappingData.wrapperPath.setString(pathStr);
                       }
                   }
               }
           }

           path.setOffset(servletPath);
           path.setEnd(pathEnd);
       }

   }

   /* welcome file processing - take 2
    * Now that we have looked for welcome files with a physical
    * backing, now look for an extension mapping listed
    * but may not have a physical backing to it. This is for
    * the case of index.jsf, index.do, etc.
    * A watered down version of rule 4
    */
   if (mappingData.wrapper == null) {
       boolean checkWelcomeFiles = checkJspWelcomeFiles;
       if (!checkWelcomeFiles) {
           char[] buf = path.getBuffer();
           checkWelcomeFiles = (buf[pathEnd - 1] == '/');
       }
       if (checkWelcomeFiles) {
           for (int i = 0; (i < contextVersion.welcomeResources.length)
                    && (mappingData.wrapper == null); i++) {
               path.setOffset(pathOffset);
               path.setEnd(pathEnd);
               path.append(contextVersion.welcomeResources[i], 0,
                           contextVersion.welcomeResources[i].length());
               path.setOffset(servletPath);
               internalMapExtensionWrapper(extensionWrappers, path,
                                           mappingData, false);
           }

           path.setOffset(servletPath);
           path.setEnd(pathEnd);
       }
   }


   // Rule 7 -- Default servlet
   if (mappingData.wrapper == null && !checkJspWelcomeFiles) {
       if (contextVersion.defaultWrapper != null) {
           mappingData.wrapper = contextVersion.defaultWrapper.object;
           mappingData.requestPath.setChars
               (path.getBuffer(), path.getStart(), path.getLength());
           mappingData.wrapperPath.setChars
               (path.getBuffer(), path.getStart(), path.getLength());
           mappingData.matchType = MappingMatch.DEFAULT;
       }
       // Redirection to a folder
       char[] buf = path.getBuffer();
       if (contextVersion.resources != null && buf[pathEnd -1 ] != '/') {
           String pathStr = path.toString();
           WebResource file;
           // Handle context root
           if (pathStr.length() == 0) {
               file = contextVersion.resources.getResource("/");
           } else {
               file = contextVersion.resources.getResource(pathStr);
           }
           if (file != null && file.isDirectory() &&
                   contextVersion.object.getMapperDirectoryRedirectEnabled()) {
               // Note: this mutates the path: do not do any processing
               // after this (since we set the redirectPath, there
               // shouldn't be any)
               path.setOffset(pathOffset);
               path.append('/');
               mappingData.redirectPath.setChars
                   (path.getBuffer(), path.getStart(), path.getLength());
           } else {
               mappingData.requestPath.setString(pathStr);
               mappingData.wrapperPath.setString(pathStr);
           }
       }
   }

   path.setOffset(pathOffset);
   path.setEnd(pathEnd);
}

以上过程也知道了,只要一个http请求解析出http协议的字段信息,就立马明确了其整个的执行链路过程,链路数据是存储在mappingdata中,这点和Tomcat4有些不一样

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容

  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,183评论 11 349
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,747评论 6 342
  • 0 系列目录# WEB请求处理 WEB请求处理一:浏览器请求发起处理 WEB请求处理二:Nginx请求反向代理 本...
    七寸知架构阅读 13,884评论 22 190
  • 曾听过这样一个故事,让我记忆犹新,至今都难以忘怀。在很久很久以前,一个荒漠上,到处都是一片黄色的景象:黄色...
    hiliary阅读 372评论 0 0
  • 周四(0713)下午,动儿告诉我第二天开直播,早上六点开始,持续三个小时。因为约了周五早上去大李家,周五没能亲历动...
    小可以之动阅读 583评论 4 50