0

我需要缓存一个网页,然后对于未来的请求,检查缓存(使用 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 ,但这也不起作用,所以我想看看我是否可以利用大型缓存枪所拥有的任何魔力,但到目前为止我还不能。

谢谢你的帮助!

4

3 回答 3

1

正如这里已经说过的, 应该在 HttpServletResponse.getOutputStream()/.getWriter() 上调用 .close() 吗?

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

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

于 2019-08-19T05:09:19.440 回答
1

TestFilter课堂上,能不能在这一行下断点,调试一下?

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

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

page.writeTo( servletOutputStream );

您或许可以根据这里的参考示例进行重写。本质上,声明PrintWriter您控制状态,从缓存中获取内容,写入响应,然后关闭编写器。

于 2019-08-19T03:58:42.657 回答
0

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

页面内容从一开始就在 HttpEntity 中: HttpEntity entity = 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 );
    }

感谢大家的帮助!

于 2019-08-19T18:45:27.097 回答