【问题标题】:Is it possible to manually cache an entire web page?是否可以手动缓存整个网页?
【发布时间】:2019-08-19 02:33:32
【问题描述】:

我需要缓存一个网页,然后对于未来的请求,检查缓存(使用 url 作为键),如果找到,从缓存中返回网页而不是发出请求。

我正在使用 Smiley 的 ProxyServlet,servlet 写入 OutputStream 的方法似乎非常适合缓存。我只添加了两行代码:

/**
 * Copy response body data (the entity) from the proxy to the servlet client.
 * TODO: CACHE entity here for retrieval in filter
 */
protected void copyResponseEntity( HttpResponse proxyResponse, HttpServletResponse servletResponse,
        HttpRequest proxyRequest, HttpServletRequest servletRequest ) throws IOException
{
    HttpEntity entity = proxyResponse.getEntity();
    if ( entity != null )
    {
        String key =  getCurrentUrlFromRequest( servletRequest );  // 1
        basicCache.getCache().put( key, proxyResponse.getEntity() ); // 2
        OutputStream servletOutputStream = servletResponse.getOutputStream();
        entity.writeTo( servletOutputStream );
    }
}

它有点工作,它确实将 HttpEntity 存储在缓存中。 但是当我返回浏览器并再次请求相同的 url 时,当代码返回到我的过滤器中时,我使用 url 作为键获取 HttpEntity,并将其写入响应,但我得到一个“流已关闭" 错误:

java.io.IOException: Stream closed
    at java.base/java.util.zip.GZIPInputStream.ensureOpen(GZIPInputStream.java:63) ~[na:na]
    at java.base/java.util.zip.GZIPInputStream.read(GZIPInputStream.java:114) ~[na:na]
    at java.base/java.io.FilterInputStream.read(FilterInputStream.java:107) ~[na:na]
    at org.apache.http.client.entity.LazyDecompressingInputStream.read(LazyDecompressingInputStream.java:64) ~[httpclient-4.5.9.jar:4.5.9]
    at org.apache.http.client.entity.DecompressingEntity.writeTo(DecompressingEntity.java:93) ~[httpclient-4.5.9.jar:4.5.9]
    at com.myapp.test.foo.filters.TestFilter.doFilter(TestFilter.java:37) ~[classes/:na]

这是过滤器:

@Component
@WebFilter( urlPatterns = "/proxytest", description = "a filter for test servlet", initParams = {
        @WebInitParam( name = "msg", value = "==> " ) }, filterName = "test filter" )
public class TestFilter implements Filter
{

    private FilterConfig filterConfig;

    @Autowired BasicCache basicCache;


    @Override
    public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain )
            throws IOException, ServletException
    {
        String url = getCurrentUrlFromRequest( servletRequest ); // 1
        HttpEntity page = (HttpEntity) basicCache.getCache().get( url ); //2
        if ( null != page )  // 3
        {
            OutputStream servletOutputStream = servletResponse.getOutputStream();  // 4
            page.writeTo( servletOutputStream );  // 5 stream closed :(
        }
        else
        {
            filterChain.doFilter( servletRequest, servletResponse );
        }

    }


    public String getCurrentUrlFromRequest( ServletRequest request )
    {
        if ( !( request instanceof HttpServletRequest ) ) return null;

        return getCurrentUrlFromRequest( (HttpServletRequest) request );
    }

    public String getCurrentUrlFromRequest( HttpServletRequest request )
    {
        StringBuffer requestURL = request.getRequestURL();
        String queryString = request.getQueryString();

        if ( queryString == null ) return requestURL.toString();

        return requestURL.append( '?' ).append( queryString ).toString();
    }

    @Override
    public void destroy()
    {
    }

    @Override
    public void init( FilterConfig filterConfig ) throws ServletException
    {
        this.filterConfig = filterConfig;
    }

}

哦,还有 BasicCache 类以防万一:

@Component
public class BasicCache
{

    private UserManagedCache<String, HttpEntity> userManagedCache;

    public BasicCache()
    { 
        userManagedCache = UserManagedCacheBuilder.newUserManagedCacheBuilder( String.class, HttpEntity.class )
                .build( true );
    }

    public UserManagedCache getCache()
    {
        return userManagedCache;
    }

    public void destroy()
    {
        if ( null != userManagedCache )
        {
            userManagedCache.close();
        }
    }
}

我被这种非常本地化/手动/任何你想称之为缓存的方式所困扰——我不能使用明显的“只需连接 ehcache / redis / 任何东西,让它做它的事情”。因此,虽然我知道这些精细的缓存可以缓存整个网页,但我不知道它们是否允许我以这种公认的不同寻常的方式工作。

所以我希望 SO 可以告诉我如何完成这项工作。我首先尝试为我的基本缓存连接一个 ConcurrentHashMap,但这也不起作用,所以我想看看我是否可以利用大型缓存枪所拥有的任何魔力,但到目前为止我还不能。

感谢您的帮助!

【问题讨论】:

  • HttpEntity 只是当前与您的源建立的 io 流的句柄,您只能从中读取一次。要缓存结果,您需要将流内容复制到您自己的数据结构中。例如。一个字节数组。

标签: java caching servlets servlet-filters ehcache-3


【解决方案1】:

TestFilter类中,能不能在这一行下下断点调试一下?

HttpEntity page = (HttpEntity) basicCache.getCache().get( url );

底层流可能不处于您实际执行的状态:

page.writeTo( servletOutputStream );

你也许可以根据the reference example here重写。 本质上,声明一个PrintWriter,您可以控制状态、从缓存中获取内容、写入响应并关闭编写器。

【讨论】:

  • 这是从浏览器第二次调用后,该行代码中名为 page 的 HttpEntity 变量的状态(第一次调用显然为 null,因为它尚未被缓存):i.imgur.com/KgMloa9.pngI希望你能从中有所收获,感谢收看!
【解决方案2】:

正如这里已经说过的 Should one call .close() on HttpServletResponse.getOutputStream()/.getWriter()?

您最好为您的 servlet 实现一个包装器,详细信息在下面的文章中。

https://www.oracle.com/technetwork/java/filters-137243.html#72674

【讨论】:

  • 感谢您的回复。阅读您的链接后,我知道我可以包装 servlet 响应,但我错过了最后一步。如果我将 HttpServletResponseWrapper 缓存在我的代理 servlet 中,那么在来自浏览器的下一个请求中,当我的过滤器从缓存中获取 HttpServletResponseWrapper 时,我怎样才能将其推出门并破坏过滤器链?
【解决方案3】:

我想知道为什么所有这些很棒的答案都同意包装 HttpServletResponse 而我无法让它工作——我觉得自己像个白痴。它不起作用,因为响应中没有任何要包装的内容。

页面内容从一开始就在HttpEntity中: HttpEntity 实体 = proxyResponse.getEntity();

在我意识到追逐 servlet 响应/请求不是答案后,我很幸运地发现:org.apache.http.entity.BufferedHttpEntity

它完成了上面所有有用的 SO 海报告诉我要做的事情(包装一个 HttpEntity 以便您可以重复地获取内容),但它是在正确的对象上完成的。

所以上面的第一个方法只是稍微调整了一下,它仍然可以很好地代理:

    if ( entity != null )
    {
        String key =  getCurrentUrlFromRequest( servletRequest );
        OutputStream servletOutputStream = servletResponse.getOutputStream();
        BufferedHttpEntity wrapper = new BufferedHttpEntity( entity );
        basicCache.getCache().put( key, wrapper );
        wrapper.writeTo( servletOutputStream );
    }

将 BasicCache 更改为期望 String 和 BufferedHttpEntity,然后对于后续请求,在过滤器中从缓存中抓取 BufferedHttpEntity,完成所有工作的行与上面的最后一行相同:

    if ( null != page )
    {           
        OutputStream servletOutputStream = servletResponse.getOutputStream();
        page.writeTo( servletOutputStream );  // bingo
    }
    else
    {
        filterChain.doFilter( servletRequest, servletResponse );
    }

感谢大家的帮助!

【讨论】:

  • 很高兴我们为您提供帮助,您当然可以选择与您的问题最相关的答案 :)
猜你喜欢
  • 1970-01-01
  • 2010-12-31
  • 2016-11-21
  • 1970-01-01
  • 2015-12-22
  • 1970-01-01
  • 1970-01-01
  • 2012-02-21
  • 2011-07-08
相关资源
最近更新 更多