【问题标题】:Changing data on GET page request (dealing with preloading requests)更改 GET 页面请求上的数据(处理预加载请求)
【发布时间】:2015-08-04 09:14:43
【问题描述】:

我有一个 portlet。当 portlet 加载时,在第一个视图被渲染之前,在某些情况下需要调用一个存储库来更改数据库中的数据。我不会更详细地说明为什么这是必要的,并且关于这是一个设计缺陷的答案没有帮助。我知道这是一个设计缺陷,但我仍然想找到以下问题的替代解决方案:

这种设置的问题在于,浏览器会发送预加载请求。例如,portlet 所在页面的 URL 是 /test-portlet。现在,当您在地址栏中键入它时,如果您的浏览器历史记录中有它,那么当浏览器向您建议时,它已经向该页面发送了一个 GET 请求。如果在第一个 GET 请求被解析之前按下回车,那么浏览器会发送一个新的 GET 请求。这意味着 portlet 接收到 2 个单独的请求,它开始并行处理这些请求。第一个数据库过程可能正常工作,但考虑到数据库过程的性质,第二个调用通常会出现异常。

从 Java 应用程序中处理上述问题的好方法是什么?

旁注:我正在使用 Spring MVC。

一个可能的控制器的简单示例:

@RequestMapping
public String index( Model model, RenderRequest request ){
    String username = dummyRepository.changeSomeData(request.getAttribute("userId"));
    model.add("userName", username);
    return "view";
}

我会对完全阻止第一次执行的解决方案感兴趣。例如,某种浏览器不会触发的从控制器到 POST 的重定向。但不确定是否可以实现。

【问题讨论】:

  • 您检查过请求标头吗?是否有任何标题表明您处于“预加载”场景?在这种情况下,您可以跳过任何需要跳过的内容。
  • 你能简单描述一下你的数据库程序是什么吗??
  • @MSIbrahim 我刚刚简化了场景。实际上有一个 REST API 调用可以操作 2 个数据库。我认为这无关紧要,因为我当然不想在较低级别上进行更改,因为这完全是 Web 层的问题。
  • 你需要什么顺序来运行你的项目你有任何想法,比如获取/重定向/发布类似的东西
  • @MSIbrahim 有点不清楚你的问题到底是什么。正如我在最初的问题中所述,我有一个可能的获取/重定向/发布(“例如某种从控制器到 POST 的重定向,浏览器不会触发。但不确定它是否可以实现。”),但我不确定如何在这种情况下实现它,或者即使它可能或有效。

标签: java spring spring-mvc portlet


【解决方案1】:

使用锁我认为你可以解决它,使第二个请求等待第一个完成然后处理它。我没有使用 java 中的锁的经验,但我发现了另一个关于 java 中的文件锁的堆栈交换帖子: How can I lock a file using java (if possible)

【讨论】:

  • 一个可能的想法,但第二个请求仍然会被处理。我会对完全阻止两种执行的解决方案更感兴趣。例如某种来自控制器的重定向,浏览器不会在预加载请求时触发。不确定是否可以实现。不过还是谢谢。
  • 这是http协议的问题,我认为这是不可能的。浏览器进行了很多“setcy”优化,这使得它们可以执行大量请求,这些请求对于它们请求的服务器来说都是困难的,并且在需要传输的数据量上并不聪明。让 chrome 等浏览器与您的网站一起工作的唯一方法是确保它可以处理它。但是看看连接类型的http协议,如果你能做到你所要求的,那就是你可以做到的。
【解决方案2】:

请参考this answer,它可能会帮助您检测和忽略一些预加载请求。但是,您还应该确保“最坏情况”有效,也许使用@jpeg 建议的锁定,但它可以像在某处使用synchronize 块一样简单。

【讨论】:

  • 不幸的是,有些情况下请求是相同的。我认为问题可能主要是 Chrome,如此处所述:link它可能会帮助您检测和忽略一些预加载请求。 - 如果有一个解决方案可以避免像 if(thisheader); if(thatheader); 等那样进行大量不同的检查,我会觉得很好仍然无法涵盖所有​​的可能性。
  • 一个灵魂可能是 JS Visibility API,但我仍然会等待后端问题的可能解决方案。我发现从客户端/浏览器端处理这个问题有点骇人听闻。
【解决方案3】:

由于我没有看到 chrome 添加了一些特定的标头(或者无论如何通知服务器有关预渲染状态),因此可能无法在服务器端检测到它......至少不能直接检测到它。但是,您可以在客户端模拟检测,然后将其与服务器调用结合起来。

请注意,您可以在客户端检测到预渲染:

if (document.webkitVisibilityState == 'prerender' ||  document.visibilityState ==    'prerender' || document.visibilityState[0] == 'prerender') {
    // prerendering takes place
}  

现在,您可以通过在浏览器处于预加载状态时显示警告框来中断客户端的预加载(或者您也可以在 javascript 中只出现一些错误,而不是使用 alert()):

 if (document.webkitVisibilityState == 'prerender' ||  document.visibilityState ==    'prerender' || document.visibilityState[0] == 'prerender') {
    alert('this is alert during prerendering..')
} 

现在,当 chrome 预渲染页面时,它会失败,因为 javascript 警报会阻止浏览器继续执行 javascript。

如果您输入 chrome:chrome://net-internals/#prerender,您可以跟踪 chrome 执行预渲染的时间和页面。如果是上述示例(在预渲染期间带有警告框),您可以在此处看到:

Link Rel Prerender (cross 域)http://some.url.which.is.preloaded Javascript 警报 2015-06-07 19:26:18.758

最终状态 - Javascript Alret 证明 chrome 无法预加载页面(我对此进行了测试)。

现在这如何解决您的问题?好吧,您可以将它与异步调用 (AJAX) 结合起来,并根据页面实际是否预渲染来加载一些内容(从另一个 url)。

考虑以下代码(可能由您的 portlet 在 url /test-portlet 下呈现):

<html>
  <body>


    <div id="content"></div>

<script>

if (document.webkitVisibilityState == 'prerender' ||  document.visibilityState ==    'prerender' || document.visibilityState[0] == 'prerender') {
    // when chrome uses prerendering we block the request with alert
    alert('this is alert during prerendering..');
} else {
    // in case no prerendering takes place we load the actual content asynchronously

    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (xhr.readyState == 4) {
             // when the content is loaded we place the html inside "content" div               
             document.getElementById('content').innerHTML = xhr.responseText; 

        }
    }
    xhr.open('GET', '/hidden-portlet', true); // we call the actual portlet
    xhr.send(null);

}

</script>

  </body>
</html>  

如您所见,/hidden-portlet 仅在浏览器正常加载页面(没有预加载)的情况下才会加载。 url /hidden-portlet(可以是另一个portlet/servlet)下的服务器端处理程序包含在预呈现期间不应执行的实际代码。所以执行的是 /hidden-portlet

dummyRepository.changeSomeData(request.getAttribute("userId"));

这个 portlet 还可以返回普通视图(呈现的 html),由于 /test-portlet 上的技巧:document.getElementById('content').innerHTML = xhr.responseText;.

所以总结地址/test-portlet 下的portlet 只返回带有触发实际portlet 的javascript 代码的html。

如果您有许多脆弱的 portlet,您可以更进一步,因此您可以使用 /test-portlet?actualUrl=hidden-portlet 之类的请求参数对您的 /test-portlet 进行参数化,以便从 url 获取实际 portlet 的地址(可以读取作为服务器端的请求参数)。在这种情况下,服务器将动态呈现应该加载的 url:

所以不是硬编码:

xhr.open('GET', '/hidden-portlet', true); 

你将拥有

xhr.open('GET', '/THIS_IS_DYNAMICALLY_REPLACED_EITHER_ON_SERVER_OR_CLIENT_SIDE_WITH_THE_ADDRES_FROM_URL', true); 

【讨论】:

  • 感谢您的意见。正如我在另一个答案的 cmets 中所述,我知道可以从客户端处理。看起来这可能是一个很好的问题在一周内没有找到任何明确的答案(我仍然会说它是hackish)。我真的很抱歉,但从这个问题的角度来看,我不能接受答案,因为这个问题期待来自服务器端的解决方案 - 可能与否。您已经非常彻底(尽管异步 portlet 加载从来都不是问题 :)),但我能做的最多的是赞成。
  • 如你所愿:) 在顶部我只说据我所知你不能只使用服务器端来解决它,因为魔法在客户端。客户端需要通知服务器“预加载阶段”。由于 chrome 不这样做,您需要自己发送通知。或者,您可以考虑阻止第二个请求(实际请求)的解决方案,但您不能不阻止第一个。
  • 阻塞第二个请求的问题是第二个请求实际上是构造返回给用户的视图的请求。它使用存储库中的数据。服务器端的一个理论解决方案是在第一次请求执行期间将所需的所有内容添加到会话中,然后在第二次请求期间检查会话。尽管这在任何方面都不是防错的,因为请求可能会在毫秒的时间范围内进入,并且到第二个请求执行时,会话中可能仍然没有任何内容。
  • 使用会话似乎是一种选择,但这个解决方案还有另一个问题。可以执行预加载请求,将数据放入会话中,但用户可能没有按回车,因此他永远不会进入该页面。通过这种方式(拥有许多用户),您可以使用不需要的会话数据向您的内存发送垃圾邮件。另一件事是,您仍然无法在服务器端区分是用户点击了两次页面(他可以在 url 中键入两次相同的内容,尽管这似乎很少见)还是一个预加载请求和另一个真实请求。