0002 - Spring MVC 拦截器源码简析:拦截器加载与执行
1、概述
Spring MVC中的拦截器(Interceptor)类似于Servlet中的过滤器(Filter),它主要用于拦截用户请求并作相应的处理。例如通过拦截器可以进行权限验证、记录请求信息的日志、判断用户是否登录等。
2、简单示例
2.1.继承 HandlerInterceptorAdapter 抽象类实现一个拦截器。代码如下:
public class DemoInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("[DemoInterceptor]preHandle");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("[DemoInterceptor]postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("[DemoInterceptor]afterCompletion");
}
}
2.2.在指定给 DispatcherServlet 的配置文件中新增相关拦截器配置。
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="com.ryan.springtest.interceptor.DemoInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>
此时启动服务访问任一 URL,即可看到相应的输出信息。
[DemoInterceptor]preHandle
[DemoInterceptor]postHandle
[DemoInterceptor]afterCompletion
3、示例分析
简单分析一下上面的实例代码。
通过观察 HandlerInterceptorAdapter 的继承关系与数据结构,可知 HandlerInterceptorAdapter 实现了 AsyncHandlerInterceptor 与 HandlerInterceptor 接口,并对里面的抽象方法做了默认实现。
- preHandle:Controller 执行之前执行,返回 true 表示继续流程,false 中断当前流程,但会执行之前拦截器的 afterCompletion 方法;
- postHandle:Controller 方法调用之后,视图渲染之前执行;
- afterCompletion:渲染视图之后执行;
- afterConcurrentHandlingStarted:涉及到 Spring 的异步处理特性,先不讨论。
3.1、运行流程图如下:
4、源码分析
源码的分析将分为三部分:拦截器的加载,拦截器链的生成,拦截器链的执行。
4.1、拦截器的加载
配置文件简析
观察示例代码中的配置文件,省略了与分析无关的配置后,如下所示:
<beans
xmlns:mvc="http://www.springframework.org/schema/mvc"
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd">
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="com.ryan.springtest.interceptor.DemoInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>
</beans>
配置信息分为两部分:(1)声明 Schema 的命名空间并指定 xsd 文件;(2)拦截器的具体配置。
interceptors 的配置采用了 Schema Based XML 的组件定义方式,这是 Spring 为了简化组件配置而引入的重要手段,这里不作展开。
通过配置文件,我们可以定位到具体解析拦截器配置的类,类的路径在 spring-webmvc 下的 META-INF 中的 spring.handlers 文件中指定。
spring.handlers 文件内容如下,可知实际解析配置的类是 MvcNamespaceHandler。
http\://www.springframework.org/schema/mvc=org.springframework.web.servlet.config.MvcNamespaceHandler
暂时不对 MvcNamespaceHandler 进行具体的分析,先来看看 MvcNamespaceHandler 是在何时被创建和执行的。
4.2、拦截器的加载过程
拦截器的加载是在 DispatcherServlet 初始化的过程中完成的,下面通过 DispatcherServlet 初始化时的调用图来了解一下拦截器加载的具体触发时机。
通过继承关系,可以发现 DispatcherServlet 实际上是一个 Servlet,根据 Servlet 的规范,服务启动时会调用 Servlet 的 init 方法,init 方法在 HttpServletBean 中实现,HttpServletBean 又会调用 FrameworkServlet 来创建 SpringMVC 单独的容器,并向容器进行了设置,然后调用容器的 refresh 方法触发容器的初始化操作。
在 DispatcherServlet 的初始化中,分为两个层次。
- DispatcherServlet 对容器的初始化。
- 容器对配置文件的解析以及相关组件的初始化。
所以真正初始化拦截器的动作是由容器(XmlWebApplicationContext)来完成的,观察下图容器初始化过程。
上图展示的是,从 XmlWebApplicationContext 的 refresh 方法开始,到调用 MvcNamespaceHandler 解析拦截器配置的执行过程。
初始化过程涉及到多个组件间的调用,简要分析如下。
- XmlWebApplicationContext 中创建了 beanFactory,通过 beanFactory 获取到 XmlBeanDefinitionReader 实例,接着获取到配置文件的路径,使用 XmlBeanDefinitionReader 来解析配置文件。
- XmlBeanDefinitionReader 将配置文件转换为 Document 对象,并创建了 BeanDefinitionDocumentReader 对象来解析 Document 对象。
- BeanDefinitionDocumentReader 遍历 Document 中的所有节点,拦截器的配置节点不属于默认的配置节点,将创建对应的 NamespaceHandler 来解析,即上文提到的 MvcNamespaceHandler。
- 创建 MvcNamespaceHandler 后,首先会调用对应的 init 方法注册所有的 BeanDefinitionParser,接着会遍历注册的 BeanDefinitionParser,找到合适的 BeanDefinitionParser 对传入的节点进行解析。
下面开始对 MvcNamespaceHandler 的分析,观察 MvcNamespaceHandler 的继承关系。
MvcNamespaceHandler 继承了 NamespaceHandlerSupport 抽象类并实现了 NamespaceHandler 接口,上文的分析提到,创建 MvcNamespaceHandler 后首先会调用 init 方法进行初始化,init 方法在 NamespaceHandler 中定义并在 MvcNamespaceHandler 中实现,截取代码如下。
public class MvcNamespaceHandler extends NamespaceHandlerSupport {
@Override
public void init() {
//略
registerBeanDefinitionParser("interceptors", new InterceptorsBeanDefinitionParser());
//略
}
}
init 方法中,调用了 registerBeanDefinitionParser 方法注册了相关的解析器,拦截器对应的解析器是 InterceptorsBeanDefinitionParser。
private final Map<String, BeanDefinitionParser> parsers = new HashMap<String, BeanDefinitionParser>();
protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) {
this.parsers.put(elementName, parser);
}
registerBeanDefinitionParser 方法在 NamespaceHandlerSupport 中实现,如上所示,向 parsers 这个 Map 中 put 进了对应的 Key 与 Value。
初始化完成之后,便会开始遍历配置文件中的节点,当扫描到 <mvc:interceptors> 节点时,便会调用 MvcNamespaceHandler 中的 parse 方法进行解析。
parse 方法在 NamespaceHandler 中定义,并在 NamespaceHandlerSupport 中实现。
public BeanDefinition parse(Element element, ParserContext parserContext) {
return findParserForElement(element, parserContext).parse(element, parserContext);
}
private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
String localName = parserContext.getDelegate().getLocalName(element);
BeanDefinitionParser parser = this.parsers.get(localName);
if (parser == null) {
parserContext.getReaderContext().fatal(
"Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
}
return parser;
}
如上所示,获取到节点的 localName,即 interceptors,接着从 parsers 获取对应的解析器,调用解析器的 parse 方法进行解析,即上文注册的 InterceptorsBeanDefinitionParser,具体代码如下所示。
public BeanDefinition parse(Element element, ParserContext parserContext) {
CompositeComponentDefinition compDefinition = new
CompositeComponentDefinition(element.getTagName(),
parserContext.extractSource(element));
parserContext.pushContainingComponent(compDefinition);
RuntimeBeanReference pathMatcherRef = null;
if (element.hasAttribute("path-matcher")) {
pathMatcherRef = new RuntimeBeanReference(element.getAttribute("path-matcher"));
}
List<Element> interceptors = DomUtils.getChildElementsByTagName(element, "bean", "ref", "interceptor");
for (Element interceptor : interceptors) {
RootBeanDefinition mappedInterceptorDef = new
RootBeanDefinition(MappedInterceptor.class);
mappedInterceptorDef.setSource(parserContext.extractSource(interceptor));
mappedInterceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
ManagedList<String> includePatterns = null;
ManagedList<String> excludePatterns = null;
Object interceptorBean;
if ("interceptor".equals(interceptor.getLocalName())) {
includePatterns = getIncludePatterns(interceptor, "mapping");
excludePatterns = getIncludePatterns(interceptor, "exclude-mapping");
Element beanElem = DomUtils.getChildElementsByTagName(interceptor, "bean", "ref").get(0);
interceptorBean = parserContext.getDelegate().parsePropertySubElement(beanElem, null);
}
else {
interceptorBean = parserContext.getDelegate().parsePropertySubElement(interceptor, null);
}
mappedInterceptorDef.getConstructorArgumentValues().
addIndexedArgumentValue(0, includePatterns);
mappedInterceptorDef.getConstructorArgumentValues().
addIndexedArgumentValue(1, excludePatterns);
mappedInterceptorDef.getConstructorArgumentValues().
addIndexedArgumentValue(2, interceptorBean);
if (pathMatcherRef != null) {
mappedInterceptorDef.getPropertyValues().add("pathMatcher", pathMatcherRef);
}
String beanName = parserContext.getReaderContext().registerWithGeneratedName(mappedInterceptorDef);
parserContext.registerComponent(new BeanComponentDefinition(mappedInterceptorDef, beanName));
}
parserContext.popAndRegisterContainingComponent();
return null;
}
传入的 element 即为 mvc:interceptors 节点对象,上述代码主要做了如下一些事情:
- 获取 mvc:interceptors 节点下的 bean、ref、interceptor 节点数组。
- 遍历节点数组,创建 MappedInterceptor 对象。若节点对象为 interceptor,将mapping、exclude-mapping、bean 或 ref 节点信息设置到 MappedInterceptor中。若节点对象为 bean 或 ref,则仅设置 bean 或 ref 节点信息,mapping 与 exclude-mapping 则设置为空,即会拦截所有请求。
- 将 MappedInterceptor 对象注册到容器中,以便后续使用。
至此,我们已经根据配置文件中的拦截器配置,生成了 MappedInterceptor 拦截器对象。
SpringMVC 接收到 Web 请求时,会由 HandlerMapping 生成对应的拦截器链,为了便于处理,SpringMVC 还会将 MappedInterceptor 对象加载到 HandlerMapping 的成员变量中,这一步的加载稍微隐藏得比较深,可以观察下面的流程图。
handlerMaping初始化
FrameworkServlet 在 configureAndRefreshWebApplicationContext 方法中,向容器中注册了一个监听器(ContextRefreshListener),正是这一步操作触发了上述流程,简要分析如下:
- AbstractApplicationContext 在 refresh 方法中加载完拦截器后,会调用 finishRefresh 方法,触发 ContextRefreshListener 的执行。
- ContextRefreshListener 触发 FrameworkServlet 的 onApplicationEvent 方法,把初始化的控制权交给 DispatcherServlet。
- DispatcherServlet 触发 HandlerMapping 进行初始化,HandlerMapping 默认实现之一是 RequestMappingHandlerMapping,这里以此为例进行说明。
- 观察 RequestMappingHandlerMapping 的继承关系,实现了 ApplicationContextAware 接口,Spring 在初始化 ApplicationContextAware 实现类的时候,会调用其 setApplicationContext 方法。
- setApplicationContext 方法由 ApplicationObjectSupport 实现,最终会调用 AbstractHandlerMapping 的 initApplicationContext 方法,完成拦截器的加载,相关代码展示如下。
protected void initApplicationContext() throws BeansException {
extendInterceptors(this.interceptors);
detectMappedInterceptors(this.adaptedInterceptors);
initInterceptors();
}
protected void detectMappedInterceptors(List<HandlerInterceptor> mappedInterceptors) {
mappedInterceptors.addAll(
BeanFactoryUtils.beansOfTypeIncludingAncestors(
obtainApplicationContext(), MappedInterceptor.class, true, false).values());
}
detectMappedInterceptors 方法探测 ApplicationContext 中已经解析过的 MappedInterceptor,全部存放到 AbstractHandlerMapping 的 adaptedInterceptors 属性上,extendInterceptors 方法留给子类扩展,目前还没有子类实现。
至此,拦截器的相关信息已经加载完成,后续有请求进来的时候就可以直接进行匹配。
4.3、拦截器链生成
当请求进来的时候会被匹配的拦截器拦截,多个拦截器组成一个拦截器链,并按照我们定义的顺序执行。
SpringMVC 中的所有请求都需要经过 DispatcherServlet 进行分发,拦截器链也是在其中生成的,当请求被 DispatcherServlet 拦截后,会在 doDispatch 方法中进行分发,下面截取部分关键代码。
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);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
//略
}
catch (Exception ex) {
//略
}
}
catch (Exception ex) {
//略
}
}
每个请求需生成对应的 HandlerExecutionChain,HandlerExecutionChain 中包含了目标 service 和对应的拦截器链,从上面的源码可以看到,getHandler 会返回一个 HandlerExecutionChain。
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping hm : this.handlerMappings) {
if (logger.isTraceEnabled()) {
logger.trace("Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
}
HandlerExecutionChain handler = hm.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
getHandler 方法中会遍历找到所有 HandlerMapping,调用其中的 getHandler 方法,直到找到一个合适的。其中就包括上文提到的 RequestMappingHandlerMapping。
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
Object handler = getHandlerInternal(request);
//略
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
//略
return executionChain;
}
getHandler 的实现在 AbstractHandlerMapping 类中,找到对应请求的 handler 后,便会调用 getHandlerExecutionChain 方法获取 HandlerExecutionChain,这里就是生成拦截器链的关键代码。
protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
if (interceptor instanceof MappedInterceptor) {
MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
chain.addInterceptor(mappedInterceptor.getInterceptor());
}
}
else {
chain.addInterceptor(interceptor);
}
}
return chain;
}
getHandlerExecutionChain 方法中,遍历 AbstarctHandlerMapping 的 adaptedInterceptors 属性,使用默认的 pathMatcher,判断当前的请求是否符合拦截条件,若符合则将 mappedInterceptor 放进 HandlerExecutionChain 中。
至此一个 HandlerExecutionChain 便构建好了,包含一个 handler 和我们想要的拦截器链。
4.4、拦截器链执行
获取到拦截器链之后,接下来就是按照一定的顺序执行其中的方法,回到上一步分析开始的 doDispatch 方法中,可以看到拦截器链的具体执行过程。
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
//略
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
//略
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
//略
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
//略
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
//略
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
//略
}
}
在本文开始的代码示例中,我们实现了拦截器中的三个方法,这三个方法的具体执行时机和顺序分析如下。
(1)preHandle
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
HandlerInterceptor[] interceptors = getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
for (int i = 0; i < interceptors.length; i++) {
HandlerInterceptor interceptor = interceptors[i];
if (!interceptor.preHandle(request, response, this.handler)) {
triggerAfterCompletion(request, response, null);
return false;
}
this.interceptorIndex = i;
}
}
return true;
}
preHandle 的执行顺序在 HandlerExecutionChain 中控制,正序遍历拦截器链,执行拦截器的 preHandle 方法,并会记录下当前执行的下标,若 preHandle 返回 false 则会执行 triggerAfterCompletion 方法,若异常则抛出。
(2)postHandle
mappedHandler.applyPostHandle(processedRequest, response, mv);
ha.handle 执行目标 Controller 的方法,执行完后就会执行 applyPostHandle。
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
throws Exception {
HandlerInterceptor[] interceptors = getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
for (int i = interceptors.length - 1; i >= 0; i--) {
HandlerInterceptor interceptor = interceptors[i];
interceptor.postHandle(request, response, this.handler, mv);
}
}
}
postHandle 的执行顺序在 HandlerExecutionChain 中控制,倒序遍历拦截器链,并执行拦截器的 postHandle 方法,若异常则抛出。
(3)afterCompletion
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
在末尾的 processDispatchResult 方法和异常处理的 triggerAfterCompletion 方法中,都会调用 HandlerExecutionChain的triggerAfterCompletion 方法。
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex)
throws Exception {
HandlerInterceptor[] interceptors = getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
for (int i = this.interceptorIndex; i >= 0; i--) {
HandlerInterceptor interceptor = interceptors[i];
try {
interceptor.afterCompletion(request, response, this.handler, ex);
}
catch (Throwable ex2) {
logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
}
}
}
}
triggerAfterCompletion 方法从指定下标 interceptorIndex 开始,倒序遍历拦截器链,并执行拦截器的 afterCompletion 方法,若异常则记录到日志中,interceptorIndex 的下标是在 preHandle 中设置的,这里就是上文中提到的当 preHandle 返回 false,仍会执行之前拦截器的 afterCompletion 方法的原因。
至此,拦截器链的执行便完成了。
0002 - Spring MVC 拦截器源码简析:拦截器加载与执行的更多相关文章
- SpringMVC学习(一)——概念、流程图、源码简析
学习资料:开涛的<跟我学SpringMVC.pdf> 众所周知,springMVC是比较常用的web框架,通常整合spring使用.这里抛开spring,单纯的对springMVC做一下总 ...
- 【Spring源码分析】非懒加载的单例Bean初始化前后的一些操作
前言 之前两篇文章[Spring源码分析]非懒加载的单例Bean初始化过程(上篇)和[Spring源码分析]非懒加载的单例Bean初始化过程(下篇)比较详细地分析了非懒加载的单例Bean的初始化过程, ...
- Spring源码分析:非懒加载的单例Bean初始化前后的一些操作
之前两篇文章Spring源码分析:非懒加载的单例Bean初始化过程(上)和Spring源码分析:非懒加载的单例Bean初始化过程(下)比较详细地分析了非懒加载的单例Bean的初始化过程,整个流程始于A ...
- Flink源码阅读(一)——Flink on Yarn的Per-job模式源码简析
一.前言 个人感觉学习Flink其实最不应该错过的博文是Flink社区的博文系列,里面的文章是不会让人失望的.强烈安利:https://ververica.cn/developers-resource ...
- 【Spring源码分析】非懒加载的单例Bean初始化过程(下篇)
doCreateBean方法 上文[Spring源码分析]非懒加载的单例Bean初始化过程(上篇),分析了单例的Bean初始化流程,并跟踪代码进入了主流程,看到了Bean是如何被实例化出来的.先贴一下 ...
- Spring源码分析:非懒加载的单例Bean初始化过程(下)
上文Spring源码分析:非懒加载的单例Bean初始化过程(上),分析了单例的Bean初始化流程,并跟踪代码进入了主流程,看到了Bean是如何被实例化出来的.先贴一下AbstractAutowireC ...
- django-jwt token校验源码简析
一. jwt token校验源码简析 1.1 前言 之前使用jwt签发了token,里面的头部包含了加密的方式.是否有签名等,而载荷中包含用户名.用户主键.过期时间等信息,最后的签名还使用了摘要算法进 ...
- wemall app商城源码Android之ListView异步加载网络图片(优化缓存机制)
wemall-mobile是基于WeMall的android app商城,只需要在原商城目录下上传接口文件即可完成服务端的配置,客户端可定制修改.本文分享wemall app商城源码Android之L ...
- OpenStack之Glance源码简析
Glance简介 OpenStack镜像服务器是一套虚拟机镜像发现.注册.检索. glance架构图: Glance源码结构: glance/api:主要负责接收响应镜像管理命令的Restful请求, ...
随机推荐
- nginx常用参数设置
1)隐藏nginx header 版本号 使用curl -I http://www.10.0.3.46 会发现server那里显示版本号 在nginx.conf的http里添加参数server_tok ...
- css动画和js动画的差异
代码复杂度,js 动画代码相对复杂一些 动画运行时,对动画的控制程度上,js 能够让动画,暂停,取消,终止,css动画不能添加事件 动画性能看,js 动画多了一个js 解析的过程,性能不如 css 动 ...
- java BitSet2
15. int nextClearBit(int startIndex)返回第一个设置为 false 的位的索引,这发生在指定的起始索引或之后的索引上. 16. int nextSetBit(int ...
- vsphere和vmware快照的不足之处
当快照创建时虚拟机执行一个读操作,hypervisor会检查快照VMDK,查看是否有被读取的区块存在.如果有,则从快照中为虚拟机提供这个区块,如果没有,虚拟机还需要去读取基础VMDK.如果只有一个快照 ...
- c# 一些细节
1.动态对象和匿名对象偶然看到一个语法,觉得特别方便然后频繁使用,但是没有深究,直到今天忽然发现我潜意思中对它的认知居然是错误的. var data=new { State=1,Message=&qu ...
- Shell中怎么获取当前日期和时间
转载自:https://zhidao.baidu.com/question/627912810044012524.html 获得当天的日期 [root@master ~]# date +%Y-%m-% ...
- supervisor的安装部署及集群管理
supervisor的安装部署及集群管理 supervisor官网:http://www.supervisord.org/ 参考链接: http://blog.csdn.net/xyang81/art ...
- php统计中英文混合的文章字数
function ccStrLen($str) #计算中英文混合字符串的长度 { $ccLen=0; $ascLen=strlen($str); $ind=0; $hasCC=ereg("[ ...
- .net(C#)常见面试题
1. 简述 private. protected. public. internal 修饰符的访问权限. 答 . private : 私有成员, 在类的内部才可以访问. protected : 保护成 ...
- linux 查看系统磁盘、内存大小
1.磁盘 df -h cat /proc/partitions 2.内存 cat /proc/meminfo cat /proc/meminfo