如何为Java HttpURLConnection流量启用线路日志记录?

57

我在另一个项目中使用了Jakarta commons HttpClient,现在我希望使用“标准”的HttpUrlConnection来实现相同的wire logging输出。

我曾经使用过Fiddler作为代理,但我希望直接从Java中记录流量日志。

仅仅捕获连接的输入和输出流是不够的,因为HTTP头是由HttpUrlConnection类编写和消耗的,所以我将无法记录这些头部信息。

8个回答

67
根据 Sun的HttpURLConnection源代码,可以通过 JUL 实现一些日志记录支持。
设置(根据需要调整路径):
-Djava.util.logging.config.file=/full/path/to/logging.properties

logging.properties:

handlers= java.util.logging.ConsoleHandler
java.util.logging.ConsoleHandler.level = FINEST
sun.net.www.protocol.http.HttpURLConnection.level=ALL

这将记录到控制台,根据需要进行调整,例如记录到文件中。
示例输出:
2010-08-07 00:00:31 sun.net.www.protocol.http.HttpURLConnection writeRequests
FIN: sun.net.www.MessageHeader@16caf435 pairs: {GET /howto.html HTTP/1.1: null}{User-Agent: Java/1.6.0_20}{Host: www.rgagnon.com}{Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2}{Connection: keep-alive}
2010-08-07 00:00:31 sun.net.www.protocol.http.HttpURLConnection getInputStream
FIN: sun.net.www.MessageHeader@5ac0728 pairs: {null: HTTP/1.1 200 OK}{Date: Sat, 07 Aug 2010 04:00:33 GMT}{Server: Apache}{Accept-Ranges: bytes}{Content-Length: 17912}{Keep-Alive: timeout=5, max=64}{Connection: Keep-Alive}{Content-Type: text/html}

请注意,这仅打印标题而不包括正文。
有关详细信息,请参见http://www.rgagnon.com/javadetails/java-debug-HttpURLConnection-problem.html
还有系统属性-Djavax.net.debug=all。但它主要用于SSL调试

14
通过编程的方式,您可以使用以下代码实现:sun.util.logging.PlatformLogger .getLogger("sun.net.www.protocol.http.HttpURLConnection") .setLevel(PlatformLogger.Level.ALL);该代码用于设置日志记录器,在HTTP连接中记录所有级别的日志信息。 - Erich Eichinger
1
这将引用文件 JRE_HOME\lib\logging.properties,该文件应该已经存在。如果您没有看到任何输出,则日志级别可能太低了,请找到行 java.util.logging.ConsoleHandler.level 并将其设置为 FINEST - user3792852
1
@user3792852:好观点。我顺便把它加到答案里了,希望你不介意。 - sleske
如果您正在使用Tomcat,请记住著名的catalina.sh(或Windows上的catalina.bat)会设置-Djava.util.logging.config.file,这可能会覆盖您自己的命令行选项... - A. Masson

16

通过在默认的SSLSocketFactory之上实现自己的SSLSocketFactory,我已经能够记录所有SSL流量。

这对我起作用的原因是我们所有的连接都使用HTTPS,并且可以使用方法HttpsURLConnection.setSSLSocketFactory设置套接字工厂。

一种更完整的解决方案,使得所有套接字监视可行,可以在http://www.javaspecialists.eu/archive/Issue169.html找到。 感谢Lawrence Dol指出了使用Socket.setSocketImplFactory,让我朝正确的方向努力。

以下是我未准备好投入生产环境的代码:

public class WireLogSSLSocketFactory extends SSLSocketFactory {

    private SSLSocketFactory delegate;

    public WireLogSSLSocketFactory(SSLSocketFactory sf0) {
        this.delegate = sf0;
    }

    public Socket createSocket(Socket s, String host, int port,
            boolean autoClose) throws IOException {
        return new WireLogSocket((SSLSocket) delegate.createSocket(s, host, port, autoClose));
    }

    /*
    ...
    */

    private static class WireLogSocket extends SSLSocket {

        private SSLSocket delegate;

        public WireLogSocket(SSLSocket s) {
            this.delegate = s;
        }

        public OutputStream getOutputStream() throws IOException {
            return new LoggingOutputStream(delegate.getOutputStream());
        }

        /*
        ...
        */

        private static class LoggingOutputStream extends FilterOutputStream {
            private static final Logger logger = Logger.getLogger(WireLogSocket.LoggingOutputStream.class);
            //I'm using a fixed charset because my app always uses the same. 
            private static final String CHARSET = "ISO-8859-1";
            private StringBuffer sb = new StringBuffer();

            public LoggingOutputStream(OutputStream out) {
                super(out);
            }

            public void write(byte[] b, int off, int len)
                    throws IOException {
                sb.append(new String(b, off, len, CHARSET));
                logger.info("\n" + sb.toString());
                out.write(b, off, len);
            }

            public void write(int b) throws IOException {
                sb.append(b);
                logger.info("\n" + sb.toString());
                out.write(b);
            }

            public void close() throws IOException {
                logger.info("\n" + sb.toString());
                super.close();
            }
        }
    }
}

1
Socket.setSocketImplFactory(),用于非SSL,也许? - Lawrence Dol
1
@Serhill:一些详细的示例和/或代码摘录可能会让你获得采纳。 - Lawrence Dol
我认为javaspecialist提出的“在默认选项上实现自己的SSLSocketFactory来处理SSL流量”解决方案是最好的。 - Gladwin Burboz
1
还可以使用系统属性“-Dssl.SocketFactory.provider=WireLogSSLSoketFactory” [http://java.sun.com/j2se/1.5.0/docs/guide/security/jsse/JSSERefGuide.html#InstallationAndCustomization]。 - Gladwin Burboz
看看能否在这里实现使用拦截器模式的框架。您可以实现不同的拦截器并将它们注册到框架中,以便在委托给下一个之前拦截某些API。类似于Servlet技术中的FilterChain。 - Gladwin Burboz
2
@Serhill:尝试使用此类;添加了所有必要的覆盖并将 HttpsURLConnection socketFactory 设置为此类的实例(使用默认 SocketFactory 实例化)。然而,在调用 HttpsURLConnection.getOutputStream() 时它不连接,而在使用默认 SSLSocketFactory 时,它会连接。我一定漏了什么。有任何想法吗? - Brian Reinhold

10

解决方案#1:使用装饰器模式

您需要在HttpURLConnection类上使用装饰器模式来扩展其功能。然后覆盖所有HttpURLConnection方法,并将操作委托给组件指针,同时捕获所需信息并记录它。

还要确保覆盖父类URLConnection.getOutputStream()OutputStreamURLConnection.html#getInputStream()InputStream方法,以返回装饰的OutputStreamInputStream对象。

.

解决方案#2:使用自定义的内存http代理

编写一个简单的http代理服务器,并在应用程序启动和初始化期间在其单独的线程中启动它。请参见示例简单代理服务器

将您的应用程序配置为使用上述HTTP代理处理所有请求。请参见配置Java以使用代理

现在,所有流量都通过上面的代理进行,就像在fiddler中一样。因此,您可以访问原始的http流,即“从客户端到服务器”以及“从服务器返回客户端”。您需要解释这个原始信息并根据需要记录它。

更新:使用HTTP代理作为适配器连接基于SSL的Web服务器。

  == Client System =============================== 
  |                                              | 
  |    ------------------------------            | 
  |   |                              |           | 
  |   |    Java process              |           | 
  |   |                       ----   |           | 
  |   |        ----------    |    |  |           | 
  |   |       |          |    -O  |  |           | 
  |   |       |  Logging |        |  |           | 
  |   |       |   Proxy <---HTTP--   |    -----  | 
  |   |       |  Adapter |           |   |     | | 
  |   |       |  Thread o------------------>   | | 
  |   |       |        o |           |   |     | | 
  |   |        --------|-            |   | Log | | 
  |   |                |             |    -----  | 
  |    ----------------|-------------            | 
  |                    |                         | 
  =====================|========================== 
                       |                           
                       |                           
                     HTTPS                         
                      SSL                          
                       |                           
  == Server System ====|========================== 
  |                    |                         | 
  |    ----------------|----------------         | 
  |   |                V                |        | 
  |   |                                 |        | 
  |   |   Web Server                    |        | 
  |   |                                 |        | 
  |    ---------------------------------         | 
  |                                              | 
  ================================================ 

添加解决方案#2:使用自定义的内存中HTTP代理。 - Gladwin Burboz
一个自定义代理无法处理SSL。这就是为什么我正在寻找直接从Java记录流量而不是使用Fiddler的原因。 - Serxipc
有关SSL,请参见我的更新“使用HTTP代理作为适配器连接基于SSL的Web服务器。” - Gladwin Burboz
通过代理嗅探SSL的问题在于,为了查看未加密内容,必须打破证书信任链。Fiddler能够记录SSL,因为它生成虚假的服务器证书。如果我直接在我们的应用程序上执行相同操作,在加密之前,我们将不会打破证书链。谢谢您的努力。 - Serxipc
仅因为“绘画”就加上一个 +1 :o) - Lonzak

7
在Linux中,您可以在strace下运行VM:
strace -o strace.out -s 4096 -e trace=network -f java ...

7

刷新Java 8环境:

请参考@sleske的答案。

System.setProperty("javax.net.debug","all");

这个功能对我来说很容易使用。

@weberjn的建议也非常好。

strace -o strace.out -s 4096 -e trace=network -f java

但是它在处理SSL流量时没有用处,因为它只能转储编码流。

所有其他代码技巧对我来说都没有起作用,但可能是我没有尝试足够努力。


2

使用AspectJ插入Pointcut以在方法周围添加日志建议如何?我相信AspectJ可以编织到私有/受保护的方法中。

似乎sun.net.www.protocol.http.HttpURLConnection.writeRequest可能会调用sun.net.www.http.HttpClient.writeRequest,它将MessageHeader对象作为输入,因此这将是您的目标。

最终可能会起作用,但非常脆弱,只能在Sun JVM上工作;而且您只能信任您使用的确切版本。


3
依赖未公开、未记录、私有的API是一个不好的想法。 - Lawrence Dol
1
@LawrenceDol:没错。然而,对于单个的调试会话来说,这可能是可以接受的。 - sleske

2

如果你只是想获取线路上的内容(头信息、正文等),你可能需要尝试使用wireshark

这样做的好处是不需要改变任何代码,但如果你想通过代码启用日志记录,则此答案不适用。


2
我不认为你可以自动完成这个任务,但是你可以通过将 FilterOutputStreamFilterInputStream 子类化,并将 HttpUrlConnection 的输出和输入流作为参数传递。然后,当字节被写入/读取时,将它们记录下来并通过基础流传递。

我能用这个方案捕获HTTP头吗?据我理解,您的建议只有POST或GET内容会通过流传递。 - Serxipc
@Serhii 这将包括所有进出的内容,因此包括标头。 - Bozho
2
@Bozho:实际上,我认为它不会包括头文件。当你获得流时,头文件已经被发送(对于输出流)或接收和解析(对于输入流)。 - Lawrence Dol

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接