文章目录
通过Apache Common
封装好的HttpClient
请求
HttpClient
的Get或Post请求方式步骤
- 生成一个
HttpClient
对象并设置相应的参数; - 生成一个
GetMethod
对象或PostMethod
并设置响应的参数; - 用
HttpClient
生成的对象来执行GetMethod
生成的Get方法/执行PostMethod
生成Post方法; - 处理响应状态码;
- 若响应正常,处理HTTP响应内容;
- 释放连接。
引入jar包
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>3.1</version>
</dependency>
<!--SpringBoot 的webstarter自带,可以不用引入 -->
<dependency>
<groupId>com.fasterxml</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.3</version>
</dependency>
Get方式请求
请求:
public class HttpClientService {
private final ObjectMapper objectMapper;
private final HttpClient httpClient;
public HttpClientService(ObjectMapper objectMapper){
//1.生成HttpClient对象并设置参数
this.objectMapper = objectMapper;
this.httpClient = new HttpClient();
//连接超时时间
this.httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(5000);
//读写超时时间
this.httpClient.getParams().setParameter(HttpConnectionParams.SO_TIMEOUT,5000);
}
/**
*
* @param url
* @param param 这里使用泛型和Object都是一样的,不影响
* @return
* @throws IOException
* @throws IllegalAccessException
*/
public <R> R doGet(String url,Object param,Class<R> returnType,String token) throws IOException, IllegalAccessException {
//2.生成GetMethod对象并设置参数
if(Objects.nonNull(param)){
List<String> params = new ArrayList<>();
Class<?> clazz = param.getClass();
Field[] declaredFields = clazz.getDeclaredFields();
for (Field declaredField : declaredFields) {
declaredField.setAccessible(true);
Object o = declaredField.get(param);
if (declaredField.getType().equals(String.class)) {
//这里拼接的时候注意要使用URL编码
String s = (String) declaredField.get(param);
s = URLEncoder.encode(s);
o = s;
}
params.add(declaredField.getName() + "=" + o);
}
String paramStr = params.stream().collect(Collectors.joining("&"));
url = url+"?"+paramStr;
}
GetMethod getMethod = new GetMethod(url);
//设置get请求超时为5秒
//getMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 5000);
//设置请求重试处理,用的是默认的重试处理:请求三次
getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler());
//设置请求头
//(可选)设置请求头鉴权信息
if(StringUtils.isNotBlank(token)){
getMethod.addRequestHeader("Authorization",token);
}
String response = "";
//3.执行HTTP GET 请求
try {
int statusCode = this.httpClient.executeMethod(getMethod);
//4.判断访问的状态码
if (statusCode != HttpStatus.SC_OK) {
log.error("请求出错:" + getMethod.getStatusLine());
return null;
}
//5.处理HTTP响应内容
//HTTP响应头部信息,这里简单打印
Header[] headers = getMethod.getResponseHeaders();
for(Header h : headers) {
log.info(h.getName() + "---------------" + h.getValue());
}
//读取HTTP响应内容,这里简单打印网页内容
//读取为字节数组
byte[] responseBody = getMethod.getResponseBody();
response = new String(responseBody, "UTF-8");
log.info(response);
//读取为InputStream,在网页内容数据量大时候推荐使用
//InputStream response = getMethod.getResponseBodyAsStream();
} finally {
//6.释放连接
getMethod.releaseConnection();
}
/**
* 如果是List<T>这种带泛型的对象,则需要使用TypeReference<类型> typeRef = new TypeReference...
* 注意日期的类型,需要事前设置类型转换器
*/
R jsonResult = objectMapper.readValue(response, returnType);
return jsonResult;
}
测试程序:
/**
* 使用HttpURLConnection发送Post请求
*/
@SneakyThrows
public TestHttpAccessResponse sendHttpGetRequestByOldHttpClient() {
TestHttpAccessRequest request = new TestHttpAccessRequest();
request.setAge(16);
request.setName("刘伞");
request.setAddress("佛山市");
String httpUrl = "http://localhost:8082/nacos-service-provider/testHttpAccessGet";
/**
* 如果是List<T>这种带泛型的对象,则需要使用TypeReference<类型> typeRef = new TypeReference...
* 注意日期的类型,需要事前设置类型转换器
*/
String token = httpClientService.getToken();
JsonResult jsonResult = httpClientService.doGet(httpUrl, request, JsonResult.class,token);
if (Objects.isNull(jsonResult) || !ReturnCode.SUCCESS.getCode().equals(jsonResult.getCode())) {
if (Objects.isNull(jsonResult)) {
throw new BizException(ReturnCode.ERROR);
} else {
throw new BizException(jsonResult.getCode(), jsonResult.getMessage());
}
}
/**
* 由于我做了统一的返回体JsonResult,所以还需要再转一遍
*/
TestHttpAccessResponse response = objectMapper.convertValue(jsonResult.getData(), TestHttpAccessResponse.class);
return response;
}
结果:
无授权
{
"code": "40026",
"message": "token不存在或过期,请登录",
"data": null
}
成功:
{
"code": "40000",
"message": "操作成功",
"data": {
"name": "刘伞",
"age": 16,
"address": "佛山市"
}
}
Post方式请求
请求方法::
@Slf4j
@Service
public class HttpClientService {
private final ObjectMapper objectMapper;
private final HttpClient httpClient;
public HttpClientService(ObjectMapper objectMapper){
this.objectMapper = objectMapper;
this.httpClient = new HttpClient();
//连接超时时间
this.httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(5000);
//读写超时时间
this.httpClient.getParams().setParameter(HttpConnectionParams.SO_TIMEOUT,5000);
}
public <R> R doPost(String httpUrl,Object param,Class<R> returnType) throws IOException {
String json = "";
if(Objects.nonNull(param)){
json = objectMapper.writeValueAsString(param);
}
PostMethod postMethod = new PostMethod(httpUrl);
//postMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 5000);
//这里accept表示的是可接收的返回值类型,一般设置为*/*
//如果不设置将会使用浏览器的默认值,不过一般都会支持*/*,为了保险还是加上吧
//如果返回值类型不匹配则会提示No content to map due to end-of-input,可以尝试设置text/html试一下
postMethod.addRequestHeader("accept", "*/*");
//HTTP1.1协议默认使用Keep-Alive,如果是1.0的HTTP协议需要设置
//Keep-Alive发送请求时性能更高
postMethod.addRequestHeader("connection", "Keep-Alive");
//告诉服务器请求的媒体类型,目前基本都是用json,所以设置json格式传送
//即使不设置也是application/json,可能由于下面用到了StringRequestEntity、
//但是我注释了下面的StringRequestEntity还是application/json,目前搞不懂,所以暂时这么设置了
postMethod.addRequestHeader("Content-Type", "application/json;charset=UTF-8");
//设置下面这个Header,没什么用,注释了也能正常访问
//postMethod.addRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36");
//添加请求参数
RequestEntity se = new StringRequestEntity(json, "application/json", "UTF-8");
postMethod.setRequestEntity(se);
StringBuilder res = new StringBuilder();
try {
//必须要UTF-8,不然中文乱码
//3. 执行方法
int code = httpClient.executeMethod(postMethod);
//4. 处理状态码
if(code!=200){
log.error("请求出错:"+postMethod.getStatusLine());
//这里最好是抛出一个异常
return null;
}
//5. 正常,进行处理
//方法1:拿到字节数据后使用new String
byte[] responseBody = postMethod.getResponseBody();
res.append(new String(responseBody,"UTF-8"));
log.info(res.toString());
//方法2:逐行读,在网页内容数据量大时候推荐使用
/*InputStream responseStream = postMethod.getResponseBodyAsStream();
InputStreamReader reader = new InputStreamReader(responseStream,"UTF-8");
BufferedReader bufferedReader = new BufferedReader(reader);
String line = null;
while((line = bufferedReader.readLine()) !=null){
res.append(line).append("\r\n");
}*/
log.info(res.toString());
}finally {
//6. 释放连接
postMethod.releaseConnection();
}
R r = objectMapper.readValue(res.toString(), returnType);
return r;
}
}
请求头设置:
1)accept=*/*
告诉服务器,客户端这边期望接收的媒体类型
2)connection=Keep-Alive
: 使用长连接,可以不用频繁的建立/释放连接,节省资源
3)Content-Type=application/json;charset=UTF-8
: 告诉服务器,客户端发送的内容是什么媒体类型
最后一个user-Agent我也不理解是什么意思,注释了对访问没有影响。
测试程序:
/**
* 使用HttpURLConnection发送Post请求
*/
@SneakyThrows
public TestHttpAccessResponse sendHttpRequestByOldHttpClient() {
TestHttpAccessRequest request = new TestHttpAccessRequest();
request.setAge(16);
request.setName("刘伞");
request.setAddress("佛山市");
String httpUrl = "http://localhost:8082/nacos-service-provider/testHttpAccess";
// String httpUrl = "http://198.168.22.11:8085/nacos-service-provider/testHttpAccess";
/**
* 如果是List<T>这种带泛型的对象,则需要使用TypeReference<类型> typeRef = new TypeReference...
* 注意日期的类型,需要事前设置类型转换器
*/
String token = httpClientService.getToken();
JsonResult jsonResult = httpClientService.doPost(httpUrl, request, JsonResult.class,token);
if (Objects.isNull(jsonResult) || !ReturnCode.SUCCESS.getCode().equals(jsonResult.getCode())) {
if (Objects.isNull(jsonResult)) {
throw new BizException(ReturnCode.ERROR);
} else {
throw new BizException(jsonResult.getCode(), jsonResult.getMessage());
}
}
/**
* 由于我做了统一的返回体JsonResult,所以还需要再转一遍
* 这里不封装进去是因为,除了JsonResult可能还会有其他的返回体
*/
TestHttpAccessResponse response = objectMapper.convertValue(jsonResult.getData(), TestHttpAccessResponse.class);
return response;
}
结果:
无授权
{
"code": "40026",
"message": "token不存在或过期,请登录",
"data": null
}
成功:
{
"code": "40000",
"message": "操作成功",
"data": {
"name": "刘伞",
"age": 16,
"address": "佛山市"
}
}
关键属性设置
连接超时时间
connectionTimeout
: 连接超时的时间,经过查看源码,在执行httpConnection.open()
方法的时候使用到,实际上就是new Socket()
的超时时间。当遇到一个无法ping通的ip时会出现connectionTimeout
的问题,如:
/**
* 使用HttpURLConnection发送Post请求
*/
@SneakyThrows
public TestHttpAccessResponse sendHttpRequestByOldHttpClient() {
TestHttpAccessRequest request = new TestHttpAccessRequest();
request.setAge(16);
request.setName("刘伞");
request.setAddress("佛山市");
//这里我把请求的url的ip和port设置成了一个无法ping通的值
//并且,httpClient设置了5000ms的链接超时
String httpUrl = "http://198.168.22.11:8085/nacos-service-provider/testHttpAccess";
/**
* 如果是List<T>这种带泛型的对象,则需要使用TypeReference<类型> typeRef = new TypeReference...
* 注意日期的类型,需要事前设置类型转换器
*/
JsonResult jsonResult = httpClientService.doPost(httpUrl, request, JsonResult.class);
//......
}
结果:
大概经过了5s(调试),因为没办法创建连接而提示异常了,且没有重试
如果不设置的话,大概会有20s左右的超时时间,失败后会重试3次。
读超时时间(一定要设置)
有两种设置方法
- 对连接对象设置读超时时间:
this.httpClient.getParams().setParameter(HttpClientParams.SO_TIMEOUT,5000);
2)对方法对象设置读写超时时间:postMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 5000);
(优先级高)
设置的必要性:如果不设置读写超时时间,访问这个URL会一直等待,直到读写完成。我在公司里面某一次定时任务的生产事故,就是因为没有设置读超时时间,结果定时任务一直卡着不动,又查找不出问题。
最后通过打日志才发现卡在请求外部服务的接口里。
如果设置了读超时时间,当服务提供者的接口在指定时间内没有返回,则会提示SocketTimeoutException
异常。
各种网络异常
ConnectTimeoutException/ConnectException
没有设置超时时间时:java.net.ConnectException: Connection timed out: connect
设置了超时时间:org.apache.commons.httpclient.ConnectTimeoutException: The host did not accept the connection within timeout of 5000 ms
这种情况可能出现在ip 地址ping不通,或者是服务器(防火墙等)丢弃了该请求报文包,也可能是服务器应答太慢,又或者存在间歇性的问题(这种情况很难从日志文件中排查问题)。
目前我通过一个ping不通的ip地址重现了该错误。
Socket超时:SocketTimeoutException
java.net.SocketTimeoutException: Read timed out
Socket可以设置SoTimeout表示读写的超时时间,如果不设置默认为0,表示没有时间限制;
也可以说是服务端与客户端传输数据包之间的时间间隔,超过这个间隔将抛出 java.net.SocketTimeoutException: Read timed out
相关阅读
Java实现HTTP请求的几种方式-HttpURLConnection(一)
Java实现HTTP请求的几种方式-CloseableHttpClient(三)
Java实现HTTP请求的几种方式-RestTemplate(四)
Java实现HTTP请求的几种方式-OKHttp(五)