HttpClient介绍及SpringBoot整合
1.HttpClient介绍
HTTP 协议是现在 Internet 上使用得最多、最重要的协议,越来越多的 Java 应用程序需要直接通过 HTTP 协议来访问网络资源。虽然在 JDK 的 java net包中已经提供了访问 HTTP 协议的基本功能,但是对于大部分应用程序来说,JDK 库本身提供的功能还不够丰富和灵活。HttpClient 是 Apache Jakarta Common 下的子项目,用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。HttpClient 已经应用在很多的项目中,比如 Apache Jakarta 上很著名的另外两个开源项目 Cactus 和 HTMLUnit 都使用了 HttpClient。
2.功能介绍
以下列出的是 HttpClient 提供的主要的功能,更多详细的功能可以参见 HttpClient 的主页。
(1)实现了所有 HTTP 的方法(GET,POST,PUT,HEAD 等)
(2)支持自动转向
(3)支持 HTTPS 协议
(4)支持代理服务器等
3.导入jar包
<!--添加httpClient依赖 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
4.常见问题
4.1.字符编码
某目标页的编码可能出现在两个地方,第一个地方是服务器返回的http头中,另外一个地方是得到的html/xml页面中。
在http头的Content-Type字段可能会包含字符编码信息。例如可能返回的头会包含这样子的信息:Content-Type: text/html; charset=UTF-8。这个头信息表明该页的编码是UTF-8,但是服务器返回的头信息未必与内容能匹配上。比如对于一些双字节语言国家,可能服务器返回的编码类型是UTF-8,但真正的内容却不是UTF-8编码的,因此需要在另外的地方去得到页面的编码信息;但是如果服务器返回的编码不是UTF-8,而是具体的一些编码,比如gb2312等,那服务器返回的可能是正确的编码信息。通过method对象的getResponseCharSet()方法就可以得到http头中的编码信息。
对于象xml或者html这样的文件,允许作者在页面中直接指定编码类型。比如在html中会有这样的标签;或者在xml中会有<?xml version="1.0" encoding="gb2312"?>这样的标签,在这些情况下,可能与http头中返回的编码信息冲突,需要用户自己判断到底那种编码类型应该是真正的编码。
4.2.自动转向
根据RFC2616中对自动转向的定义,主要有两种:301和302。301表示永久的移走(Moved Permanently),当返回的是301,则表示请求的资源已经被移到一个固定的新地方,任何向该地址发起请求都会被转到新的地址上。302表示暂时的转向,比如在服务器端的servlet程序调用了sendRedirect方法,则在客户端就会得到一个302的代码,这时服务器返回的头信息中location的值就是sendRedirect转向的目标地址。
HttpClient支持自动转向处理,但是象POST和PUT方式这种要求接受后继服务的请求方式,暂时不支持自动转向,因此如果碰到POST方式提交后返回的是301或者302的话需要自己处理。就像刚才在POSTMethod中举的例子:如果想进入登录BBS后的页面,必须重新发起登录的请求,请求的地址可以在头字段location中得到。不过需要注意的是,有时候location返回的可能是相对路径,因此需要对location返回的值做一些处理才可以发起向新地址的请求。
另外除了在头中包含的信息可能使页面发生重定向外,在页面中也有可能会发生页面的重定向。引起页面自动转发的标签是:。如果你想在程序中也处理这种情况的话得自己分析页面来实现转向。需要注意的是,在上面那个标签中url的值也可以是一个相对地址,如果是这样的话,需要对它做一些处理后才可以转发。
5.SpringBoot整合HttpClient
5.1 配置文件httpclient.properties
#最大连接数
http.maxTotal = 1000
#并发数
http.defaultMaxPerRoute = 20
#创建连接的最长时间
http.connectTimeout=5000
#从连接池中获取到连接的最长时间
http.connectionRequestTimeout=500
#数据传输的最长时间
http.socketTimeout=5000
#提交请求前测试连接是否可用
http.staleConnectionCheckEnabled=true
5.2 配置类
@Configuration //注解--配置类
@PropertySource(value="classpath:/properties/httpClient.properties")
public class HttpClientConfig {
@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; //提交时检查链接是否可用
//定义httpClient链接池
@Bean(name="httpClientConnectionManager")
public PoolingHttpClientConnectionManager getPoolingHttpClientConnectionManager() {
PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager();
manager.setMaxTotal(maxTotal); //设定最大链接数
manager.setDefaultMaxPerRoute(defaultMaxPerRoute); //设定并发链接数
return manager;
}
//定义HttpClient
/**
* 实例化连接池,设置连接池管理器。
* 这里需要以参数形式注入上面实例化的连接池管理器
@Qualifier 指定bean标签进行注入
*/
@Bean(name = "httpClientBuilder")
public HttpClientBuilder getHttpClientBuilder(@Qualifier("httpClientConnectionManager")PoolingHttpClientConnectionManager httpClientConnectionManager){
//HttpClientBuilder中的构造方法被protected修饰,所以这里不能直接使用new来实例化一个HttpClientBuilder,可以使用HttpClientBuilder提供的静态方法create()来获取HttpClientBuilder对象
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
httpClientBuilder.setConnectionManager(httpClientConnectionManager);
return httpClientBuilder;
}
/**
* 注入连接池,用于获取httpClient
* 1.定义池对象
* 2.创建builder对象 关联池
* 3.利用builder实例化对象
*/
@Bean
public CloseableHttpClient getCloseableHttpClient(@Qualifier("httpClientBuilder") HttpClientBuilder httpClientBuilder){
return httpClientBuilder.build();
}
/**
* Builder是RequestConfig的一个内部类
* 通过RequestConfig的custom方法来获取到一个Builder对象
* 设置builder的连接信息
*/
@Bean(name = "builder")
public RequestConfig.Builder getBuilder(){
RequestConfig.Builder builder = RequestConfig.custom();
return builder.setConnectTimeout(connectTimeout)
.setConnectionRequestTimeout(connectionRequestTimeout)
.setSocketTimeout(socketTimeout)
.setStaleConnectionCheckEnabled(staleConnectionCheckEnabled);
}
/**
* 使用builder构建一个RequestConfig对象
* @param builder
* @return
*/
@Bean
public RequestConfig getRequestConfig(@Qualifier("builder") RequestConfig.Builder builder){
return builder.build();
}
}
5.3 关闭超时连接
@Component //交给spring容器管理
public class HttpClientClose extends Thread{
@Autowired
private PoolingHttpClientConnectionManager manage;
//开关 volatitle表示多线程可变数据,一个线程修改,其他线程立即修改
private volatile boolean shutdown;
public HttpClientClose() {
//System.out.println("执行构造方法,实例化对象");
//线程开启启动
this.start();
}
//以异步的形式关闭httpClient对象
@Override
public void run() {
try {
//如果服务没有关闭,执行线程
while(!shutdown) {
synchronized (this) {
wait(5000); //等待5秒
//System.out.println("线程开始执行,关闭超时链接");
//关闭超时的链接
PoolStats stats = manage.getTotalStats();
int av = stats.getAvailable(); //获取可用的线程数量
int pend = stats.getPending(); //获取阻塞的线程数量
int lea = stats.getLeased(); //获取当前正在使用的链接数量
int max = stats.getMax();
manage.closeExpiredConnections();
}
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException();
}
super.run();
}
//关闭清理无效连接的线程
@PreDestroy //容器关闭时执行该方法.
public void shutdown() {
shutdown = true;
synchronized (this) {
//System.out.println("关闭全部链接!!");
notifyAll(); //全部从等待中唤醒.执行关闭操作;
}
}
}
6.实现httpClient工具API
6.1编辑HttpClientService
@Service //将对象交给Spring容器管理
public class HttpClientService {
@Autowired
private CloseableHttpClient httpClient;
@Autowired
private RequestConfig requestConfig;
/**
* 需求:给定url地址,则可以自动的发起请求,并且将返回值结果返回给用户.
* 参数分析:
* 1.用户的url参数地址
* 2.判断用户是否需要携带参数 使用Map集合封装数据.
* 3.指定字符集编码 需要用户自己指定
* 返回值: String类型
*/
public String doGet(String url,Map<String,String> params,String charset) {
//1.判断用户是否传递字符集编码
if(StringUtils.isEmpty(charset)) {
//设定默认值
charset = "UTF-8";
}
/**
* 2.判断用户是否携带参数
* 没有带参数: http://manage.jt.com/item
* 带参数: http://manage.jt.com/item?id=1&name=xxxx&;
* 补充知识: entry={key=value} Map~~~>entry~~~~~key:value
*/
if(params !=null) {//如果参数不为null,则需要进行参数拼接.
url += "?";
//map集合遍历. 从中动态的获取key=value.
for (Map.Entry<String,String> entry : params.entrySet()) {
String key = entry.getKey().trim(); //去除多余的空格
String value = entry.getValue().trim();
url += key+"="+value+"&";
}
//去除多余的&符
url =url.substring(0, url.length()-1);
}
//3.定义请求对象
HttpGet httpGet = new HttpGet(url);
//添加超时配置
httpGet.setConfig(requestConfig);
//4.发起请求
try {
HttpResponse httpResponse = httpClient.execute(httpGet);
//5.判断用户的请求是否正常
if(httpResponse.getStatusLine().getStatusCode()==200) {
//6.动态获取返回值
return EntityUtils.toString(httpResponse.getEntity());
}
int status = httpResponse.getStatusLine().getStatusCode();
throw new RuntimeException("获取后台错误的状态码:"+status+"|请求路径为:url"+url);
} catch (IOException e) {
e.printStackTrace();
//将检查异常,转化为运行时异常
throw new RuntimeException(e);
}
}
/**
* 重载多个doGet方法 满足不同的用户需要
* @param url
* @param params
* @param charset
* @return
*/
public String doGet(String url) {
return doGet(url, null, null);
}
public String doGet(String url,Map<String,String> params) {
return doGet(url, params, null);
}
//实现httpClient POST提交
public String doPost(String url,Map<String,String> params,String charset){
String result = null;
//1.定义请求类型
HttpPost post = new HttpPost(url);
post.setConfig(requestConfig); //定义超时时间
//2.判断字符集是否为null
if(StringUtils.isEmpty(charset)){
charset = "UTF-8";
}
//3.判断用户是否传递参数
if(params !=null){
//3.2准备List集合信息
List<NameValuePair> parameters = new ArrayList<>();
//3.3将数据封装到List集合中
for (Map.Entry<String,String> entry : params.entrySet()) {
parameters.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
}
//3.1模拟表单提交
try {
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters,charset); //采用u8编码
//3.4将实体对象封装到请求对象中
post.setEntity(formEntity);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
//4.发送请求
try {
CloseableHttpResponse response = httpClient.execute(post);
//4.1判断返回值状态
if(response.getStatusLine().getStatusCode() == 200) {
//4.2表示请求成功
result = EntityUtils.toString(response.getEntity(),charset);
}else{
System.out.println("获取状态码信息:"+response.getStatusLine().getStatusCode());
throw new RuntimeException();
}
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
public String doPost(String url){
return doPost(url, null, null);
}
public String doPost(String url,Map<String,String> params){
return doPost(url, params, null);
}
public String doPost(String url,String charset){
return doPost(url, null, charset);
}
}
6.2编辑测试类
@Autowired
private HttpClientService httpClient;
@Test
public void testGet() {
String url = "https://www.baidu.com";
String result = httpClient.doGet(url);
System.out.println(result);
}
```