HttpClient实现跨域请求,我们也可以理解为不同系统间的接口调用,那么什么为跨域呢?可以总结为不同域名,不同端口或者相同域名不同端口的系统。分布式项目是现在市场的主流,跨域请求在项目中各子系统中的联系也是必不可少。熟悉的跨域请求技术一个jsonp,一个HttpClient,jsonp底层是通过<script>调用回调函数并传递参数实现跨域,但他只能实现get请求,有时间整理出来再与大家分享。废话不多说,下面一起探讨HttpClient。
HttpClient其主要作用就是通过Http协议,向某个URL地址发起请求,并且获取响应结果,我们通过一个简单的案例就可以快速入门
1.1发起Get请求
public class DoGET {
public static void main(String[] args) throws Exception {
// 创建Httpclient对象,相当于打开了浏览器
CloseableHttpClient httpclient = HttpClients.createDefault();
// 创建HttpGet请求,相当于在浏览器输入地址
HttpGet httpGet = new HttpGet("http://www.baidu.com/");
CloseableHttpResponse response = null;
try {
// 执行请求,相当于敲完地址后按下回车。获取响应
response = httpclient.execute(httpGet);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
// 解析响应,获取数据
String content = EntityUtils.toString(response.getEntity(), "UTF-8");
System.out.println(content);
}
} finally {
if (response != null) {
// 关闭资源
response.close();
}
// 关闭浏览器
httpclient.close();
}
}
}
1.2带参数的get请求
public class DoGETParam {
public static void main(String[] args) throws Exception {
// 创建Httpclient对象
CloseableHttpClient httpclient = HttpClients.createDefault();
// 创建URI对象,并且设置请求参数
URI uri = new URIBuilder("http://www.baidu.com/s").setParameter("wd", "java").build();
// 创建http GET请求
HttpGet httpGet = new HttpGet(uri);
CloseableHttpResponse response = null;
try {
// 执行请求
response = httpclient.execute(httpGet);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
// 解析响应数据
String content = EntityUtils.toString(response.getEntity(), "UTF-8");
System.out.println(content);
}
} finally {
if (response != null) {
response.close();
}
httpclient.close();
}
}
}
1.3发起post请求
public class DoPOST {
public static void main(String[] args) throws Exception {
// 创建Httpclient对象
CloseableHttpClient httpclient = HttpClients.createDefault();
// 创建http POST请求
HttpPost httpPost = new HttpPost("http://www.oschina.net/");
// 把自己伪装成浏览器。否则开源中国会拦截访问
httpPost.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36");
CloseableHttpResponse response = null;
try {
// 执行请求
response = httpclient.execute(httpPost);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
// 解析响应数据
String content = EntityUtils.toString(response.getEntity(), "UTF-8");
System.out.println(content);
}
} finally {
if (response != null) {
response.close();
}
// 关闭浏览器
httpclient.close();
}
}
}
1.4带参数post请求
public class DoPOSTParam {
public static void main(String[] args) throws Exception {
// 创建Httpclient对象
CloseableHttpClient httpclient = HttpClients.createDefault();
// 创建http POST请求,访问开源中国
HttpPost httpPost = new HttpPost("http://www.oschina.net/search");
// 根据开源中国的请求需要,设置post请求参数
List<NameValuePair> parameters = new ArrayList<NameValuePair>(0);
parameters.add(new BasicNameValuePair("scope", "project"));
parameters.add(new BasicNameValuePair("q", "java"));
parameters.add(new BasicNameValuePair("fromerr", "8bDnUWwC"));
// 构造一个form表单式的实体
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters);
// 将请求实体设置到httpPost对象中
httpPost.setEntity(formEntity);
CloseableHttpResponse response = null;
try {
// 执行请求
response = httpclient.execute(httpPost);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
// 解析响应体
String content = EntityUtils.toString(response.getEntity(), "UTF-8");
System.out.println(content);
}
} finally {
if (response != null) {
response.close();
}
// 关闭浏览器
httpclient.close();
}
}
}
2.5使用连接池管理器
public class HttpConnectManager {
public static void main(String[] args) throws Exception {
// 构建连接池管理器对象
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
// 设置最大连接数
cm.setMaxTotal(200);
// 设置每个主机地址的并发数
cm.setDefaultMaxPerRoute(20);
// 构建请求配置信息
RequestConfig config = RequestConfig.custom().setConnectTimeout(1000) // 创建连接的最长时间
.setConnectionRequestTimeout(500) // 从连接池中获取到连接的最长时间
.setSocketTimeout(10 * 1000) // 数据传输的最长时间
.setStaleConnectionCheckEnabled(true) // 提交请求前测试连接是否可用
.build();
// 构建HttpClient对象,把连接池、请求配置对象作为参数
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm)
.setDefaultRequestConfig(config).build();
doGet(httpClient);
doGet(httpClient);
}
public static void doGet(CloseableHttpClient httpClient) throws Exception {
// 创建http GET请求
HttpGet httpGet = new HttpGet("http://www.baidu.com/");
CloseableHttpResponse response = null;
try {
// 执行请求
response = httpClient.execute(httpGet);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
String content = EntityUtils.toString(response.getEntity(), "UTF-8");
System.out.println("内容长度:" + content.length());
}
} finally {
if (response != null) {
response.close();
}
// 此处不能关闭httpClient,如果关闭httpClient,连接池也会销毁
// httpClient.close();
}
}
}
注意:
1) 在案例中的配置相当重要,特别是各种有关超时时间的配置。如果不配置1,可能就会导致坏连接一直阻塞,影响其它请 求。或者导致无法获取结果,产生意想不到的错误。
2) HttpClient对象不要关闭,否则会导致整个连接池失效。
实际开发中,HttpClient虽然有连接池来管理连接。但是,如果访问的服务端已经断开了连接,作为客户端的HttpClient是不知道的。此时,池中的这个连接就是一个无效的连接。如果这样的连接很多,就会造成有效连接数的减少,服务器的压力变大,直至崩溃。因此,定期清理无效的链接显得尤为重要。
定期清理无效链接
public class ClientEvictExpiredConnections {
public static void main(String[] args) throws Exception {
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
// 设置最大连接数
cm.setMaxTotal(200);
// 设置每个主机地址的并发数
cm.setDefaultMaxPerRoute(20);
// 开启线程,执行清理链接的任务
new IdleConnectionEvictor(cm).start();
}
// 创建内部类,继承Thread,成为线程类
public static class IdleConnectionEvictor extends Thread {
private final HttpClientConnectionManager connMgr;
private volatile boolean shutdown;
public IdleConnectionEvictor(HttpClientConnectionManager connMgr) {
this.connMgr = connMgr;
}
// 定义线程任务,定时关闭失效的连接
@Override
public void run() {
try {
while (!shutdown) {
synchronized (this) {
// 每隔5秒钟一次
wait(5000);
// 关闭失效的连接
connMgr.closeExpiredConnections();
}
}
} catch (InterruptedException ex) {
// 结束
}
}
public void shutdown() {
shutdown = true;
synchronized (this) {
notifyAll();
}
}
}
}
SSM中整合HttpClient
要在项目中整合HttpClient,其实就是把HttpClient注册到Spring容器中。
而一般注册一个Bean,也有两种方式:XML配置或者注解。
我们选哪种?
方式1,XML配置
l 新建applicationContext-httpClient.xml文件:
l 编写配置
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
<!-- 配置连接池管理器 -->
<bean id="httpClientConnectionManager" class="org.apache.http.impl.conn.PoolingHttpClientConnectionManager">
<!-- 配置最大连接数 -->
<property name="maxTotal" value="${http.maxTotal}" />
<!-- 配置每个主机地址的最大连接数 -->
<property name="defaultMaxPerRoute" value="${http.defaultMaxPerRoute}" />
</bean>
<!-- 配置RequestConfigBuilder -->
<bean id="requestConfigBuilder" class="org.apache.http.client.config.RequestConfig.Builder">
<!-- 创建连接的最长时间 -->
<property name="connectTimeout" value="${http.connectTimeout}" />
<!-- 从连接池中获取到连接的最长时间 -->
<property name="connectionRequestTimeout" value="${http.connectionRequestTimeout}" />
<!-- 数据传输的最长时间 -->
<property name="socketTimeout" value="${http.socketTimeout}" />
<!-- 提交请求前测试连接是否可用 -->
<property name="staleConnectionCheckEnabled" value="${http.staleConnectionCheckEnabled}" />
</bean>
<!-- 配置RequestConfig -->
<bean id="requestConfig" factory-bean="requestConfigBuilder" factory-method="build"/>
<!-- 配置HttpClientBuilder -->
<bean id="httpClientBuilder" class="org.apache.http.impl.client.HttpClientBuilder">
<!-- 注入连接池管理器 -->
<property name="connectionManager" ref="httpClientConnectionManager" />
<!-- 注入默认的请求配置对象 -->
<property name="defaultRequestConfig" ref="requestConfig" />
</bean>
<!-- 配置HttpClient -->
<bean factory-bean="httpClientBuilder" factory-method="build"></bean>
</beans>
注意,在上面的配置中,引用了${},外部属性文件。所以,在Spring的核心配置中添加配置文件:appliationContext.xml中
l httpClient.properties :资源文件放在项目的根目录下 resource
l 定义一个HttpClient的通用Service:提供get请求和POST请求的方法
@Service
public class XmlApiService {
// 注入在XML中已经配置好的HttpClient对象
@Autowired
private CloseableHttpClient httpClient;
/**
* 无参的get请求
*/
public String doGet(String uri) throws IOException {
// 创建HttpGet请求,相当于在浏览器输入地址
HttpGet httpGet = new HttpGet(uri);
CloseableHttpResponse response = null;
try {
// 执行请求,相当于敲完地址后按下回车。获取响应
response = httpClient.execute(httpGet);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
// 解析响应,获取数据
return EntityUtils.toString(response.getEntity(), "UTF-8");
}
} finally {
if (response != null) {
response.close();
}
}
return null;
}
/**
* 有参get请求
*/
public String doGet(String uri, Map<String, String> params) throws Exception {
// 创建地址构建器
URIBuilder builder = new URIBuilder(uri);
// 拼接参数
for (Map.Entry<String, String> me : params.entrySet()) {
builder.addParameter(me.getKey(), me.getValue());
}
return doGet(builder.build().toString());
}
/**
* 有参POST请求
*/
public String doPost(String uri, Map<String, String> params) throws ParseException, IOException {
// 创建http POST请求
HttpPost httpPost = new HttpPost(uri);
httpPost.setHeader("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36");
if (params != null) {
// 设置参数
List<NameValuePair> parameters = new ArrayList<NameValuePair>(0);
for (Map.Entry<String, String> me : params.entrySet()) {
parameters.add(new BasicNameValuePair(me.getKey(), me.getValue()));
}
// 构造一个form表单式的实体
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters);
// 将请求实体设置到httpPost对象中
httpPost.setEntity(formEntity);
}
CloseableHttpResponse response = null;
try {
// 执行请求
response = httpClient.execute(httpPost);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
String content = EntityUtils.toString(response.getEntity(), "UTF-8");
System.out.println(content);
}
} finally {
if (response != null) {
response.close();
}
}
return null;
}
/**
* 无参POST请求
*/
public String doPost(String uri) throws ParseException, IOException {
return doPost(uri, null);
}
}
l 先把清理无效连接的线程复制到项目中:
注意修改构造函数,在构造函数中启动当前线程任务,这样当Bean一旦注册,线程就启动了
/**
* 定时清理无效链接的线程
*/
public class IdleConnectionEvictor extends Thread {
private final HttpClientConnectionManager connMgr;
private volatile boolean shutdown;
public IdleConnectionEvictor(HttpClientConnectionManager connMgr) {
this.connMgr = connMgr;
// 启动线程
this.start();
}
// 定义线程任务,定时关闭失效的连接
@Override
public void run() {
try {
while (!shutdown) {
synchronized (this) {
// 每隔5秒钟一次
wait(5000);
// 关闭失效的连接
connMgr.closeExpiredConnections();
}
}
} catch (InterruptedException ex) {
// 结束
}
System.out.println("清理任务 即将结束。。");
}
public void shutdown() {
shutdown = true;
synchronized (this) {
notifyAll();
}
}
}
l 在applicationContext-httpclient.xml配置文件中注册该类:
<bean class="com.taotao.common.bean.IdleConnectionEvictor" destroy-method="shutdown">
<constructor-arg index="0" ref="httpClientConnectionManager"></constructor-arg>
</bean>
l 完整的applicationContext-httpclient.xml:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
<!-- 设置请求参数的配置 -->
<bean id="configBuilder" class="org.apache.http.client.config.RequestConfig.Builder">
<!-- 创建连接的最长时间 -->
<property name="connectTimeout" value="${httpclient.connectTimeout}"></property>
<!-- 从连接池中获取到连接的最长时间 -->
<property name="connectionRequestTimeout" value="${httpclient.connectionRequestTimeout}"></property>
<!-- 数据传输的最长时间 -->
<property name="socketTimeout" value="${httpclient.socketTimeout}"></property>
<!-- 提交请求前测试连接是否可用 -->
<property name="staleConnectionCheckEnabled" value="${httpclient.staleConnectionCheckEnabled}"></property>
</bean>
<!-- 配置RequestConfig -->
<bean id="requestConfig" factory-bean="configBuilder" factory-method="build"></bean>
<!-- httpclient连接管理器 -->
<bean id="httpClientConnectionManager" class="org.apache.http.impl.conn.PoolingHttpClientConnectionManager">
<!-- 最大连接数 -->
<property name="maxTotal" value="${httpclient.maxTotal}"></property>
<!-- 设置每个主机地址的并发数 -->
<property name="defaultMaxPerRoute" value="${httpclient.defaultMaxPerRoute}"></property>
</bean>
<!-- httpclient的构造器 -->
<bean id="httpClientBuilder" class="org.apache.http.impl.client.HttpClientBuilder">
<property name="connectionManager" ref="httpClientConnectionManager"></property>
<!-- 注入默认的请求配置对象 -->
<property name="defaultRequestConfig" ref="requestConfig" />
</bean>
<!-- 配置httpclient对象 scope="prototype" : 默认是单例,我们改成prototype,每次重新创建一个实例-->
<bean class="org.apache.http.impl.client.CloseableHttpClient"
factory-bean="httpClientBuilder" factory-method="build" >
</bean>
<!-- 自定义定期关闭无效HttpClient连接的类 -->
<bean class="com.taotao.common.bean.IdleConnectionEvictor" destroy-method="shutdown">
<constructor-arg index="0" ref="httpClientConnectionManager"></constructor-arg>
</bean>
</beans>
l 测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring/applicationContext*.xml")
public class ApiServiceTest {
@Autowired
private XmlApiService apiService;
@Test
public void testDoGetString() throws IOException {
String content = this.apiService.doGet("http://www.baidu.com");
System.out.println(content);
}
@Test
public void testDoGetStringMapOfStringString() throws Exception {
Map<String,String> params = new HashMap<>();
params.put("categoryId", "13");
params.put("rows", "6");
params.put("callback", "func");
String content = this.apiService.doGet("http://manage.taotao.com/rest/api/content",params);
System.out.println(content);
}
@Test
public void testDoPostStringMapOfStringString() throws ParseException, IOException {
Map<String,String> params = new HashMap<>();
params.put("scope", "project");
params.put("q", "java");
params.put("fromerr", "8bDnUWwC");
String content = this.apiService.doPost("http://www.oschina.net/search", params);
System.out.println(content);
}
@Test
public void testDoPostString() {
fail("Not yet implemented");
}
}
一切OK,整合成功
方式2:注解方式
直接在APIService中,通过@value注解来注入 配置参数。
然后通过@PostConstructor 注解来声明一个初始化方法,在这个方法中完成HttpClient对象的创建。
/**
* 通过HttpClient进行远程调用的Service
*/
@Service
public class ApiService {
@Value("${http.maxTotal}")
private Integer maxTotal;
@Value("${http.defaultMaxPerRoute}")
private Integer defaultMaxPerRoute;
@Value("${http.connectTimeout}")
private Integer connectTimeout;
@Value("${http.connectionRequestTimeout}")
private Integer connectionRequestTimeout;
@Value("${http.socketTimeout}")
private Integer socketTimeout;
@Value("${http.staleConnectionCheckEnabled}")
private Boolean staleConnectionCheckEnabled;
private CloseableHttpClient httpClient;
// PostConstruct注解标明该方法为初始化方法,会在对象构造函数执行完毕后执行
@PostConstruct
public void init() {
// 构建连接池管理器对象
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
// 设置最大连接数
cm.setMaxTotal(maxTotal);
// 设置每个主机地址的并发数
cm.setDefaultMaxPerRoute(defaultMaxPerRoute);
// 构建请求配置信息
RequestConfig config = RequestConfig.custom().setConnectTimeout(connectTimeout) // 创建连接的最长时间
.setConnectionRequestTimeout(connectionRequestTimeout) // 从连接池中获取到连接的最长时间
.setSocketTimeout(socketTimeout) // 数据传输的最长时间
.setStaleConnectionCheckEnabled(staleConnectionCheckEnabled) // 提交请求前测试连接是否可用
.build();
// 构建HttpClient对象,把连接池、请求配置对象作为参数
httpClient = HttpClients.custom().setConnectionManager(cm).setDefaultRequestConfig(config).build();
}
/**
* 无参的get请求
*/
public String doGet(String uri) throws IOException {
// 创建HttpGet请求,相当于在浏览器输入地址
HttpGet httpGet = new HttpGet(uri);
CloseableHttpResponse response = null;
try {
// 执行请求,相当于敲完地址后按下回车。获取响应
response = httpClient.execute(httpGet);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
// 解析响应,获取数据
return EntityUtils.toString(response.getEntity(), "UTF-8");
}
} finally {
if (response != null) {
response.close();
}
}
return null;
}
/**
* 有参get请求
*/
public String doGet(String uri, Map<String, String> params) throws Exception {
// 创建地址构建器
URIBuilder builder = new URIBuilder(uri);
// 拼接参数
for (Map.Entry<String, String> me : params.entrySet()) {
builder.addParameter(me.getKey(), me.getValue());
}
return doGet(builder.build().toString());
}
/**
* 有参POST请求
*/
public String doPost(String uri, Map<String, String> params) throws ParseException, IOException {
// 创建http POST请求
HttpPost httpPost = new HttpPost(uri);
httpPost.setHeader("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36");
if (params != null) {
// 设置参数
List<NameValuePair> parameters = new ArrayList<NameValuePair>(0);
for (Map.Entry<String, String> me : params.entrySet()) {
parameters.add(new BasicNameValuePair(me.getKey(), me.getValue()));
}
// 构造一个form表单式的实体
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters);
// 将请求实体设置到httpPost对象中
httpPost.setEntity(formEntity);
}
CloseableHttpResponse response = null;
try {
// 执行请求
response = httpClient.execute(httpPost);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
String content = EntityUtils.toString(response.getEntity(), "UTF-8");
System.out.println(content);
}
} finally {
if (response != null) {
response.close();
}
}
return null;
}
/**
* 无参POST请求
*/
public String doPost(String uri) throws ParseException, IOException {
return doPost(uri, null);
}
}
添加定时清理无效连接的任务
我们这里按照方式2,也就是注解注入的方式来添加定时清理无效链接的任务。
l 首先,在ApiService类中,定义内部类,实现 Runnable接口,成为任务类,编写定时清理的线程任务
// 创建内部类,实现Runnable接口,成为线程任务类
private class IdleConnectionEvictor implements Runnable {
private final HttpClientConnectionManager connMgr;
private volatile boolean shutdown;
public IdleConnectionEvictor(HttpClientConnectionManager connMgr) {
this.connMgr = connMgr;
}
// 定义线程任务,定时关闭失效的连接
@Override
public void run() {
try {
while (!shutdown) {
synchronized (this) {
// 每隔5秒钟一次
wait(5000);
// 关闭失效的连接
connMgr.closeExpiredConnections();
}
}
} catch (InterruptedException ex) {
// 结束
}
}
public void shutdown() {
shutdown = true;
synchronized (this) {
notifyAll();
System.out.println("定时清理任务即将结束。。");
}
}
}
l 在初始化方法,init中,HttpClient对象创建完毕后,创建任务,并开启线程执行
l 定义一个destroy方法,当ApiService被销毁前,结束线程任务