java代理http请求
前言
估计看到这个标题你会觉得多此一举,因为nginx完全可以实现代理,且配置非常方便。之前接到一个需求,服务器必须使用java程序进行代理,并记录详细的出入参,以便后续根据日志查到问题。先暂且不纠结是否必要,来谈谈具体实现。
需求明细
1、使用http代理post接口请求(接口前缀一样)
2、能接收json或xml数据,返pdf流文件或json数据
3、详细记录出入参
具体实现
俗话说无图无真相,惯例边上代码边解释
1、构建springboot项目,添加pom.xml依赖
    <parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.2.2.RELEASEversion>
<relativePath/>
parent>
<groupId>cn.gudukegroupId>
<artifactId>guduke-agentartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>guduke-agent/name>
<description>代理服务description>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpclientartifactId>
<version>4.5.10version>
dependency>
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpmimeartifactId>
<version>4.5.10version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.76version>
dependency>
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-classicartifactId>
<version>1.2.3version>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-to-slf4jartifactId>
<version>2.10.0version>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>jul-to-slf4jartifactId>
<version>1.7.25version>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
dependencies>
2、application.yml添加配置信息
server:
servlet:
context-path: /guduke-agent
port: 9530
# 需要代理的接口地址  
agent:
insur: http://www.guduke.cn:20001/agent
3、定义工具类AgentUtil
/**
 * @ClassName AgentUtil
 * @Description 代理工具类
 * @Author yuxk
 * @Date 2021/10/8 20:15
 * @Version 1.0
 **/
public class AgentUtil {
public static final String CONTENT_LENGTH = "content-length";
public static final String CONTENT_TYPE = "application/pdf";
private static final Logger logger = LoggerFactory.getLogger(AgentUtil.class);
private static PoolingHttpClientConnectionManager connMgr;
private static RequestConfig requestConfig;
    # 设置1分钟超时时间
private static final int MAX_TIMEOUT = 60000;
static {
// 设置连接池
        connMgr = new PoolingHttpClientConnectionManager();
// 设置连接池大小
        connMgr.setMaxTotal(100);
        connMgr.setDefaultMaxPerRoute(connMgr.getMaxTotal());
        connMgr.setValidateAfterInactivity(1000);
        RequestConfig.Builder configBuilder = RequestConfig.custom();
// 设置连接超时
        configBuilder.setConnectTimeout(MAX_TIMEOUT);
// 设置读取超时
        configBuilder.setSocketTimeout(MAX_TIMEOUT);
// 设置从连接池获取连接实例的超时
        configBuilder.setConnectionRequestTimeout(MAX_TIMEOUT);
        requestConfig = configBuilder.build();
    }
/**
     * @Author yuxk
     * @Description post请求
     * @Date 2022/4/15 9:05
     * @Param apiUrl 请求地址
     * @Param headersMap 请求头
     * @Param json 请求参数
     * @Param infno 功能号
     * @Param contentType 请求类型
     * @Return java.util.Map
     **/
public static Map doPost(String apiUrl, Map headersMap, Object json, String infno, String contentType)   {
        Map resultMap = new HashMap<>();
        CloseableHttpClient httpClient = null;
// 根据http或https构建CloseableHttpClient对象
if (apiUrl.startsWith("https")) {
            httpClient = HttpClients.custom().setSSLSocketFactory(createSSLConnSocketFactory())
                    .setConnectionManager(connMgr).setDefaultRequestConfig(requestConfig).build();
        } else {
            httpClient = HttpClients.createDefault();
        }
        Object httpStr = null;
        HttpPost httpPost = new HttpPost(apiUrl);
        CloseableHttpResponse response = null;
try {
            httpPost.setConfig(requestConfig);
// 解决中文乱码问题
            StringEntity stringEntity = new StringEntity(json.toString(), "UTF-8");
            stringEntity.setContentEncoding("UTF-8");
            stringEntity.setContentType(contentType);
            httpPost.setEntity(stringEntity);
// 设置请求头
if (!headersMap.isEmpty()) {
for (Map.Entry map : headersMap.entrySet()) {
// 去除字段长度,提交会报错
if (!CONTENT_LENGTH.equals(map.getKey())) {
                        httpPost.setHeader(map.getKey(), map.getValue());
                    }
                }
            }
            response = httpClient.execute(httpPost);
// 获取返回的请求头信息
            Map responseMap = new HashMap<>();
            Header[] allHeaders = response.getAllHeaders();
for (Header header : allHeaders) {
                responseMap.put(header.getName(), header.getValue());
            }
            HttpEntity entity = response.getEntity();
// 查看是否为流文件
            String disposition = responseMap.get("Content-Disposition");
            String type = responseMap.get("Content-Type");
if (!StringUtils.isEmpty(disposition) || CONTENT_TYPE.equals(type)) {
                resultMap.put("Content-Disposition", disposition);
if (!StringUtils.isEmpty(type)) {
                    responseMap.put("Content-Type", type);
                }
                httpStr = EntityUtils.toByteArray(entity);
            } else {
                httpStr = EntityUtils.toString(entity, "UTF-8");
            }
        } catch (IOException e) {
            logger.error("调用服务异常:{}", e.getMessage());
        } finally {
if (response != null) {
try {
                    EntityUtils.consume(response.getEntity());
                } catch (IOException e) {
                    logger.error(e.getMessage());
                }
            }
        }
        resultMap.put("result", httpStr);
return resultMap;
    }
/**
     * @Author yuxk
     * @Description 创建SSL安全连接
     * @Date 2022/4/15 9:05
     * @Param
     * @Return org.apache.http.conn.ssl.SSLConnectionSocketFactory
     **/
private static SSLConnectionSocketFactory createSSLConnSocketFactory() {
        SSLConnectionSocketFactory sslsf = null;
try {
            SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
@Override
public boolean isTrusted(X509Certificate[] chain, String authType) {
return true;
                }
            }).build();
            sslsf = new SSLConnectionSocketFactory(sslContext, new HostnameVerifier() {
@Override
public boolean verify(String arg0, SSLSession arg1) {
return true;
                }
            });
        } catch (GeneralSecurityException e) {
            logger.error("SSL连接异常:{}", e.getMessage());
        }
return sslsf;
    }
/**
     * @Author yuxk
     * @Description 获取随机文件名
     * @Date 2022/4/15 9:05
     * @Param
     * @Return java.lang.String
     **/
public static String getFilename() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
return sdf.format(new Date()) + (int) (Math.random() * (9000) + 1000);
    }
/**
     * @Author yuxk
     * @Description request请求参数转成字节数组
     * @Date 2022/4/15 9:04
     * @Param request
     * @Return byte[]
     **/
public static byte[] getRequestPostBytes(HttpServletRequest request)
throws IOException {
int contentLength = request.getContentLength();
if (contentLength < 0) {
return null;
        }
byte[] buffer = new byte[contentLength];
for (int i = 0; i < contentLength; ) {
int readlen = request.getInputStream().read(buffer, i,
                    contentLength - i);
if (readlen == -1) {
break;
            }
            i += readlen;
        }
return buffer;
    }
}
   4、创建代理controller类AgentController
/**
 * @ClassName AgentController
 * @Description 代理控制类
 * @Author yuxk
 * @Date 2021/10/8 20:08
 * @Version 1.0
 **/
@RestController
@Slf4j
public class AgentController {
@Value("${agent.insur}")
private String insurUrl;
@PostMapping("/insur/**")
public ResponseEntity insur(HttpServletRequest request) throws IOException {
        String path = request.getServletPath();
        log.info("开始调用接口");
// 获取接口名称
        String infno = path.replace("/insur", "");
        String actualno = infno.startsWith("/") ? infno.substring(1) : "";
        log.info("接口名称:{}", actualno);
// 获取请求入参
byte[] bytes = AgentUtil.getRequestPostBytes(request);
        String param = new String(bytes, "UTF-8");
        log.info("请求入参:{}", param);
// 获取请求头数据
        Map headersMap = new HashMap<>(16);
        Enumeration headerNames = request.getHeaderNames();
        StringBuilder builder = new StringBuilder(16);
while (headerNames.hasMoreElements()) {
            String name = headerNames.nextElement();
            String value = request.getHeader(name);
            headersMap.put(name, value);
            builder.append(name + "=" + value + ",");
        }
        log.info("请求头参数:{}", StringUtils.isEmpty(builder.toString()) ? "" : builder.substring(0, builder.length() - 1));
// 获取请求方式
        String contentType = request.getContentType();
// 调用接口,获取结果集和文件类型
        String realUrl= insurUrl + infno;
        Map resultMap = AgentUtil.doPost(realUrl, headersMap, param, contentType, actualno);
        String disposition = (String) resultMap.get("Content-Disposition");
        String type = (String) resultMap.get("Content-Type");
if (!StringUtils.isEmpty(disposition) || AgentUtil.CONTENT_TYPE.equals(type)) {
// 文件以流返回
            HttpHeaders headers = new HttpHeaders();
if (!StringUtils.isEmpty(type)) {
                headers.add("Content-Type", type);
            }
if (AgentUtil.CONTENT_TYPE.equals(type)) {
                headers.add("Content-Disposition", AgentUtil.getFilename() + ".pdf");
            } else {
                headers.add("Content-Disposition", disposition);
            }
            log.info("请求回参:{}", resultMap.get("result"));
return new ResponseEntity<>(resultMap.get("result"), headers, HttpStatus.OK);
        }
        log.info("请求回参:{}", resultMap.get("result"));
return new ResponseEntity<>(resultMap.get("result"), HttpStatus.OK);
    }
}
   @PostMapping("/insur/**") 路径带**可以匹配多个路径request.getServletPath() 获取请求路径如/insur/1101,目的是为了后面进行路径替换request.getHeaderNames() 获取所有的请求头参数,便于后面java请求时把原请求头参数一并带过去
获取请求结果之后的Content-Disposition和Content-Type数据,若是流文件java以流返回,若是json数据直接返回
5、构建切面类ControllerAspect添加日志请求ID
/**
 * @ClassName ControllerAspect
 * @Description TODO
 * @Author yuxk
 * @Date 2021/12/15 13:52
 * @Version 1.0
 **/
@Component
@Aspect
public class ControllerAspect {
@Pointcut("execution(* cn.hsa.powersi.controller.*.*(..))")
public void executionService() {
    }
@Before(value = "executionService()")
public void doBefore(JoinPoint joinPoint) {
// 添加日志打印
        String requestId = MDC.get("requestId");
if (StringUtils.isEmpty(requestId)) {
            requestId = String.valueOf(UUID.randomUUID());
            MDC.put("traceID", requestId);
        }
    }
@AfterReturning(pointcut = "executionService()")
public void doAfter() {
        MDC.clear();
    }
@AfterThrowing(pointcut = "executionService()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
        MDC.clear();
    }
}
6、创建logback-spring.xml日志配置文件,保存出入参到文件
复制"1.0" encoding="UTF-8"?>
"false">
    
    "context" name="LOG_HOME" source="logging.file.path" defaultValue="/app/log/guduke-agent"/>
    
    
    "context" name="APP_NAME" source="spring.application.name" defaultValue="guduke-agent"/>
    
    "context" name="ROOT_LEVEL" source="logging.level.root" defaultValue="INFO"/>
    "context" name="PATTERN" source="logging.file.pattern"
                    defaultValue="%d{yyyy-MM-dd HH:mm:ss.SSS} - [%X{traceID}] - [%thread] %-5level %logger{50}.%M\(%line\) - %msg%n"/>
    
    "context" name="MAXHISTORY" source="logging.file.maxHistory" defaultValue="180"/>
    
    "context" name="MAXFILESIZE" source="logging.file.maxFileSize" defaultValue="100MB"/>
    
    "context" name="TOTALSIZECAP" source="logging.file.totalSizeCap" defaultValue="10GB"/>
    
    "STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            
            ${PATTERN} 
         
     
    
    "FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            
            ${LOG_HOME}/${APP_NAME}.%d{yyyy-MM-dd}.%i.log 
            
            ${MAXHISTORY} 
            ${MAXFILESIZE} 
            ${TOTALSIZECAP} 
         
        class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            
            ${PATTERN} 
         
     
    
    
    "prod">
        
        "${ROOT_LEVEL}">
            
            "FILE"/>
          
     
    
    "!prod">
        "${ROOT_LEVEL}">
            "STDOUT"/>
            "FILE"/>
           
     
        评论
