V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
DtFlys
V2EX  ›  Java

Java 配置 HTTP/Socks 代理竟能如此简单

  •  
  •   DtFlys · 2023-08-30 17:01:02 +08:00 · 2402 次点击
    这是一个创建于 449 天前的主题,其中的信息可能已经有所发展或是发生改变。

    在网络请求过程中,使用代理是一种常见的需求。代理服务器可以帮助我们隐藏真实的 IP 地址、加速访问速度、访问公司特定内网等等要求。在 Java 中,我们可以通过一些库或框架来实现代理的设置和使用。

    但如果使用 OkHttp 、HttpClient 亦或是 Retrofit 和 Feign ,需要实现 Socks 协议代理都需要实现SSLSocketFactory类或ConnectionSocketFactory接口的子类,重写createSokcet方法,实现起来非常的麻烦。如果代理还需要用户名密码验证(大部分都会有),还需要实现Authenticator的子类,并通过ThreadLocal分配到请求各自的线程中,整个过程需要自己写很多代码,无比烦人。

    而本文将介绍如何使用一种最简单的方法,即使用声明式 HTTP 框架 Forest ,结合@HTTPProxy@SocksProxy注解来发送 HTTP/HTTPS 请求,来快速实现代理功能。

    Forest 的基本使用

    添加 Forest 依赖

    <dependency>
        <groupId>com.dtflys.forest</groupId>
        <artifactId>forest-spring-boot-starter</artifactId>
        <version>1.5.33</version>
    </dependency>
    

    如果您的项目不是 spring-boot 项目,请看官方文档来配置不同环境下的依赖。

    先看看没有代理的情况

    // 定义一个 Forest 客户端接口
    public interface MyClient {
        // 当调用该方法时,会自动使用 Get 请求访问地址 https://example.com
        @Get("https://example.com")
        String getData();
    }
    

    假如https://example.com这个地址是需要通过代理才能正常访问,那么以下代码将不会成功

    // 注入 Forest 客户端实例
    @Resource
    MyClient myClient;
    
    ... ...
    // 网络请求将会失败
    String data = myClient.getData();
    

    使用 HTTP 代理

    在接口上挂上@HTTPProxy接口即可

    // 通过 @HTTPProxy 注解配置代理服务的地址和端口
    @HTTPProxy(host = "127.0.0.1", port = "1081")
    public interface MyClient {
        @Get("https://example.com")
        String getData();
    }
    

    如果代理服务需要验证

    // 通过 @HTTPProxy 注解配置代理服务的地址和端口以及用户验证信息
    @HTTPProxy(host = "127.0.0.1", port = "1081", username = "root", password = "123456")
    public interface MyClient {
        @Get("https://example.com")
        String getData();
    }
    

    使用 Socks 代理

    如果您需要连的是 Socks 协议的代理端口,那也很简单,可以用上面的方法如法炮制,只不过注解名换了一下而已

    // 通过 @SocksProxy 注解配置 Socks 协议代理服务的地址和端口
    @SocksProxy(host = "127.0.0.1", port = "1081")
    public interface MyClient {
        @Get("https://example.com")
        String getData();
    }
    

    加上用户名密码

    // 通过 @SocksProxy 注解配置 Socks 协议代理服务的地址和端口以及用户验证信息
    @SocksProxy(host = "127.0.0.1", port = "1081", username = "root", password = "123456")
    public interface MyClient {
        @Get("https://example.com")
        String getData();
    }
    

    全局配置

    如果不想把代理的参数( host, port 等)写死在注解代码中,可以通过字符串模板来引用配置文件的属性

    先在application.yml配置文件中添加以下配置(属性名可以自己随意起):

    proxy:
       host: 127.0.0.1
       port: 1081
       username: root
       password: 123456
    

    通过字符串模板在注解中进行引用

    @SocksProxy(
        host = "#{proxy.host}",
        port = "#{proxy.port}",
        username = "#{proxy.username}",
        password = "#{proxy.password}"
    )
    public interface MyClient {
        @Get("https://example.com")
        String getData();
    }
    

    封装注解

    如果您有很多接口类要配置代理,并且不想在每个接口上放这么一大坨参数,可以使用自定义注解对@HTTPProxy@SocksProxy进行封装

    // 自定义一个注解 @MyProxy
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD, ElementType.TYPE})
    // 将 @SockProxy 注解以及参数添加到这里
    @SocksProxy(
        host = "#{proxy.host}",
        port = "#{proxy.port}",
        username = "#{proxy.username}",
        password = "#{proxy.password}"
    )
    public @interface MyProxy {
    }
    

    然后在需要代理的接口上挂上您自定义的@MyProxy注解就可以了

    @MyProxy
    public interface MyClient1 {
       @Get("https://example.com/data1")
       String getData1();
    }
    
    @MyProxy
    public interface MyClient2 {
       @Get("https://example.com/data2")
       String getData2();
    }
    
    

    此时,MyClient1 和 MyClient2 接口的请求都会走同样的代理

    非声明式方式

    以上都是以声明式的方式,配合@HTTProxy以及@SocksProxy注解来完成 HTTP/Socks 代理的设置过程的。

    如果不想定义接口、配置、注解等等玩意儿,那用编程式的 API 直接干就完了。

    // 通过 HTTP 的代理发送请求
    String data1 = Forest.get("https://example.com")
          .proxy(ForestProxy.http("127.0.0.1", 1081)
                    .username("root")
                    .password("123456"))
          .executeAsString();
          
    // 通过 Socks 的代理发送请求
    String data2 = Forest.get("https://example.com")
          .proxy(ForestProxy.socks("127.0.0.1", 1081)
                    .username("root")
                    .password("123456"))
          .executeAsString();
    
    16 条回复    2023-08-31 18:12:59 +08:00
    xiaooloong
        1
    xiaooloong  
       2023-08-30 17:07:21 +08:00   ❤️ 3
    你说得对,但是 word 说全文有 1067 个字
    《非常简单》
    NessajCN
        2
    NessajCN  
       2023-08-30 17:12:44 +08:00   ❤️ 1
    python:
    import requests
    resp = requests.get('http://go.to',
    proxies=dict( http='socks5://user:pass@host:port',
    https='socks5://user:pass@host:port'))

    「如此简单」
    DtFlys
        3
    DtFlys  
    OP
       2023-08-30 17:14:40 +08:00   ❤️ 2
    @xiaooloong 如果你认真看完文章,对比过其它 Java 的解决方案,就知道为什么它如此简单了
    DtFlys
        4
    DtFlys  
    OP
       2023-08-30 17:16:03 +08:00
    @NessajCN Python 确实简单,但 Java 里很难找到简单的方案
    aosan926
        5
    aosan926  
       2023-08-30 17:24:47 +08:00
    之前的项目中有用过这个框架,后来在升级 Spring Boot 3 的时候不支持就切到 Retrofit ,感觉也挺好用的
    mmdsun
        6
    mmdsun  
       2023-08-30 17:31:17 +08:00
    感谢分享,框架看起来不错。

    https://www.baeldung.com/spring-6-http-interface
    Java 用新版本 SOCKS 代理会很简单:
    HttpClient.create().proxy(proxyOptions -> proxyOptions.type(ProxyProvider.Proxy.SOCKS5)
    .host(..)
    .port(..)
    );



    如果用 URLConnection con = url.openConnection();可以用启动参数,这种是 HTTP 代理。
    java -Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=3128
    DtFlys
        7
    DtFlys  
    OP
       2023-08-30 17:35:01 +08:00
    @mmdsun 感谢分享,Java11 以后的 http 确实要比以前好很多
    DtFlys
        8
    DtFlys  
    OP
       2023-08-30 17:35:46 +08:00
    @aosan926 Forest 1.5.30 版本开始支持 springboot3 了,但 maven 坐标了
    mmdsun
        9
    mmdsun  
       2023-08-30 17:50:12 +08:00
    @DtFlys 这实是 reactor.netty.http.client 的包,spring webflux 项目会引入这个包,配合 spring 的 WebClien 使用。不过,目前仅支持反应式的项目,使用人还比较少。

    @Bean
    public WebClient webClient(){
    return WebClient.builder()
    .clientConnector(new ReactorClientHttpConnector( httpClient))
    }
    vacuitym
        10
    vacuitym  
       2023-08-30 18:00:11 +08:00
    好像启动的时候可以在 jvm 参数里面加代理配置
    DtFlys
        11
    DtFlys  
    OP
       2023-08-30 18:14:22 +08:00
    @vacuitym 是的,但这种方式有 2 个问题:
    1. jvm 参数只能用于全局代理,如果想要只有几个请求走代理,或不同请求走不同的代理,就办不到了,只能自己写代码通过 new Proxy 来实现
    2. jvm 参数不能配置验证信息,如果代理需要密码验证,只能手写 Authenticator 继承类来实现,而且也会碰到全局/局部验证的问题
    fdwjtz
        12
    fdwjtz  
       2023-08-31 04:57:11 +08:00 via Android
    直接改 http_proxy 和 socks_proxy 这俩环境变量行不通么?....
    tedzhou1221
        13
    tedzhou1221  
       2023-08-31 09:15:38 +08:00
    在一个小项目中也使用了 Forest ,的确不错。
    vacuitym
        14
    vacuitym  
       2023-08-31 10:12:28 +08:00
    @DtFlys jvm 加代理是支持密码验证的。其他的确实是问题
    xsen
        15
    xsen  
       2023-08-31 16:05:53 +08:00
    真的,现在一看 java 的各种注解就浑身不舒服
    这真的不叫简单。。lz 恐怕对简单的理解有歧义
    DtFlys
        16
    DtFlys  
    OP
       2023-08-31 18:12:59 +08:00
    @xsen 恐怕你没看到最后,也有非注解,纯编程式的方式
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3141 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 00:39 · PVG 08:39 · LAX 16:39 · JFK 19:39
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.