OkHttp可用来发送http请求,做微服务。使用方法很简单,并没有做很多事情,只需要创建Request(包括url,header,body),创建并执行Call,读取ResponseBody中的数据即可。
1.读取Response数据
1.1.String方式读取
private <T> T callAndReadResponseBodyString(Request request, TypeToken<RemoteApiResult<T>> typeToken){
Response response = null;
try {
response = this.getOkHttpClient().newCall(request).execute();
if(!response.isSuccessful())
throw new IOException();
} catch (IOException e) {
return null;
}
RemoteApiResult<T> remoteApiResult = this.gson.fromJson(response.body().charStream(), typeToken.getType());
return remoteApiResult.getData();
}
远程服务方将Java对象转成Json格式返回(Spring MVC 的Controller中return的java对象会自动转),调用方可以通过Gson将Json字符串转换为Java对象。
这种方式很方便,但存在一些问题:
1.Java对象(自己定义)属性中不要有byte,byte[]类型,byte[]类型转成Json再转回byte[]不一定对了;
2.官方文档指出当ResponseBody大于1M时推荐以流的方式读取。
1.2.Stream方式读取
private byte[] callAndReadResponseBodyStream(Request request){
Response response = null;
try {
response = this.getOkHttpClient().newCall(request).execute();
if(!response.isSuccessful())
throw new IOException();
} catch (IOException e) {
return new byte[0];
}
//获取ResponseBody的输入流
InputStream inputStream = response.body().byteStream();
//从输入流读取字节码
byte[] bytes = StreamUtil.getAllBytesOfInputStream(inputStream);
return bytes;
}
2.发送Request
2.1.同步Get请求
/**
* 同步get请求
* @param url
* @return 指定类型的对象
*/
public <T> T syncGet(String url, Map<String, String> requestParams, Map<String, String> headers, TypeToken<RemoteApiResult<T>> typeToken){
Request request = new Request.Builder()
.url(this.buildUrlWithRequestParams(url, requestParams))
.headers(this.buildHeaders(headers))
.build();
return this.callAndReadResponseBodyString(request, typeToken);
}
Controller返回一个Java对象即可。
另外,OkHttp中的url是要加上协议的,如http、https等,否则会报错url无效。
2.2.异步Get请求
/**
* 异步get请求
* @param url
*/
public <T> void asynGet(String url, Map<String, String> requestParams, Map<String, String> headers, ResponseHandler handler){
Request request = new Request.Builder()
.url(this.buildUrlWithRequestParams(url, requestParams))
.headers(this.buildHeaders(headers))
.build();
this.getOkHttpClient().newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
if(handler != null)
handler.handleFailure(call, e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if(handler != null)
handler.handleResponse(call, response);
}
});
}
自己实现Callback接口的两个方法即可,这里通过自己定义的ResponseHandler类来处理。
Controller返回void,数据写到HttpServletResponse的输出流里。如:
@RequestMapping(value = "/get/image/author", method = RequestMethod.GET)
public void getAuthorImage(HttpServletResponse response){
File file = new File("F:\\demo\\okhttp\\okhttp-service\\src\\main\\resources\\static\\author.jpg");
try(FileInputStream fileInputStream = new FileInputStream(file);
OutputStream outputStream = response.getOutputStream()
) {
byte[] bytes = new byte[(int) file.length()];
fileInputStream.read(bytes);
outputStream.write(bytes);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
2.3.POST String
public <T> T postString(String url, Map<String, String> requestParams, Map<String, String> headers, String str, MediaType mediaType, TypeToken<RemoteApiResult<T>> typeToken){
Request request = new Request.Builder()
.url(this.buildUrlWithRequestParams(url, requestParams))
.headers(this.buildHeaders(headers))
.post(RequestBody.create(mediaType, str))
.build();
return this.callAndReadResponseBodyString(request, typeToken);
}
RequestBody的数据Controller端有两种方式可以读取:
1.通过HttpServletRequest
@RequestMapping(value = "/reply", method = RequestMethod.POST)
public ApiResult<String> sendMessage(HttpServletRequest request){
String msg;
try {
msg = HttpUtil.readRequestBodyToString(request);
} catch (IOException e) {
msg = null;
}
return new ApiResult<>("Your message: \"" + msg + "\" has been received successfully.");
}
public static String readRequestBodyToString(HttpServletRequest request) throws IOException {
BufferedReader reader = request.getReader();
StringBuilder result = new StringBuilder();
String line;
while((line = reader.readLine()) != null){
result.append(line);
}
return result.toString();
}
2.通过@RequestBody
@RequestMapping(value = "/reply2", method = RequestMethod.POST)
public ApiResult<String> sendMessage(@RequestBody String msg){
return new ApiResult<>("Your message: \"" + msg + "\" has been received successfully.");
}
2.4.POST Stream
public <T> T postStream(String url, Map<String, String> requestParams, Map<String, String> headers, MediaType mediaType, Object object, TypeToken<RemoteApiResult<T>> typeToken){
RequestBody requestFileBody = new RequestBody() {
@Override
public MediaType contentType() {
return mediaType;
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
OutputStream outputStream = sink.outputStream();
String json = gson.toJson(object);
byte[] bytes = json.getBytes("utf-8");
outputStream.write(bytes);
}
};
Request request = new Request.Builder()
.url(this.buildUrlWithRequestParams(url, requestParams))
.headers(this.buildHeaders(headers))
.post(requestFileBody)
.build();
return this.callAndReadResponseBodyString(request, typeToken);
}
感觉就是把Java对象写到流里POST。
Controller可以以byte[]作为参数,数据是可以拿到的,处理方式自己定义即可。
@RequestMapping(value = "/upload", method = RequestMethod.POST)
public ApiResult<String> uploadFile(@RequestBody byte[] bytes) {
String string = new String(bytes);
return new ApiResult<>(string);
}
2.5.POST File
public <T> T postFile(String url, Map<String, String> requestParams, Map<String, String> headers, MediaType mediaType, File file, TypeToken<RemoteApiResult<T>> typeToken){
Request request = new Request.Builder()
.url(this.buildUrlWithRequestParams(url,requestParams))
.headers(this.buildHeaders(headers))
.post(RequestBody.create(mediaType, file))
.build();
return this.callAndReadResponseBodyString(request, typeToken);
}
Controller也可以用byte[]作为参数,不过显然不能简单的转成String了,除非是文本文件如txt。
2.6.POST Form表单
public <T> T postFormData(String url, Map<String, String> requestParams, Map<String, String> headers, Map<String, String> formData, TypeToken<RemoteApiResult<T>> typeToken){
Request request = new Request.Builder()
.url(this.buildUrlWithRequestParams(url, requestParams))
.headers(buildHeaders(headers))
.post(buildRequestBody(formData))
.build();
return this.callAndReadResponseBodyString(request, typeToken);
}
跟在页面提交form表单是一样的效果,所以Controller的写法同页面提交form表单。
2.7.POST Multipart Request
public <T> T postMultipartRequest(String url, Map<String, String> requestParams, Map<String, String> headers, List<FormDataPart> formDataParts, TypeToken<RemoteApiResult<T>> typeToken){
MultipartBody.Builder builder = new MultipartBody.Builder();
builder.setType(MultipartBody.FORM);
for(FormDataPart part : formDataParts){
if(part instanceof KeyValueFormDataPart) {
KeyValueFormDataPart keyValueFormDataPart = (KeyValueFormDataPart) part;
builder.addFormDataPart(keyValueFormDataPart.getName(), keyValueFormDataPart.getValue());
}
else {
FileFormDataPart fileFormDataPart = (FileFormDataPart)part;
builder.addFormDataPart(fileFormDataPart.getName(), fileFormDataPart.getFileName(), fileFormDataPart.getRequestBody());
}
}
RequestBody requestBody = builder.build();
Request request = new Request.Builder()
.url(this.buildUrlWithRequestParams(url, requestParams))
.headers(this.buildHeaders(headers))
.post(requestBody)
.build();
return this.callAndReadResponseBodyString(request, typeToken);
}
这里我自己定义了FormDataPart基类和KeyValueFormDataPart、FileFormDataPart两个子类。
这个可以用于带文件上传的form表单,如:
<form action="..." method="post">
<table>
<tr>
<td>用户名</td>
<td><input type="text" name="username"/></td>
</tr>
<tr>
<td>密码</td>
<td><input type="password" name="password"/></td>
</tr>
<tr>
<td>头像</td>
<td><input type="file" name="icon"/></td>
</tr>
</table>
<input type="submit" value="提交"/>
</form>
Controller的接收数据方式如下:
@RequestMapping(value = "/update/head", method = RequestMethod.POST)
public ApiResult<User> updateHeadIcon(String username, String password, MultipartFile icon){
byte[] bytes;
try {
bytes = icon.getBytes();
} catch (IOException e) {
bytes = null;
}
return userService.updateHeadIcon(username, password, null);
}
file类型的表单项可以用MultipartFile类型作为对应的入参,然后读取字节码即可。
2.8.设置cache、time out,更改OkHttpClient设置
一般,OkHttpClient是事先创建好的单一实例,如:
private HttpUtil(){
client = new OkHttpClient.Builder()
.connectTimeout(connectTimeout, TimeUnit.SECONDS)
.writeTimeout(writeTimeout, TimeUnit.SECONDS)
.readTimeout(readTimeout, TimeUnit.SECONDS)
.build();
}
创建单一实例类HttpUtil时对OkHttpClient进行一些配置,如time out。
如果在发送请求之前临时想改变OkHttpClient的一些配置,可以这样做:
public OkHttpClient getOkHttpClient(){
if(cacheable){
return this.client.newBuilder().cache(this.cache).build();
} else {
return this.client;
}
}
通过原来的OkHttpClient得到一个Builder来重新build OkHttpClient。例如,可以临时配置Cache。
private Cache cache = new Cache(new File("F:/demo/okhttp/okhttp-client/src/main/resources/cache"), 10 * 1024 *1024);
注意:不是配置了cache就会生效的,需要在服务方返回的Response头里加上cache相关的配置,如
@RequestMapping(value = "/query", method = RequestMethod.GET)
public ApiResult<User> queryUser(HttpServletResponse response, @RequestParam("username") String username){
response.addHeader("Cache-Control", "max-age=3600");
response.addHeader("Last-Modified", Long.toString(System.currentTimeMillis()));
return userService.queryUser(username);
}
3.其它
问题:Last-Modified没有调试成功,不知道问题出在哪。
本文完整代码:https://github.com/JinchaoLv/okhttp-demo