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"/>
评论