背景:
近期想写一个针对某高校系统的套壳系统,提高其安全与使用便利性,优化用户体验,使用的第三方库为HttpClient。且最近简单了解了MyBatisPlus在MyBatis 的基础上只做增强不做改变的设计,所以简单用HttpClient进行了设计,不再使用原始的设计为工具类。
代码:
HttpClient配置类
package com.xjl.common.config;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* HttpClient配置类
*
* @author lkb
* @version 1.0
*/
@Configuration
@ConfigurationProperties(prefix = "http")
public class HttpClientConfig {
@Value("${http.maxTotal:100}")
private Integer maxTotal;
@Value("${http.defaultMaxPerRoute:20}")
private Integer defaultMaxPerRoute;
@Value("${http.connectTimeout:5000}")
private Integer connectTimeout;
@Value("${http.connectionRequestTimeout:5000}")
private Integer connectionRequestTimeout;
@Value("${http.socketTimeout:5000}")
private Integer socketTimeout;
/**
* 初始化连接池管理器,设置最大总连接数和并发连接数。
*
* @return 配置好的PoolingHttpClientConnectionManager。
*/
@Bean(name = "httpClientConnectionManager")
public PoolingHttpClientConnectionManager getHttpClientConnectionManager() {
PoolingHttpClientConnectionManager httpClientConnectionManager = new PoolingHttpClientConnectionManager();
// 最大总连接数
httpClientConnectionManager.setMaxTotal(maxTotal);
// 并发连接数
httpClientConnectionManager.setDefaultMaxPerRoute(defaultMaxPerRoute);
return httpClientConnectionManager;
}
/**
* 初始化连接池,设置连接池管理器。
* 连接池管理器作为参数被注入。
*
* @param httpClientConnectionManager 先前配置的连接池管理器。
* @return 配置好的HttpClientBuilder。
*/
@Bean(name = "httpClientBuilder")
public HttpClientBuilder getHttpClientBuilder(@Qualifier("httpClientConnectionManager") PoolingHttpClientConnectionManager httpClientConnectionManager) {
// HttpClientBuilder的构造方法是protected的,因此我们使用静态的create()方法来获取实例。
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
httpClientBuilder.setConnectionManager(httpClientConnectionManager);
return httpClientBuilder;
}
/**
* 注入连接池,用于获取httpClient。
*
* @param httpClientBuilder 配置好的HttpClientBuilder。
* @return 配置好的CloseableHttpClient。
*/
@Bean
public CloseableHttpClient getCloseableHttpClient(@Qualifier("httpClientBuilder") HttpClientBuilder httpClientBuilder) {
return httpClientBuilder.build();
}
/**
* Builder是RequestConfig的一个内部类。
* 通过RequestConfig的custom方法获取Builder对象。
* 设置builder的连接信息。
* 这里还可以设置代理、cookieSpec等属性,有需要的话可以在此设置。
*
* @return 配置好的RequestConfig.Builder。
*/
@Bean(name = "builder")
public RequestConfig.Builder getBuilder() {
RequestConfig.Builder builder = RequestConfig.custom();
return builder.setConnectTimeout(connectTimeout)
.setConnectionRequestTimeout(connectionRequestTimeout)
.setSocketTimeout(socketTimeout)
.setCookieSpec(CookieSpecs.IGNORE_COOKIES)
// .setProxy(HttpHost.create("127.0.0.1:8080"))
;
}
/**
* 使用builder构建一个RequestConfig对象。
*
* @param builder 配置好的RequestConfig.Builder。
* @return 配置好的RequestConfig。
*/
@Bean
public RequestConfig getRequestConfig(@Qualifier("builder") RequestConfig.Builder builder) {
return builder.build();
}
public Integer getMaxTotal() {
return maxTotal;
}
public void setMaxTotal(Integer maxTotal) {
this.maxTotal = maxTotal;
}
public Integer getDefaultMaxPerRoute() {
return defaultMaxPerRoute;
}
public void setDefaultMaxPerRoute(Integer defaultMaxPerRoute) {
this.defaultMaxPerRoute = defaultMaxPerRoute;
}
public Integer getConnectTimeout() {
return connectTimeout;
}
public void setConnectTimeout(Integer connectTimeout) {
this.connectTimeout = connectTimeout;
}
public Integer getConnectionRequestTimeout() {
return connectionRequestTimeout;
}
public void setConnectionRequestTimeout(Integer connectionRequestTimeout) {
this.connectionRequestTimeout = connectionRequestTimeout;
}
public Integer getSocketTimeout() {
return socketTimeout;
}
public void setSocketTimeout(Integer socketTimeout) {
this.socketTimeout = socketTimeout;
}
}
HttpClient连接定时关闭线程
package com.xjl.common.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.pool.PoolStats;
import org.springframework.stereotype.Component;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* HttpClient 连接关闭线程
* 在应用关闭时清理无效连接
*
* @author lkb
* @version 1.0
*/
@Component
@Slf4j
public class HttpClientClose extends Thread {
@Resource
private PoolingHttpClientConnectionManager manager;
/**
* 开关 volatitle表示多线程可变数据,一个线程修改,其他线程立即修改
*/
private volatile boolean shutdown;
public HttpClientClose() {
///System.out.println("执行构造方法,实例化对象");
//线程开启启动
this.start();
}
@Override
public void run() {
try {
// 如果服务没有关闭,执行线程
while (!shutdown) {
synchronized (this) {
// 等待5秒
wait(5000);
// 关闭超时的连接
PoolStats stats = manager.getTotalStats();
// 获取可用的线程数量
int av = stats.getAvailable();
// 获取阻塞的线程数量
int pend = stats.getPending();
// 获取当前正在使用的连接数量
int lea = stats.getLeased();
int max = stats.getMax();
manager.closeExpiredConnections();
manager.closeIdleConnections(30L, TimeUnit.SECONDS);
log.debug("Available: {}, Pending: {}, Leased: {}, Max: {}", av, pend, lea, max);
}
}
} catch (InterruptedException e) {
log.error("HttpClientClose thread interrupted.", e);
// 保持中断状态
Thread.currentThread().interrupt();
}
}
/**
* 关闭清理无效连接的线程
*/
@PreDestroy
public void shutdown() {
shutdown = true;
synchronized (this) {
// 全部从等待中唤醒.执行关闭操作;
notifyAll();
}
}
}
使用MyBatisPlus设计思路对HttpClient进行功能增强
package com.xjl.common.service;
import org.apache.http.HttpResponse;
import org.apache.http.entity.mime.content.ContentBody;
import java.io.File;
import java.util.Map;
/**
* @author lkb
* @version 1.0
*/
public interface HttpClientService {
/**
* 发送GET请求并返回响应内容。
*
* @param url 请求的URL
* @return 响应内容
*/
HttpResponse httpGet(String url);
/**
* 发送带参数的GET请求并返回响应内容。
*
* @param url 请求的URL
* @param headers 请求的参数
* @return 响应内容
*/
HttpResponse httpGet(String url, Map<String, String> headers);
/**
* 发送带参数和自定义请求头的GET请求并返回响应内容。
*
* @param url 请求的URL
* @param headers 请求的自定义头部信息
* @param params 请求的参数
* @return 响应内容
*/
HttpResponse httpGet(String url, Map<String, String> headers, Map<String, Object> params);
/**
* 发送POST请求并返回响应内容。
*
* @param url 请求的URL
* @return 响应内容
*/
HttpResponse httpPost(String url);
/**
* 发送带JSON数据的POST请求并返回响应内容。
*
* @param url 请求的URL
* @param json JSON数据
* @return 响应内容
*/
HttpResponse httpPost(String url, String json);
/**
* 发送带参数的POST请求并返回响应内容。
*
* @param url 请求的URL
* @param headers 请求头
* @return 响应内容
*/
HttpResponse httpPost(String url, Map<String, String> headers);
/**
* 发送带参数和自定义请求头的POST请求并返回响应内容。
*
* @param url 请求的URL
* @param headers 请求的自定义头部信息
* @param params 请求的参数
* @return 响应内容
*/
HttpResponse httpPost(String url, Map<String, String> headers, Map<String, Object> params);
HttpResponse httpPost(String url, Map<String, String> headers, Map<String, Object> textParams, Map<String, File> fileParams, Map<String, Object> textNextParams);
/**
* 发送带参数和自定义请求头的POST请求并返回响应内容。
*
* @param url 请求的URL
* @param headers 请求的自定义头部信息
* @param params 请求的参数
* @return 响应内容
*/
HttpResponse httpPostFileMultiPart(String url, Map<String, String> headers, Map<String, ContentBody> params);
/**
* 解析URL中的参数,并将它们存储在一个Map中
*
* @param url 指定url
* @return 返回map对象
*/
Map<String, Object> extractUrlParameters(String url);
}
package com.xjl.common.service.impl;
import com.xjl.common.exception.EasyInvoiceException;
import com.xjl.common.service.HttpClientService;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.ContentBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicNameValuePair;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 实现 {@link HttpClientService} 接口,用于进行 HTTP 请求。
*
* @author lkb
* @version 1.0
*/
@Slf4j
public class HttpClientServiceImpl implements HttpClientService {
private static final String BOUNDARY_PREFIX = "------WebKitFormBoundary";
@Resource
private CloseableHttpClient httpClient;
@Resource
private RequestConfig config;
/**
* 发送GET请求并返回响应内容。
*
* @param url 请求的URL
* @return 响应内容
*/
@Override
public CloseableHttpResponse httpGet(String url) {
return httpGet(url, null, new LinkedHashMap<>());
}
/**
* 发送带参数的GET请求并返回响应内容。
*
* @param url 请求的URL
* @param headers 请求的参数
* @return 响应内容
*/
@Override
public CloseableHttpResponse httpGet(String url, Map<String, String> headers) {
return httpGet(url, headers, null);
}
/**
* 发送带参数和自定义请求头的GET请求并返回响应内容。
*
* @param url 请求的URL
* @param headers 请求的自定义头部信息
* @param params 请求的参数
* @return 响应内容
*/
@Override
public CloseableHttpResponse httpGet(String url, Map<String, String> headers, Map<String, Object> params) {
if (headers != null) {
headers.putAll(preHeaders());
} else {
headers = preHeaders();
}
try {
List<NameValuePair> pairs = convertParams(params);
URIBuilder uriBuilder = new URIBuilder(url).addParameters(pairs);
HttpGet httpGet = new HttpGet(uriBuilder.build());
httpGet.setConfig(config);
headers.forEach((key, value) -> httpGet.addHeader(key, String.valueOf(value)));
return executeRequest(httpGet);
} catch (Exception e) {
throw new RuntimeException("执行GET请求失败", e);
}
}
/**
* 发送POST请求并返回响应内容。
*
* @param url 请求的URL
* @return 响应内容
*/
@Override
public CloseableHttpResponse httpPost(String url) {
return httpPost(url, null, null);
}
/**
* 发送带JSON数据的POST请求并返回响应内容。
*
* @param url 请求的URL
* @param json JSON数据
* @return 响应内容
*/
@Override
public CloseableHttpResponse httpPost(String url, String json) {
HttpPost httpPost = new HttpPost(url);
httpPost.setConfig(config);
StringEntity entity = new StringEntity(json, StandardCharsets.UTF_8);
entity.setContentType("application/json");
httpPost.setEntity(entity);
return executeRequest(httpPost);
}
/**
* 发送带参数的POST请求并返回响应内容。
*
* @param url 请求的URL
* @param headers 请求头
* @return 响应内容
*/
@Override
public CloseableHttpResponse httpPost(String url, Map<String, String> headers) {
return httpPost(url, headers, null);
}
/**
* 发送带参数和自定义请求头的POST请求并返回响应内容。
*
* @param url 请求的URL
* @param headers 请求的自定义头部信息
* @param params 请求的参数
* @return 响应内容
*/
@Override
public CloseableHttpResponse httpPost(String url, Map<String, String> headers, Map<String, Object> params) {
if (headers != null) {
headers.putAll(preHeaders());
} else {
headers = preHeaders();
}
List<NameValuePair> pairs = convertParams(params);
HttpPost httpPost = new HttpPost(url);
httpPost.setConfig(config);
httpPost.setEntity(new UrlEncodedFormEntity(pairs, StandardCharsets.UTF_8));
headers.forEach((key, value) -> httpPost.addHeader(key, String.valueOf(value)));
return executeRequest(httpPost);
}
/**
* 发送带参数和自定义请求头的POST请求并返回响应内容。
*
* @param url 请求的URL
* @param headers 请求的自定义头部信息
* @param textParams 请求的参数
* @param fileParams 上传的文件
* @return 响应内容
*/
@Override
public CloseableHttpResponse httpPost(String url, Map<String, String> headers, Map<String, Object> textParams, Map<String, File> fileParams, Map<String, Object> textNextParams) {
headers.putAll(preHeaders());
String boundary = generateBoundary();
headers.put("Content-Type", "multipart/form-data; boundary=" + boundary);
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
if (textParams != null) {
addTextParams(builder, textParams);
}
if (fileParams != null) {
addFileParams(builder, fileParams);
}
if (textParams != null) {
addTextParams(builder, textNextParams);
}
//自定义boundary
HttpEntity httpEntity = builder.setBoundary(boundary).setContentType(ContentType.MULTIPART_FORM_DATA).setCharset(StandardCharsets.UTF_8).setMode(HttpMultipartMode.BROWSER_COMPATIBLE).build();
HttpPost httpPost = new HttpPost(url);
httpPost.setConfig(config);
httpPost.setEntity(httpEntity);
headers.forEach((key, value) -> httpPost.addHeader(key, String.valueOf(value)));
return executeRequest(httpPost);
}
/**
* 发送带参数和自定义请求头的POST请求并返回响应内容。
*
* @param url 请求的URL
* @param headers 请求的自定义头部信息
* @param params 请求的参数
* @return 响应内容
*/
@Override
public CloseableHttpResponse httpPostFileMultiPart(String url, Map<String, String> headers, Map<String, ContentBody> params) {
headers.putAll(preHeaders());
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
for (Map.Entry<String, ContentBody> param : params.entrySet()) {
builder.addPart(param.getKey(), param.getValue());
}
HttpEntity httpEntity = builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE).build();
//自定义boundary
HttpPost httpPost = new HttpPost(url);
httpPost.setConfig(config);
httpPost.setEntity(httpEntity);
headers.forEach((key, value) -> httpPost.addHeader(key, String.valueOf(value)));
return executeRequest(httpPost);
}
/**
* 解析URL中的参数,并将它们存储在一个Map中
*
* @param url 指定url
* @return 返回map对象
*/
@Override
public Map<String, Object> extractUrlParameters(String url) {
Map<String, Object> keyMap = new LinkedHashMap<>();
String[] parts = url.split("\\?");
if (parts.length == 2) {
String queryString = parts[1];
Arrays.stream(queryString.split("&"))
.map(param -> param.split("="))
.filter(keyValue -> keyValue.length == 2)
.forEach(keyValue -> keyMap.put(keyValue[0], keyValue[1]));
}
return keyMap;
}
private Map<String, String> preHeaders() {
Map<String, String> headers = new HashMap<>(12);
headers.put(HttpHeaders.USER_AGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.43");
headers.put("Upgrade-Insecure-Requests", "1");
headers.put("Connection", "keep-alive");
headers.put("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7");
headers.put("Accept-Encoding", "gzip, deflate, br");
headers.put("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6");
return headers;
}
private CloseableHttpResponse executeRequest(HttpRequestBase request) {
try {
return httpClient.execute(request);
} catch (IOException e) {
throw new EasyInvoiceException("执行HTTP请求失败", e);
}
}
private List<NameValuePair> convertParams(Map<String, Object> params) {
return Optional.ofNullable(params)
.map(map -> map.entrySet().stream().map(entry -> new BasicNameValuePair(entry.getKey(), entry.getValue().toString())))
.orElseGet(Stream::empty)
.collect(Collectors.toList());
}
private void addTextParams(MultipartEntityBuilder builder, Map<String, Object> textParams) {
Optional.ofNullable(textParams).ifPresent(params ->
params.forEach((key, value) -> builder.addTextBody(key, (String) value, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8))));
}
private void addFileParams(MultipartEntityBuilder builder, Map<String, File> fileParams) {
Optional.ofNullable(fileParams).ifPresent(params ->
params.forEach((key, file) -> {
if (file != null) {
builder.addBinaryBody(key, file);
}
})
);
}
private String generateBoundary() {
SecureRandom random = new SecureRandom();
byte[] randomBytes = new byte[16];
random.nextBytes(randomBytes);
String base64Encoded = Base64.getEncoder().encodeToString(randomBytes);
return BOUNDARY_PREFIX + base64Encoded.replace("/", "_").replace("+", "-").replace("=", "");
}
}
应用实例
直接像使用MyBatisPlus的使用方式,继承其实现类调用接口即可
package com.xjl.api.service.impl;
import com.xjl.api.service.HttpLoginService;
import com.xjl.api.vo.UniversityUserVo;
import com.xjl.common.service.impl.HttpClientServiceImpl;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.util.EntityUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 实现 {@link HttpLoginService} 接口,用于用户登录获取信息
*
* @author lkb
* @version 1.0
*/
@Service
public class HttpLoginServiceImpl extends HttpClientServiceImpl implements HttpLoginService {
private static final String COOKIE_PATTERN = "(\\S+?)=(\\S+?);";
private static final String BASE_URL = "XXXXXXXXXX";
/**
* 从JavaScript代码中提取alert中的提示信息
*
* @param scriptText alert中的文本
* @return 提取alert中的文本
*/
private static String extractAlertMessage(String scriptText) {
String alertPrefix = "alert('";
String alertSuffix = "')";
int startIndex = scriptText.indexOf(alertPrefix);
int endIndex = scriptText.indexOf(alertSuffix, startIndex + alertPrefix.length());
if (startIndex != -1 && endIndex != -1) {
return scriptText.substring(startIndex + alertPrefix.length(), endIndex);
} else {
// 没找到alert提示信息
return null;
}
}
private void getWebApp(UniversityUserVo userVo) throws IOException {
CloseableHttpResponse response = httpGet(BASE_URL + "/wsyy");
HttpEntity entity = response.getEntity();
try {
String html = EntityUtils.toString(entity, "utf-8");
Document document = Jsoup.parse(html);
Element viewStateInput = document.body().select("input[id=__VIEWSTATE]").first();
Element eventValidationInput = document.body().select("input[id=__EVENTVALIDATION]").first();
Element viewStateGeneratorInput = document.body().select("input[id=__VIEWSTATEGENERATOR]").first();
if (viewStateInput == null || eventValidationInput == null || viewStateGeneratorInput == null) {
throw new RuntimeException("获取系统信息异常");
}
String viewStateValue = viewStateInput.attr("value");
String eventValidationValue = eventValidationInput.attr("value");
String viewStateGeneratorValue = viewStateGeneratorInput.attr("value");
Map<String, Object> dataKeys = new HashMap<>(6);
dataKeys.put("__VIEWSTATE", viewStateValue);
dataKeys.put("__EVENTVALIDATION", eventValidationValue);
dataKeys.put("__VIEWSTATEGENERATOR", viewStateGeneratorValue);
userVo.setDataKeys(dataKeys);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
response.close();
}
}
}