今天要实现一个功能:
用Java+SpringBoot服务器实现接口代理转发,对数据进行二次处理,然后返回给客户端。
背景:Java服务器作为客户端的上游服务器,需要负责返回所有请求数据,即使不是自己提供的功能,也要负责请求对应服务器,并将正确结果返回。
目的:这样做可以实现接口服务统一,也能解决前端跨域问题。
这里列举两个场景。
场景一:调用其他服务器,将结果直接返回给客户端
要实现的功能是
浏览器请求: https://10.28.10.11:8081/proxy/homeList
希望请求转发到 http://10.28.11.20:4000/homeList
源码:
package com.maomao.apm.controller;
import au.com.bytecode.opencsv.CSVReader;
import com.pinganfu.dochelper.annotation.PAFDoc;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
@RestController
public class ProxyController extends BaseController {
private String targetAddr = "https://10.28.10.11:4000";
/**
* 代理所有请求
*
* @param request
* @param response
* @throws Exception
*/
@RequestMapping(value = "/proxy/**", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public void proxy(HttpServletRequest request, HttpServletResponse response) throws IOException, URISyntaxException {
// String url = URLDecoder.decode(request.getRequestURL().toString(), "UTF-8");
URI uri = new URI(request.getRequestURI());
String path = uri.getPath();
String query = request.getQueryString();
String target = targetAddr + path.replace("/proxy", "");
if (query != null && !query.equals("") && !query.equals("null")) {
target = target + "?" + query;
}
URI newUri = new URI(target);
// 执行代理查询
String methodName = request.getMethod();
HttpMethod httpMethod = HttpMethod.resolve(methodName);
if (httpMethod == null) {
return;
}
ClientHttpRequest delegate = new SimpleClientHttpRequestFactory().createRequest(newUri, httpMethod);
Enumeration<String> headerNames = request.getHeaderNames();
// 设置请求头
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
Enumeration<String> v = request.getHeaders(headerName);
List<String> arr = new ArrayList<>();
while (v.hasMoreElements()) {
arr.add(v.nextElement());
}
delegate.getHeaders().addAll(headerName, arr);
}
StreamUtils.copy(request.getInputStream(), delegate.getBody());
// 执行远程调用
ClientHttpResponse clientHttpResponse = delegate.execute();
response.setStatus(clientHttpResponse.getStatusCode().value());
// 设置响应头
clientHttpResponse.getHeaders().forEach((key, value) -> value.forEach(it -> {
response.setHeader(key, it);
}));
StreamUtils.copy(clientHttpResponse.getBody(), response.getOutputStream());
}
}
原理是代理服务器(当前代码所在服务器)在接收到客户端的请求时,新建一个请求去请求真实服务器地址,并且把客户端请求地址里“proxy”关键字删除。等真实服务器返回数据后,把数据直接返回给客户端。
这个方法做到了统一,假设是A客户端请求B服务器实际获取C服务器的内容,这段代码可以做到B服务器请求c服务器的所有接口都能代理。只需要客户端请求时,在接口里加上“proxy”字样。
场景二:调用其他服务器,将结果进行处理后返回给客户端
场景一可以解决大部分代理问题。但是如果你需要对C服务器返回的数据进行二次加工,只需要做一点点改动。
这里举例一个B服务器获取c服务器的CSV文件,并将返回的CSV文件流转换成Json返回给客户端。
要实现的功能是
浏览器请求: https://10.28.10.11:8081/proxy/homedetail.csv
希望请求转发到 http://10.28.11.187:8088/homedetail.csv
最后解析homedetail.csv并返回json数组
源码:
package com.maomao.apm.controller;
import au.com.bytecode.opencsv.CSVReader;
import com.pinganfu.dochelper.annotation.PAFDoc;
import org.apache.logging.log4j.LogManager;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
@RestController
public class ProxyController extends BaseController {
private String targetAddrUba = "http://10.28.11.187:8088";
/**
* 代理ubaReport请求所有请求
*
* @param request
* @param response
* @return
* @throws Exception
*/
@RequestMapping(value = "/ubaProxy/**")
public List<String[]> proxyUba(HttpServletRequest request, HttpServletResponse response) throws IOException, URISyntaxException {
// String url = URLDecoder.decode(request.getRequestURL().toString(), "UTF-8");
URI uri = new URI(request.getRequestURI());
String path = uri.getPath();
String query = request.getQueryString();
String target = targetAddrUba + path.replace("/ubaProxy", "");
if (query != null && !query.equals("") && !query.equals("null")) {
target = target + "?" + query;
}
URI newUri = new URI(target);
// 执行代理查询
String methodName = request.getMethod();
HttpMethod httpMethod = HttpMethod.resolve(methodName);
if (httpMethod == null) {
return null;
}
ClientHttpRequest delegate = new SimpleClientHttpRequestFactory().createRequest(newUri, httpMethod);
Enumeration<String> headerNames = request.getHeaderNames();
// 设置请求头
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
Enumeration<String> v = request.getHeaders(headerName);
List<String> arr = new ArrayList<>();
while (v.hasMoreElements()) {
arr.add(v.nextElement());
}
delegate.getHeaders().addAll(headerName, arr);
}
StreamUtils.copy(request.getInputStream(), delegate.getBody());
// 执行远程调用
ClientHttpResponse clientHttpResponse = delegate.execute();
// response.setStatus(clientHttpResponse.getStatusCode().value());
// 设置响应头
// clientHttpResponse.getHeaders().forEach((key, value) -> value.forEach(it -> {
// response.setHeader(key, it);
// }));
CSVReader csvReader = new CSVReader(new InputStreamReader(clientHttpResponse.getBody(), "utf-8"));
List<String[]> stringsList = csvReader.readAll();
return stringsList;
}
}
pom.xml
<dependency>
<groupId>net.sf.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>2.3</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20090211</version>
</dependency>
这里用了一个“ubaProxy”作为该请求功能的关键字。
原理跟场景一相同。只是在返回数据之后对数据进行了CSV转Json
的操作。
具体代码:
CSVReader csvReader = new CSVReader(new InputStreamReader(clientHttpResponse.getBody(), "utf-8"));
List<String[]> stringsList = csvReader.readAll();
return stringsList;
注意,上面代码里将response.setHeader()
相关代码注释掉了。原因是经过二次加工返回的数据,与加工前的数据长度是不一样的,如果复用加工前的reponse header,会导致content-length
数值不准确。如果加工后的数据比原始数据多,而content-length
数值还是加工前的数值,会导致返回给客户端的数据不完整。如果不填,默认会返回所有数据。
参考文档:
《Java+springboot实现nginx反向代理功能》
《JAVA:将CSV文件转换成JSON》
path不断重复打印,导致内存溢出
中间还遇到在打印代码中的path
时,path
的值一直在循环重复,控制台不停地打印,看起来像死循环了。
最后服务器撑不住,可能回报内存溢出的异常。
java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOfRange(Arrays.java:3664
解决办法
网上有很多答案是说把JVM的内存设置得更大一些:
手动设置Heap size
修改TOMCAT_HOME/bin/catalina.bat,在“echo "Using CATALINA_BASE: $CATALINA_BASE"”上面加入以下行:
Java代码
set JAVA_OPTS=%JAVA_OPTS% -server -Xms800m -Xmx800m -XX:MaxNewSize=256m
或将application.properties
里面的大小值设置得更大一些。
//src/main/resources/application.properties
#将文件写入磁盘的阈值。值可以使用后缀“MB”或“KB”分别表示兆字节或千字节。
spring.servlet.multipart.file-size-threshold=2KB
#设置单个文件的大小,
spring.servlet.multipart.max-file-size=100MB
#最大请求大小。值可以使用后缀“MB”或“KB”分别表示兆字节或千字节。
spring.servlet.multipart.max-request-size=100MB
但是我这里不是因为内存设置不够引起的。
出问题的原因是类名上面使用了@Controller
注解,把它改成@RestController
就好了。
原理是@Controller
注解会认为请求的是一个页面,而不是数据,如果没找到页面,会继续转发往下找,如果一直找不到,就死循环了。而@RestController
注解只关心请求的内容,请求到内容就直接返回,请求不到内容也返回空数据,不会进行转发。
请问一下第一种情况,应该怎么用postman测试呢
没这测试过,抱歉哈!
针对 form-data 参数格式好像不行呢
这个有解决方案吗?
确实 怎么解决呀
额,这个好像还没有研究过。有答案的话,欢迎共享一下,嘻嘻嘻~
icon_razz.gif icon_evil.gif icon_sad.gif icon_mad.gif icon_twisted.gif