1、spring mvc 应用
1、在web.xml中配置
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
2、拦截器的配置
<!-- 登录拦截器 -->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/myMobileHome/**" />
<mvc:mapping path="/myMobileOrder/**" />
<bean class="*.MobileLoginInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>
public class MobileLoginInterceptor implements HandlerInterceptor {
/**
* handler :处理器(控制器) = controller对象
* preHandle :处理请求的(再调用controller对象方法之前执行)
* :对请求进行放行(继续执行进入到方法)
* :对请求过滤
* 返回值:true = 放行
* false = 过滤
*
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("执行MobileLoginInterceptor 的preHandle方法");
return true; //放行
}
/**
* postHandle 调用控制器方法之后执行的方法
* :处理响应的
*/
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("执行MobileLoginInterceptor 的postHandle方法");
}
/**
* afterCompletion :
* 整个请求处理完毕,在视图渲染完毕时回调,一般用于资源的清理或性能的统计
* 在多个拦截器调用的过程中,
* afterCompletion是否取决于preHandler是否=true
*
*/
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("执行MobileLoginInterceptor 的afterCompletion方法");
}
}
3、spring mvc 常用注解
@RequestMapping注解为控制器指定可以处理哪些 URL 请求
@Resource和@Autowired 都是做bean的注入时使用
@RequestBody该注解用于读取Request请求的body部分数据,使用系统默认配置的HttpMessageConverter进行解析,然后把相应的数据绑定到要返回的对象上再把HttpMessageConverter返回的对象数据绑定到 controller中方法的参数上 @ResponseBody该注解用于将Controller的方法返回的对象,通过适HttpMessageConverter转换为指定格式后,写入到Response对象的body数据区
@ModelAttribute 在方法定义上使Spring MVC在调用目标处理方法前,会先逐个调用在方法级上标注了, 的方法 在方法的入参前使用
@ModelAttribute 注解:可以从隐含对象中获取隐含的模型数据中获取对象,再将请求参数–绑定到对象中,再传入入参将方法入参对象添加到模型中
@RequestParam在处理方法入参处使用 可以把请求参 数传递给请求方法
@PathVariable 绑定 URL 占位符到入参
@ExceptionHandler 注解到方法上,出现异常时会执行该方法
@ControllerAdvice 使一个Contoller成为全局的异常处理类,类中用
@ExceptionHandler方法注解的方法可以处理所有Controller发生的异常
4、spring mvc统一异常处理可以看下这个
2、WebApplicationContext 初始化过程
1、spring中ContextloaderListener 实现 ServletContextListener接口,在web.xml文件中也有配置,在tomcat启动时会先调用ServletContextListener的contextInitialized方法,ContextloaderListener 中的contextInitialized中代码做了什么
1、webapplicationcontext存在性的验证
2、创建webapplicationcontext实例
3、将实例记录在servletContext中
4、映射当前的类加载器与创建的实例到全局变量currentContextPerThread中
2、tomcat启动之后页面会通过请求去调用controller。spring mvc中的controller的调用其实也是通过servlet去实现的。【servlet的生命周期在文末】而servlet在初始化的时候会调用init方法进行初始化。在web.xml中只配置了一个servlet的类所以要执行一次init,init做了什么了,首先我们通过web.xml中配置的DispatcherServlet这个servlet,然后在其父类HttpServletBean中找到了init方法
@Override
public final void init() throws ServletException {
//当dispatcherServlet初始化的时候
if (logger.isDebugEnabled()) {
logger.debug("Initializing servlet '" + getServletName() + "'");
}
//解析init-param并封装至pvs中
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
//将当前的这个servlet类转化未一个beanwrapper,从而能够以spring的方式来对init-param的只进行注入
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
//注册自定义属性编辑器,一旦遇到Resource类型的属性将会使用ResourceEditor进行解析
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
//空实现留给子类覆盖
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}
//初始化这个servletBean
initServletBean();
if (logger.isDebugEnabled()) {
logger.debug("Servlet '" + getServletName() + "' configured successfully");
}
}
1、封装及验证初始化参数
2、将当前servlet实例转换为BeanWrapper
3、注册相对于rerource的属性编辑器
4、属性的注入
5、servletBean的初始化 initServletBean();DispatcherServlet父类的FrameworkServlet覆盖了HttpServletBean中的initServletBean方法
@Override
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
if (this.logger.isInfoEnabled()) {
this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");
}
long startTime = System.currentTimeMillis();
try {
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
catch (ServletException ex) {
this.logger.error("Context initialization failed", ex);
throw ex;
}
catch (RuntimeException ex) {
this.logger.error("Context initialization failed", ex);
throw ex;
}
if (this.logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +
elapsedTime + " ms");
}
}
从上面的代码我们可以看出实际上只是调用了 initWebApplicationContext(); 方法这个方法的主要作用时创建或者获取webApplicationContext类的对象
protected WebApplicationContext initWebApplicationContext() {
//从servletContext中获取webApplicationContext
WebApplicationContext rootContext =WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
//context实例在构造函数中被注入
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) {
//根据contextAttribute属性加载webApplicationContext
wac = findWebApplicationContext();
}
if (wac == null) {
// No context instance is defined for this servlet -> create a local one
wac = createWebApplicationContext(rootContext);
}
//
if (!this.refreshEventReceived) {
//上下文不是具有刷新功能的ConfigurableApplicationContext
//在构建时注入的支持或上下文已经
//刷新->在此处手动触发初始刷新。
onRefresh(wac);
}
if (this.publishContext) {
// Publish the context as a servlet context attribute.
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
"' as ServletContext attribute with name [" + attrName + "]");
}
}
return wac;
}
1、通过构造函数的注入进行初始化WebApplicationContext 【WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());】
2、通过contextAttribute进行初始化WebApplicationContext【wac = findWebApplicationContext();】
3、重新创建WebApplicationContext【wac = createWebApplicationContext(rootContext);】无论是通过构造函数注入的还是单独创建,都会掉用 configureAndRefreshWebApplicationContext方法对已经创建的webApplicationContext实例进行配置及刷新。
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// The application context id is still set to its original default value
// -> assign a more useful id based on available information
if (this.contextId != null) {
wac.setId(this.contextId);
}
else {
// Generate default id...
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()));
// The wac environment's #initPropertySources will be called in any case when the context
// is refreshed; do it eagerly here to ensure servlet property sources are in place for
// use in any post-processing or initialization that occurs below prior to #refresh
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
}
postProcessWebApplicationContext(wac);
applyInitializers(wac);
wac.refresh();
}
看代码我们可以看到最后又到了 wac.refresh(); 【AbstractApplicationContext.refresh】,这个方法是用来初始化spring的ioc容器并,将容器单例的beanFactory进行初始化,在执行refresh的时候会调用finishRefresh();【作用:初始化容器的生命周期事件处理器,并发布容器的生命周期事件】方法去发布事件,然后再将bean注入到对应的对象中,即在bean依赖注入成功之后,在finishRefresh发布完事件之后DispatcherServlet类监听到事件发布了会调用 onApplicationEvent 这个方法
主要作用是初始springmvc的一些组件,好到这里 webapplicationcontext初始化完毕,即 DispatcherServlet的init方法执行完了
spring的事件是个什么的可以看博主的关于spring事件的博客
org.springframework.web.servlet.FrameworkServlet#onApplicationEvent
public void onApplicationEvent(ContextRefreshedEvent event) {
this.refreshEventReceived = true;
onRefresh(event.getApplicationContext());
}
org.springframework.web.servlet.DispatcherServlet#onRefresh/initStrategies
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
MultipartResolver:解析多部分请求,以支持从HTML表单上传文件。
LocaleResolver:解决客户正在使用的区域设置以及可能的时区,以便能够提供国际化视野。
ThemeResolver:解决Web应用程序可以使用的主题,例如提供个性化布局。
HandlerMapping:用于handlers映射请求和一系列的对于拦截器的前处理和后处理,大部分用@Controller注解。
HandlerAdapter:帮助DispatcherServlet处理映射请求处理程序的适配器,而不用考虑实际调用的是 哪个处理程序。
HandlerExceptionResolver:处理映射异常。
initRequestToViewNameTranslator:用于处理没有返回视图名时的情况下如何得到一个默认的视图名。
ViewResolver:根据实际配置解析实际的View类型。
FlashMapManager:存储并检索可用于将一个请求属性传递到另一个请求的input和output的FlashMap,通常用于重定向。
到这里webapplicationcontext初始化完毕
3、一个请求的的处理过程【DispatcherServlet的逻辑处理】
1、springmvc 是通过servlet实现的servlet中,前端的请求都是到doPost、doGet、doDelete、doPut、doDelete、doTrace、DispatcherServlet继承了FrameworkServlet类,FrameworkServlet继承了HttpServletBean抽象类,HttpServletBean继承了HttpServlet抽象类,FrameworkServlet类中实现了doPost、doGet等内部都是调用的 processRequest方法,内部又会去调用doService,然后其内部又会去调用doDispatcher放法,其内部就是具体处理当前这个请求的具体方法了
2、doDispatcher处理请求分为根据《spring源码深度解析》书,将请求的处理逻辑分为了9步
1 MultipartContent类型的request处理
2 根据request信息寻找对应的Handler
3 根据当前Handler寻找对应的HandlerAdapter
4 缓存处理
5 HandlerInterceptor的处理
6 逻辑处理
7 异常视图的处理
8 根据视图跳转页面
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 {
// 1.检查是否是文件上传的请求
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
// 2.取得处理当前请求的controller,这里也称为hanlder,处理器,
// 第一个步骤的意义就在这里体现了.这里并不是直接返回controller,
// 而是返回的HandlerExecutionChain请求处理器链对象,
// 该对象封装了handler和interceptors.
mappedHandler = getHandler(processedRequest);
// 如果handler为空,则返回404
if (mappedHandler == null) {
//如果没有找到对应的请求处理链则通过response反馈错误信息
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
//3. 获取处理request的处理器适配器handler adapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
// 处理 last-modified 请求头
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (logger.isDebugEnabled()) {
logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
//拦截器中的prehandler方法的调用
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
// 4.实际的处理器处理请求,返回结果视图对象
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
// 结果视图对象的处理
applyDefaultViewName(processedRequest, mv);
//应用所有拦截器的postHandle方法
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", 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 {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
// 请求成功响应之后的方法 mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
下面是doDispatch 中的具体步骤
1 MultipartContent类型的request处理
对于请求的处理如果是multipart的处理,即文件类型的请求,会将request转换为MyltipartHttpServletRequest类型的request
processedRequest = checkMultipart(request);
2、根据request信息寻找对应的Handler【包含当前请求对应的方法和当前请求需要被哪些拦截器进行拦截】
mappedHandler = getHandler(processedRequest);
对应下方的 getHandler方法的代码
3、根据当前Handler寻找对应的HandlerAdapter
通过HandlerAdapter得 子类的supports去做校验当前handler是否适合这个HandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
关于HandlerAdapter是干什么的博主有博客写关于handlerAdapter是干什么的博客
4、缓存处理
last-Modified缓存机制:第一次请求和第二次请求时间一样只返回304的状态码
304(not changed) 没有改变
5、拦截器的处理
//拦截器中的prehandler方法的调用
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
6、执行链接对应的方法
7、拦截器的后置处理和结果视图处理
// 结果视图对象的处理
applyDefaultViewName(processedRequest, mv);
//应用所有拦截器的postHandle方法
mappedHandler.applyPostHandle(processedRequest, response, mv);
8、根据视图跳转到页面
//处理最后的结果,渲染之类的都在这里
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
这个方法中主要有两步
1、视图名称解析
2、处理页面跳转
View接口的子类的实现调用不同 的renderMergedOutputModel方法
org.springframework.web.servlet.view.InternalResourceView 实现页面的跳转
renderMergedOutputModel
//org.springframework.web.servlet.view.json.AbstractJackson2View 实现返回json[不确定是不是这个类]
renderMergedOutputModel
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
//根据request获取对应的handler
Object handler = getHandlerInternal(request);
if (handler == null) {
//如果没有对应的request的handler则使用默认的handler
handler = getDefaultHandler();
}
//如果没有默认的无法处理返回null
if (handler == null) {
return null;
}
// Bean name or resolved handler?
if (handler instanceof String) {
String handlerName = (String) handler;
handler = obtainApplicationContext().getBean(handlerName);
}
//获取这个请求需要被哪些拦截器拦截
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
if (CorsUtils.isCorsRequest(request)) {
CorsConfiguration globalConfig = this.globalCorsConfigSource.getCorsConfiguration(request);
CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
}
return executionChain;
}
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
//如果有异常就走统一异常处理拦截器
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
//渲染这个页面
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +
"': assuming HandlerAdapter completed request handling");
}
}
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Determine locale for request and apply it to the response.
Locale locale =
(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
response.setLocale(locale);
View view;
String viewName = mv.getViewName();
if (viewName != null) {
// We need to resolve the view name.
//解析视图名称
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
"' in servlet with name '" + getServletName() + "'");
}
}
else {
// No need to lookup: the ModelAndView object contains the actual View object.
view = mv.getView();
if (view == null) {
throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
"View object in servlet with name '" + getServletName() + "'");
}
}
// Delegate to the View object for rendering.
if (logger.isDebugEnabled()) {
logger.debug("Rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'");
}
try {
if (mv.getStatus() != null) {
response.setStatus(mv.getStatus().value());
}
view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Error rendering view [" + view + "] in DispatcherServlet with name '" +
getServletName() + "'", ex);
}
throw ex;
}
}
@Override
protected void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Expose the model object as request attributes.
exposeModelAsRequestAttributes(model, request);
// Expose helpers as request attributes, if any.
exposeHelpers(request);
// Determine the path for the request dispatcher.
String dispatcherPath = prepareForRendering(request, response);
// Obtain a RequestDispatcher for the target resource (typically a JSP).
RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
if (rd == null) {
throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
"]: Check that the corresponding file exists within your web application archive!");
}
// If already included or response already committed, perform include, else forward.
if (useInclude(request, response)) {
response.setContentType(getContentType());
if (logger.isDebugEnabled()) {
logger.debug("Including resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
}
rd.include(request, response);
}
else {
// Note: The forwarded resource is supposed to determine the content type itself.
if (logger.isDebugEnabled()) {
logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
}
rd.forward(request, response);
}
}
servlet是什么
是一个java编写的程序,此程序是基于http协议的,在服务器端运行的是按照servlet规范编写的一个java类。主要是处理客户端的请求,并将其结果发送到客户端。servlet的生命周期是由servlet容器控制的,分为3个阶段:初始化,运行和销毁
1、初始化阶段
- servlet容器加载servlet类,把servlet类的class文的数据读到内存中
- servlet 容器创建一个servletconfig对象。servletconfig对象包含了servlet的初始化配置信息。
- servlet容器创建一个servlet对象。
- servlet容器调用servlet对象的init方法进行初始化。
2、运行阶段
当servlet容器结束到一个请求时,servlet容器会针对这个请求创建servletRequest和servletResponse对象,然后调用service方法。并把这两个参数传递给service方法。service方法通过servletRequest对象获得请求的信息。并处理该请求。再通过servletResponser对象生成这个请求的响应结果。然后销毁servletRequest和servletResponse对象。我们不管这个请求1时post提交还是get提交的,最终这个请求都会由service方法来处理。
3、销毁阶段
当web应用被终止时,servlet容器会先调用servlet对象的destory方法,然后销毁servlet对象,同时也会销毁与servlet对象相关联的servletConfig对象。我们可以在destory方法的实现中,释放servlet所占用的资源,如果关闭数据库连接,关闭文件输入流等。
Last-Modified
在浏览器第一次请求某一个URL时,服务器端的返回状态会是200,内容是客户端请求的资 源,同时有一个Last-Modified的属性标记此文件在服务器端最后被修改的时间。Last-Modified格式类似这样:
Last-Modified : Fri , 12 May 2006 18:53:33 GMT
客户端第二次请求此URL时,根据HTTP协议的规定,浏览器会向服务器传送If-Modified-Since 报头,询问该时间之后文件是否有被修改过:
If-Modified-Since : Fri , 12 May 2006 18:53:33 GMT
如果服务器端的资源没有变化,则自动返回 HTTP 304(Not Changed.)状态码,内容为空,这样就节省了传输数据量。当服务器端代码发生改变或者重启服务器时,则重新发出资源,返回和第一次请求时类似。从而保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源。