年前实训的时候写了个Javaweb项目,遇到的一些问题,现在总结一下方便以后查看。
登录成功后跳转到后台管理的页面,点了一下刷新发现提示我需要重新提交表单,因为这个Servlet是登录页面post登录信息跳转的,出现这个也很正常。
点击继续后发现页面显示412,后台控制台却没有报错。多刷新几次后,发现它是刷新间隔会报412,也就是一次成功进入后台,一次报412。
百度412的具体内容,在 HTTP 协议中,响应状态码 412 Precondition Failed(先决条件失败)表示客户端错误,意味着对于目标资源的访问请求被拒绝。这通常发生于采用除 GET 和 HEAD 之外的方法进行条件请求时,由首部字段 If-Unmodified-Since 或 If-None-Match 规定的先决条件不成立的情况下。这时候,请求的操作——通常是上传或修改文件——无法执行,从而返回该错误状态码。
不太看得懂。
于是我研究了一下两次有什么区别。
(解释原因的部分很长,只想直接解决的可以拉到最后)
跟踪了网页请求,发现在成功进入后台的时候,服务器的响应头中会返回
ETag:W/"216-1545291952524"
Last-Modified:Thu, 20 Dec 2018 07:45:52 GMT
这两条数据。
而在第二次刷新的时候,这两条数据会在请求消息头中被传给服务器
If-Modified-Since:Thu, 20 Dec 2018 07:45:52 GMT
If-None-Match:W/"216-1545291952524"
然后我研究了一下ETag,Last-Modified的具体含义
ETag
数据签名
典型做法:对资源内容进行hash计算
配合If-Match或者Id-Non-Match使用
对比资源的签名判断是否使用缓存
Last-Modified
上次修改时间
配合If-Modified-Since或者If-Unmodified-Since使用
对比上次修改时间验证资源是否需要更新
在浏览器和服务端设置都允许缓存策略的前提下(服务端响应头的Cache-Control不为no-store,即禁止任何缓存),如果某个请求的响应头设置了Last-Modified或Etag:
第一次请求:浏览器会记住响应头的Last-Modified和Etag;
第二次及以后请求:浏览器会携带保存的Last-Modified和Etag分别作为If-Modified-Since和If-None-Match放入请求头中携带过去,以此到服务端验证此次请求的资源是否过期或更新;服务端进行判断,若过期或更新,则返回新的资源,否则返回空即可,节省服务端消耗。
这是apach对于静态资源(含js,图片,css等)部分的缓存,用于加速并减轻后台实际web服务器的压力。
最先有的是last_modified,它的工作方式就是上述介绍的,但缺点是只能精确到秒级别。也就是说当你在一秒中修改资源两次,而客户端拿到的是第一次修改,那之后就算客户端第二次再次请求也不会拿到最新的资源。
而etag的出现正是为了解决last_modified的秒级问题,于http 1.1被提出。
tomcat对于静态数据作了缓存。
接着我们分析tomcat对于这部分静态缓存的判断处理,这部分逻辑是写在DefaultServlet类中,
我们可以在doGet方法中进入ServiceContext方法中找到以下源码:
// Check if the conditions specified in the optional If headers are
// satisfied.
if (cacheEntry.context == null) {
// Checking If headers
boolean included =
(request.getAttribute(Globals.INCLUDE_CONTEXT_PATH_ATTR) != null);
if (!included
&& !checkIfHeaders(request, response, cacheEntry.attributes)) { //这句判断是否需要返回整个资源请求
return;
}
}
上面源码的 if (!included
&& !checkIfHeaders(request, response, cacheEntry.attributes))
用于判断是否需要返回整个资源,如果indcluded与checkIfHeaders方法返回的都是false,这时就直接返回,说明资源未修改,或者是缓存不支持的请求方式。
我们接着查看checkIfHeaders方法:
/**
* Check if the conditions specified in the optional If headers are
* satisfied.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
* @param resourceAttributes The resource information
* @return boolean true if the resource meets all the specified conditions,
* and false if any of the conditions is not satisfied, in which case
* request processing is stopped
*/
protected boolean checkIfHeaders(HttpServletRequest request,
HttpServletResponse response,
ResourceAttributes resourceAttributes)
throws IOException {
return checkIfMatch(request, response, resourceAttributes)
&& checkIfModifiedSince(request, response, resourceAttributes)
&& checkIfNoneMatch(request, response, resourceAttributes)
&& checkIfUnmodifiedSince(request, response, resourceAttributes);
}
可以看到tomcat只有当这四个属性全部返回true(也就是说全部认为资源已经改变)才会返回true,这样最终会将整个资源(最新修改过的)返回客户端。
在这里,我们从上面实际过程当中看到,浏览器第二次请求资源时在http请求header中放了
If-Modified-Since:Thu, 20 Dec 2018 07:45:52 GMT
If-None-Match:W/"216-1545291952524"
这两个属性。
因此我们查看
&& checkIfModifiedSince(request, response, resourceAttributes)
&& checkIfNoneMatch(request, response, resourceAttributes)
这两个方法
checkIfModifiedSince源码如下:
/**
* Check if the if-modified-since condition is satisfied.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
* @param resourceInfo File object
* @return boolean true if the resource meets the specified condition,
* and false if the condition is not satisfied, in which case request
* processing is stopped
*/
protected boolean checkIfModifiedSince(HttpServletRequest request,
HttpServletResponse response,
ResourceAttributes resourceAttributes) {
try {
long headerValue = request.getDateHeader("If-Modified-Since");
long lastModified = resourceAttributes.getLastModified();
if (headerValue != -1) {
// If an If-None-Match header has been specified, if modified since
// is ignored.
if ((request.getHeader("If-None-Match") == null)
&& (lastModified < headerValue + 1000)) {
// The entity has not been modified since the date
// specified by the client. This is not an error case.
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
response.setHeader("ETag", resourceAttributes.getETag());
return false;
}
}
} catch (IllegalArgumentException illegalArgument) {
return true;
}
return true;
}
源码中可以看到:
if ((request.getHeader("If-None-Match") == null)
&& (lastModified < headerValue + 1000)) {
这句话表明只有在客户端浏览器发送的请求头中不包含If-None-Match,IfModifiedSince才会生效。
我们接着看checkIfNoneMatch,源码如下:
/**
* Check if the if-none-match condition is satisfied.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
* @param resourceInfo File object
* @return boolean true if the resource meets the specified condition,
* and false if the condition is not satisfied, in which case request
* processing is stopped
*/
protected boolean checkIfNoneMatch(HttpServletRequest request,
HttpServletResponse response,
ResourceAttributes resourceAttributes)
throws IOException {
String eTag = resourceAttributes.getETag();
String headerValue = request.getHeader("If-None-Match");
if (headerValue != null) {
boolean conditionSatisfied = false;
if (!headerValue.equals("*")) {
StringTokenizer commaTokenizer =
new StringTokenizer(headerValue, ",");
while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
String currentToken = commaTokenizer.nextToken();
if (currentToken.trim().equals(eTag))
conditionSatisfied = true;
}
} else {
conditionSatisfied = true;
}
if (conditionSatisfied) {
// For GET and HEAD, we should respond with
// 304 Not Modified.
// For every other method, 412 Precondition Failed is sent
// back.
if ( ("GET".equals(request.getMethod()))
|| ("HEAD".equals(request.getMethod())) ) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
response.setHeader("ETag", eTag);
return false;
}
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
return false;
}
}
return true;
}
这里:
String eTag = resourceAttributes.getETag();
String headerValue = request.getHeader("If-None-Match");
这两句比较简单,就是分别从服务器缓存和http请求头中中取出etag。
接着判断这两个etag如果相等,则conditionSatisfied为true,会执行到以下语句:
if (conditionSatisfied) {
// For GET and HEAD, we should respond with
// 304 Not Modified.
// For every other method, 412 Precondition Failed is sent
// back.
if ( ("GET".equals(request.getMethod()))
|| ("HEAD".equals(request.getMethod())) ) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
response.setHeader("ETag", eTag);
return false;
}
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
return false;
}
这段语句中可以发现,如果资源未改变的情况下,并且请求方式为GET或者HEAD时,会返回304状态码。否则返回一个412状态码,同样不会返回资源内容。
如果上述最终
if ((request.getHeader("If-None-Match") == null)
&& (lastModified < headerValue + 1000))
条件不成立,即资源更新了或者是第一次请求,这里会读取当前请求资源文件,并最终放入http响应中。
==================================================================================
至此,终于知道为啥会报412了,tomcat直接设定,控制台自然也不会报错。错误的原因找到了,那应该怎么解决这个问题呢?
方法一:
清缓存,这样第二次访问的时候就不会有If-Modified-Since和If-None-Match被传过去了。
用户自然不会手动清除缓存,所以我们在后台加禁止浏览器缓存。
代码:
response.setHeader( "Cache-Control" , "no-store" );
response.setDateHeader( "Expires" , 0);
response.setHeader( "Pragma" , "no-cache" );
不过静止用户缓存这个方法大大增加了服务器的压力,所以可以选择第二种方法。
方法二:
在web.xml中添加error-page标签,按照错误号指定跳转。出现错误号为412则跳转到login.jsp(也可以改成别的页面,随自己的想法)
我顺便还把别的没捕捉的异常也一块处理了,反正有错就重新登录。
代码:
<error-page>
<error-code>412</error-code>
<location>/login.jsp</location>
</error-page>
<error-page>
<exception-type>java.lang.Exception</exception-type>
<location>/login.jsp</location>
</error-page>
现在我只知道这两种方法,如果还有别的办法可能会更新吧。
当然根据错误的原因,还可以将提交方式修改为get或者head,不过我这个登录跳转还是老老实实不考虑这个问题了。