图源:简书 (jianshu.com)
在开发Web应用的时候,有时候会涉及到服务器之间的通信,这通常是以借口调用和返回的方式来实现的。
在之前的PHP开发中,我常用的是curl来实现服务器之间的接口调用,在Spring Boot开发中,更常见的是使用Http Client。
Http Client是一个功能强大的Java实现的Http客户端组件,可以用它来实现Http相关调用,它具有以下优点:
更多的Http Client介绍见Apache HttpClient - 简书 (jianshu.com)。
下面实际演示如何在项目中整合和使用Http Client。
这里的示例代码将基于从零开始 Spring Boot 14:文件上传 - 魔芋红茶’s blog (icexmoon.cn)最终代码进行修改,对应的代码仓库是learn_spring_boot/ch14。
先添加所需依赖:
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpclientartifactId>
dependency>
为Http Client添加相关配置:
#http客户端设置
httpclient.maxTotal=1
httpclient.defaultMaxPerRoute=1
httpclient.connectTimeout=2000
httpclient.connectionRequestTimeout=2000
httpclient.socketTimeout=2000
使用配置类加载这些配置:
@Configuration
@ConfigurationProperties(prefix = "httpclient")
@Data
public class HttpClientConfig {
private Integer maxTotal;
private Integer defaultMaxPerRoute;
private Integer connectTimeout;
private Integer connectionRequestTimeout;
private Integer socketTimeout;
/**
* HttpClient连接池
* @return
*/
@Bean
public HttpClientConnectionManager httpClientConnectionManager() {
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(maxTotal);
connectionManager.setDefaultMaxPerRoute(defaultMaxPerRoute);
return connectionManager;
}
/**
* 创建RequestConfig
* @return
*/
@Bean
public RequestConfig requestConfig() {
return RequestConfig.custom().setConnectTimeout(connectTimeout)
.setConnectionRequestTimeout(connectionRequestTimeout).setSocketTimeout(socketTimeout)
.build();
}
/**
* 创建HttpClient
* @param manager
* @param config
* @return
*/
@Bean
public CloseableHttpClient httpClient(HttpClientConnectionManager manager, RequestConfig config) {
return HttpClientBuilder.create().setConnectionManager(manager).setDefaultRequestConfig(config)
.build();
}
}
为了使用起来更方便,添加一个工具类:
@Component
@Slf4j
public class HttpClientUtil {
/**
* Util工具类封装的返回值,除了原始的HttpResponse对象,还包含一个body作为响应报文体,
* 因为返回的HttpResponse对象已经关闭,无法通过getEntity获取响应报文体
*/
@Getter
@AllArgsConstructor
public static class Response {
@ApiModelProperty("原始的apache HttpResponse对象")
private HttpResponse httpResponse;
@ApiModelProperty("响应报文内容")
private String body;
private long contentLength;
}
@Autowired
private CloseableHttpClient httpClient;
public Response doGet(String url, Map<String, String> param, Map<String, String> headers) {
CloseableHttpResponse response = null;
try {
// 创建uri
URIBuilder builder = new URIBuilder(url);
if (param != null) {
for (String key : param.keySet()) {
builder.addParameter(key, param.get(key));
}
}
builder.setCharset(Charset.forName("utf-8"));
URI uri = builder.build();
log.debug(uri.toString());
// 创建http GET请求
HttpGet httpGet = new HttpGet(uri);
if (headers != null && !headers.isEmpty()) {
headers.forEach((name, value) -> httpGet.setHeader(name, value));
}
// 执行请求
response = httpClient.execute(httpGet);
Response utilResponse = new Response(response, EntityUtils.toString(response.getEntity()), response.getEntity().getContentLength());
Header contentType = response.getFirstHeader("Content-Type");
if (contentType != null && contentType.getValue().equals("image/jpeg")) {
log.info("Http Client请求,GET url:[{}],参数:[{}],报文头:[{}],返回数据=[图片]", url, param, headers);;
} else {
log.info("Http Client请求,GET url:[{}],参数:[{}],报文头:[{}],返回数据=[{}]", url, param, headers, utilResponse.getBody());
}
return utilResponse;
} catch (Exception e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
log.error(sw.toString());
String msg = String.format("http客户端调用出错,http method:GET,请求:%s。", url);
log.error(msg);
throw new ResultException(Result.ErrorCode.DEFAULT_ERROR, "http客户端调用出错");
} finally {
close(response);
}
}
public Response doGet(String url) {
return doGet(url, null, null);
}
public Response doPost(String url, Map<String, String> param, Map<String, String> headers) {
CloseableHttpResponse response = null;
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建参数列表
if (param != null) {
List<NameValuePair> paramList = new ArrayList<>();
for (String key : param.keySet()) {
paramList.add(new BasicNameValuePair(key, param.get(key)));
}
// 模拟表单
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList, Charset.forName("UTF-8"));
httpPost.setEntity(entity);
}
if (headers != null && !headers.isEmpty()) {
headers.forEach((name, value) -> httpPost.setHeader(name, value));
}
// 执行http请求
response = httpClient.execute(httpPost);
Response utilResponse = new Response(response, EntityUtils.toString(response.getEntity()), response.getEntity().getContentLength());
log.info("Http Client请求,POST url:[{}],参数:[{}],报文头:[{}],返回数据=[{}]", url, param, headers, utilResponse.getBody());
return utilResponse;
} catch (Exception e) {
log.error("doPost url:" + url + " fail,cause :" + e.getMessage(), e);
throw new ResultException(Result.ErrorCode.NETWORK_ERROR, String.format("HttpClient请求失败,原因:%s", e.getMessage()));
} finally {
close(response);
}
}
private void close(CloseableHttpResponse response) {
try {
if (response != null) {
response.close();
}
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
public Response doPost(String url) {
return doPost(url, null, null);
}
public Response doPostJson(String url, String json) {
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建请求内容
StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
httpPost.setEntity(entity);
// 执行http请求
response = httpClient.execute(httpPost);
Response utilResponse = new Response(response, EntityUtils.toString(response.getEntity()), response.getEntity().getContentLength());
return utilResponse;
} catch (Exception e) {
log.error("doJson url:" + url + " fail,cause :" + e.getMessage(), e);
throw new ResultException(Result.ErrorCode.NETWORK_ERROR, String.format("HttpClient请求失败,原因:%s", e.getMessage()));
} finally {
close(response);
}
}
}
假设因为某些原因,我们需要给添加书籍接口增加敏感词检查的功能,我们可以通过一个提供API接入的在线服务来解决,比如:
简单注册后就可以获取到一个自己的APPKEY和接口域名,添加到配置中:
#yesapi配置
books.yesapi.host=xxxx.api.yesapi.cn
books.yesapi.appKey=xxxxxxxx
用于读取系统配置的辅助类中也添加相应属性:
@Data
@Component
public class SysProperties {
...
@Value("${books.yesapi.host}")
private String yesApiHost;
@Value("${books.yesapi.appKey}")
private String yesApiAppKey;
}
为了让调用更灵活,将对所有YesApi相关的调用封装一个基础类,用于请求时添加必要信息以及检查返回报文是否调用成功,如果失败就抛出相应异常:
@Component
public class YesApiConnect {
@Autowired
private SysProperties sysProperties;
@Autowired
private HttpClientUtil httpClientUtil;
@Data
private static class Response {
private Integer ret;
private String msg;
private Long _t;
private String _auth;
}
public String doGet(String path, Map<String, String> params, Map<String, String> headers) {
if (params.get("app_key") == null) {
params.put("app_key", sysProperties.getYesApiAppKey());
}
String url = String.format("http://%s/%s", sysProperties.getYesApiHost(), path);
HttpClientUtil.Response res = httpClientUtil.doGet(url, params, headers);
int statusCode = res.getHttpResponse().getStatusLine().getStatusCode();
//这里仅接受返回为200的响应,如果需要处理重定向等其他响应,需要做相应处理
if (statusCode != 200) {
throw new ResultException(Result.ErrorCode.DEFAULT_ERROR, String.format("yesApi接口调用出错,返回状态码为%d", statusCode));
}
Response reponse = JSON.parseObject(res.getBody(), Response.class);
if (reponse.getRet() != 200) {
throw new ResultException(Result.ErrorCode.DEFAULT_ERROR, String.format("yesApi接口调用出错,响应业务码是%d", reponse.getRet()));
}
return res.getBody();
}
}
下面就可以添加具体的调用接口了,为一个接口创建一个IService
接口和对应的实现ServiceImpl
类:
public interface IWordCheckService {
@Data
class Response {
@Data
public static class InnerData {
private Integer errCode;
private String errMsg;
private String[] sensitiveWord;
}
private Integer ret;
private InnerData data;
private String msg;
private Integer _t;
private String _auth;
}
/**
* 执行敏感词检查
*
* @param content 内容
* @return
*/
Response doWordCheck(String content);
static boolean isWordCheckAccess(Response response) {
if (response.getData().getErrCode() == 0) {
return true;
}
return false;
}
static String[] getSensitiveWords(Response response) {
return response.getData().getSensitiveWord();
}
/**
* 检查返回值中是否包含敏感词,如果存在,抛出异常
*
* @param response
*/
static void checkSensitiveWords(Response response) {
if (!isWordCheckAccess(response)) {
throw new ResultException(Result.ErrorCode.PARAM_CHECK, "包含敏感词:" + Arrays.toString(getSensitiveWords(response)));
}
}
}
@Service
public class WordCheckServiceImpl implements IWordCheckService {
@Autowired
private YesApiConnect yesApiConnect;
@Override
public Response doWordCheck(String content) {
Map<String, String> params = new HashMap<>();
params.put("content", content);
String body = yesApiConnect.doGet("api/App/Common_BannerWord/Check", params, null);
return JSON.parseObject(body, Response.class);
}
}
我习惯于将接口相关返回值格式和相应处理定义在接口中,具体接口调用在实现类中实现。
因为接口出错的情况已经在YesApiConnect
中进行了处理,所以具体接口只需要将响应报文内容转换成相应结构化对象作为进一步处理的业务实体。
最后在添加书籍接口中添加上敏感词检查的功能:
...
public Result addBook(@Validated @RequestBody AddBookDTO dto) {
...
//对书籍详情进行敏感词检查
IWordCheckService.Response response = wordCheckService.doWordCheck(dto.getDesc());
IWordCheckService.checkSensitiveWords(response);
...
}
...
不要忘了使用
@Autowired
让框架载入IWordCheckService
接口的实现。
当然这里也可以将这两行代码整合,提供一个类似mustDoWordCheck
的方法,不需要返回结果,如果没有通过敏感词检查就直接抛出异常。
最后测试的部分这里不再多说,尝试在添加书籍的时候为desc
参数添加敏感词即可。
Http Client本身功能相当强大,利用它我在工作中实现了自动登录并调用另一个系统的功能。这里仅做一个简单演示。
谢谢阅读。
本文的最终代码见代码仓库learn_spring_boot/ch15 (github.com)。