JavaEE 使用OKhttp和Action进行通信

来源:互联网 发布:手机淘宝端怎么微装修 编辑:程序博客网 时间:2024/04/29 23:18

OKhttp是一个处理网络请求的开源项目,由Square公司开发用于替代HttpUrlConnection和Apache HttpClient(Android API23 6.0中已将HttpClient移除),是一个非常适用于Android(Java)的轻量级框架。

为了在客户端使用OKhttp,本文章还将给出服务器端代码,当然这些代码只是为了测试而编写的简单代码。

发送GET请求

使用OKhttp使发送一个请求变得再简单不过,你只需要提供一个URL和一系列请求参数(对于GET请求,可以直接将请求参数添加到URL尾部)即可。

使用OKhttp发送请求之前,需要创建一个OkHttpClient对象,此对象负责发送请求。建议用一个OkHttpClient对象发送请求,而不是在发送每个请求之前都创建一个新的OkHttpClient对象。

    private static OkHttpClient client=new OkHttpClient();

一个请求被封装成一个Request对象,这个Request对象可以包含请求头(Headers)和请求体(RequestBody),这里的GET请求不会用到请求头和请求体,它们将在下文介绍。你可以通过Request.Builder来一步步构建需要的请求(而不是直接new一个Request)。下面这个方法封装了构建一个Request的操作。

    private static Request getGetRequest(String url, String[] params) {        if(params!=null && params.length>0) {            if(params.length%2!=0)                throw new IllegalArgumentException("The number of request parameters must be even!");            StringBuilder sb=new StringBuilder(url);            for(int i=0; i<params.length-1; i+=2) {                sb.append((i>0) ? "&" : "?").                append(params[i]).                append("=").                append(params[i+1]);            }            url=sb.toString();        }        return new Request.Builder().url(url).build();    }

构建好一个请求后,就可以向服务器发起一个对话(Call)了。OkHttpClient负责发起一个新的Call,这个Call会返回一个响应(Response)。注意,这里使用了同步的方式发起一个Call,最好在一个子线程中执行。

    public static void get(String url, String[] params, Callback callback) {        Request request=getGetRequest(url, params);        Call call=client.newCall(request);        try {            Response response=call.execute();            callback.onResponse(call, response);        } catch (IOException e) {            callback.onFailure(call, e);        }    }

另外也可以异步提交一个请求,不过此时不会立即返回一个Response,而是在之后的某个时间点回调Callback对象的onResponse(收到响应)或onFailure(发生异常)方法。

    call.enqueue(callback);

现在我们向服务器发送一个GET请求,并提交用户名和密码(user和password),下面是测试代码:

HttpUtils.get(    "http://localhost:8080/HttpServer/zzw/get",     new String[]{"user", "张三", "password", "123456"},     new Callback(){        @Override        public void onFailure(Call call, IOException e) {            e.printStackTrace();        }        @Override        public void onResponse(Call call, Response response) throws IOException {            System.out.println(response.body().string());        }    });

我们在onResponse方法中简单显示了来自服务器的响应。和Request类似,Reponse也可以有响应头(Headers)和响应体(ResponseBody),响应报文就封装在响应体中。相应的服务端代码如下(如果你不感兴趣可以直接跳过哦):

public class GetAction extends ActionSupport {    private static final long serialVersionUID = -7442519273670774235L;    private String user;    private String password;    public void setUser(String u) { user=u; }    public void setPassword(String p) { password=p; }    public String getUser() { return user; }    public String getPassword() { return password; }    @Override    @Action(value="/zzw/get",             results={@Result(name="success", location="user.jsp")})    public String execute() throws Exception {        return SUCCESS;    }}
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"><html><head><%@ page language="java" contentType="text/html; charset=GBK"    pageEncoding="UTF-8"%><%@ taglib uri="/struts-tags" prefix="s"%><meta http-equiv="Content-Type" content="text/html; charset=GBK"><title>get page</title></head><body>  用户: <s:property value="user"/><br>  密码: <s:property value="password"/></body></html>

这里的Action使用了Convention插件进行了配置,使其可以接收任何发给http://localhost:8080/HttpServer/zzw/get的请求。

发送POST请求

发送POST请求比GET请求稍微复杂点,不过大体流程还是一致的,先看代码:

    private static Request getPostRequest(String url, String[] params) {        FormBody.Builder builder=new FormBody.Builder();        if(params!=null && params.length>0) {            if(params.length%2!=0)                throw new IllegalArgumentException("The number of request parameters must be even!");            for(int i=0; i<params.length-1; i+=2)                builder.add(params[i], params[i+1]);        }        RequestBody requestBody=builder.build();        return new Request.Builder().url(url).post(requestBody).build();    }

由于POST请求的请求参数都是放在报文体中的,因此需要在”build”过程中添加进请求体中(相比于GET请求更安全)。另外还需要调用Builder的post方法指明发送的是一个POST请求。

注意,这里是按照“提交表单”的方式发送一个POST请求,也就是说这里的POST请求和方法为post的form表单一样。除了这种方式之外,还有一种用于上传文件的POST请求将在后面介绍。

    public static void post(String url, String[] params, Callback callback) {        Request request=getPostRequest(url, params);        Call call=client.newCall(request);        try {            Response response=call.execute();            callback.onResponse(call, response);        } catch (IOException e) {            callback.onFailure(call, e);        }    }

客户端测试代码如下:

String user=null;try {    user=URLEncoder.encode("张三", "GBK");} catch (UnsupportedEncodingException e) {    e.printStackTrace();}HttpUtils.post(    "http://localhost:8080/HttpServer/zzw/form_post",     new String[]{"user", user, "password", "123456"},     new Callback(){        @Override        public void onFailure(Call call, IOException e) {            e.printStackTrace();        }        @Override        public void onResponse(Call call, Response response) throws IOException {            System.out.println(response.body().string());        }    });

相应的服务端代码如下:

public class FormPostAction extends ActionSupport {    private static final long serialVersionUID = -2701435552091943728L;    private String user;    private String password;    private String encoding;    public void setUser(String u) { user=u; }    public void setPassword(String p) { password=p; }    public void setEncoding(String e) { encoding=e; }    public String getUser() { return user; }    public String getPassword() { return password; }    public String getEncoding() { return encoding; }    @Override    @Action(value="/zzw/form_post",             params={"encoding", "GBK"},             results={@Result(name="success", location="user.jsp")})    public String execute() throws Exception {        user=URLDecoder.decode(user, encoding);        return SUCCESS;    }}

注意到,我们在客户端对提交的字符串进行了编码,而在服务端对接收的字符串进行了解码。这是因为需要保证前端和后端编码一致才不会出现乱码,通常客户端的编码是在jsp、HTML等页面中设置的。

上传文件

当客户端需要向服务端上传文件时,比如在课程网站上提交作业时(让我又想起要在Word里面打各种奇奇怪怪的数学符号),这时再使用上面介绍的POST请求就不行了。

我们先来看个上传文件的例子,先是jsp页面代码:

<form method="post" action="upload" enctype="multipart/form-data">    选择文件: <input type="file" id="file" name="file"><br>    <input type="submit" value="上传"><br></form>

multipart_post

这张图片是上传某个文件(图片)时抓获的Request和Response信息。

在Request Headers中有一个Content-Type字段,可以看到这个字段的值包含multipart/form-data,和form表单的enctype属性值相同;另外boundary指定了多个文件(参数)之间的边界。

再看Request Payload,因为我们只上传了一个文件,所以这里只有两个boundary字符串。重点在于这里有一个Content-Disposition字段,这是我们真正需要的,里面的”name=’file’”和form表单中类型为file的input名字一致。

关于上面的Request Headers和Request Payload,详细的分析可以参考鸿洋大神的文章《从原理角度解析Android (Java) http 文件上传》。

下面是真正用于上传文件的POST请求代码:

    private static Request getPostRequest(String url, Part[] parts) {        MultipartBody.Builder builder=new MultipartBody.Builder().                setType(MultipartBody.FORM);        if(parts!=null && parts.length>0) {            for(int i=0; i<parts.length; i++)                builder.addPart(parts[i].getHeaders(), parts[i].getRequestBody());        }        RequestBody requestBody=builder.build();        return new Request.Builder().url(url).post(requestBody).build();    }

上传文件的请求体是MultipartBody而不是FormBody,在MultipartBody中可以添加需要上传的文件和需要提交的请求参数,不管是上传文件还是提交请求参数,都需要提供一个请求头和请求体。

我们提供了一个Part接口来提供请求头和请求体:

    public interface Part {        Headers getHeaders();        RequestBody getRequestBody();    }

同时我们也提交了实现Part接口的文件上传类(FilePart)和参数提交类(StringPart):

    public static class FilePart implements Part {        private File file;        private MediaType mediaType;        private String formName;        public FilePart(File f, String mt, String fn) {            file=f;            mediaType=MediaType.parse(mt);            formName=fn;        }        @Override        public Headers getHeaders() {            return Headers.of("Content-Disposition",                     "form-data; name=\""+formName+                    "\"; filename=\""+file.getName()+"\"");        }        @Override        public RequestBody getRequestBody() {            return RequestBody.create(mediaType, file);        }    }    public static class StringPart implements Part {        private String param;        private String formName;        public StringPart(String p, String fn) {            param=p;            formName=fn;        }        @Override        public Headers getHeaders() {            return Headers.of("Content-Disposition",                     "form-data; name=\""+formName+"\"");        }        @Override        public RequestBody getRequestBody() {            return RequestBody.create(null, param);        }    }

可以看到,如果想要上传文件,需要在请求头中添加form表单中类型为file的input名字和文件名,还要在请求体中添加文件的media type和File对象。

如果想要提交请求,需要在请求头中添加form表单中类型为text的input名字(对应与这里的StringPart,其实不一定是text类型),还要在请求体中添加请求参数。

下面我们进一步封装此POST请求:

    public static void post(String url, Part[] parts, Callback callback) {        Request request=getPostRequest(url, parts);        Call call=client.newCall(request);        try {            Response response=call.execute();            callback.onResponse(call, response);        } catch (IOException e) {            callback.onFailure(call, e);        }    }

好了,现在你只需要提供URL、上传内容和回调对象就行了。实际上,大多数情况下都会上传文件和提交字符串形式的请求参数,因此你可以使用我们提供的FilePart和StringPart,现在你只需要提供URL、文件和文件的media type(或者请求参数)以及回调对象即可。

现在我们马上来测试一下:

HttpUtils.post(    "http://localhost:8080/HttpServer/zzw/upload",     new Part[]{new FilePart(new File("res\\picture.jpg"), "image/jpg", "src")},     new Callback(){        @Override        public void onFailure(Call call, IOException e) {            e.printStackTrace();        }        @Override        public void onResponse(Call call, Response response) throws IOException {            System.out.println(response.body().string());        }    });

相应的服务端代码如下:

public class UploadAction extends ActionSupport {    private static final long serialVersionUID = -7408370789285125065L;    private File src;    private String srcContentType;    private String srcFileName;    private String destDir;    private String destFileName;    public void setSrc(File f) { src=f; }    public void setSrcContentType(String ct) { srcContentType=ct; }    public void setSrcFileName(String fn) { srcFileName=fn; }    public void setDestDir(String d) { destDir=d; }    public File getSrc() { return src; }    public String getSrcContentType() { return srcContentType; }    public String getSrcFileName() { return srcFileName; }    public String getDestDir() { return destDir; }    public String getDestFileName() { return destFileName; }    @Action(value="/zzw/upload",             params={"destDir", "F:\\workspace\\HttpServer\\res\\dest\\"},             results={@Result(name="success", location="upload.jsp")})    public String upload() throws Exception {        destFileName=UUID.randomUUID().toString().replaceAll("-", "")+                srcFileName.substring(srcFileName.indexOf('.'));        File dest=new File(destDir+destFileName);        FileUtils.copyFile(src, dest);        return SUCCESS;    }}

上传成功后可以在%SERVER_PROJECT%/res/dest/目录下找到客户端上传的文件。

下载文件

相信大多数人下载文件的次数都远远超过上传文件的次数,我们上面已经介绍了如何上传文件,那么你可能已经迫不及待地想要知道如何下载文件了。别急,下载文件的方法其实我们已经写好了,只需要稍微修改一下回调方法就OK了。

这里我们先来看看服务端到底是怎样相应一个下载文件的请求的:

public class DownloadAction extends ActionSupport {    private static final long serialVersionUID = 9078680027961237229L;    private String srcPath;    public String getSrcPath() { return srcPath; }    public void setSrcPath(String path) { srcPath=path; }    public InputStream getTarget() throws Exception {        return new FileInputStream(srcPath);    }}

此Action在struts.xml中的配置如下:

    <package name="zzw" extends="struts-default" namespace="/zzw">        <action name="download" class="com.zzw.action.DownloadAction">            <param name="srcPath">F:\workspace\HttpServer\res\src\butterfly.jpg</param>            <result name="success" type="stream">                <param name="contentType">image/jpg</param>                <param name="inputName">target</param>                <param name="contentDisposition">filename="butterfly.jpg"</param>                <param name="bufferSize">4096</param>            </result>        </action>    </package>

可以看到,服务端将返回一个类型为”image/jpg”的图像文件的字节流。我们只需要在客户端接收这个字节流并将其写入一个(图像)文件即可。

那么,你可能会问,到底是发送GET请求好呢,还是发送POST请求好呢?经测试后发现,不管是发送GET请求还是POST请求,都可以成功下载文件,不过发送GET请求更为简单。

下面是测试代码:

HttpUtils.get(    "http://localhost:8080/HttpServer/zzw/download",     null, new Callback(){        @Override        public void onFailure(Call call, IOException e) {            e.printStackTrace();        }        @Override        public void onResponse(Call call, Response response) throws IOException {            String fileName=response.header("Content-Disposition");            int end=fileName.lastIndexOf('"');            int begin=fileName.lastIndexOf('"', end-1)+1;            fileName=fileName.substring(begin, end);            OutputStream os=new FileOutputStream("res\\"+fileName);            os.write(response.body().bytes());            os.flush();            os.close();            System.out.println("download completed!");        }    });

注意,这里返回的Content-Disposition是filename=”butterfly.jpg”,所以需要从中提取文件名。

下载完成后可以在%CLIENT_PROJECT%/res/目录下找到下载的文件。

使用json进行通信

在网络通信中,json格式的文本是最常用的一种。既然如此,我们当然也想使用json格式的文本进行通信了。其实,json格式的文本说白了就是按照某个特定的格式编写的文本(字符串),我们只需要对其进行解析就行了(正则表达式)。

说到这,如果你真的自己去写json格式文本的解析器,那我也只能。。。Google提供了一个开源工具GSON,它使得解析json格式的文本变得像操作一个对象一样简单。

下面有一段json格式的文本:

{"name":"张三","age":22}[{"name":"李四","age":23},{"name":"王五","age":24}]

第一行是一个人,第二行是两个人的集合。我们定义一个Person类来表示这样一个人:

public class Person {    private String name;    private int age;    public Person(String n, int a) {        name=n;        age=a;    }    public void setName(String n) { name=n; }    public void setAge(int a) { age=a; }    public String getName() { return name; }    public int getAge() { return age; }    @Override    public String toString() {        return "{name:"+name+",age:"+age+"}";    }}

下面来看我们的测试代码:

HttpUtils.get("http://localhost:8080/HttpServer/zzw/json", null, new Callback(){    @Override    public void onFailure(Call call, IOException e) {        e.printStackTrace();    }    @Override    public void onResponse(Call call, Response response) throws IOException {        String msg=URLDecoder.decode(response.body().string(), "GBK");        String[] msgs=msg.split("\r\n");        System.out.println("response body="+msg);        Gson gson=new Gson();        Person person=gson.fromJson(msgs[0], Person.class);        System.out.println("person="+person);        List<Person> persons=gson.fromJson(msgs[1], new TypeToken<List<Person>>(){}.getType());        System.out.println("persons="+persons);    }});

相应的服务端代码如下:

public class JsonAction extends ActionSupport {    private static final long serialVersionUID = 1059643779198325028L;    @Action(value="/zzw/json",             results={@Result(name="success", location="json.jsp")})    public String execute() throws Exception {        HttpServletRequest request=ServletActionContext.getRequest();        request.setCharacterEncoding("GBK");        request.getSession(true);        HttpServletResponse response=ServletActionContext.getResponse();        response.setCharacterEncoding("GBK");        PrintWriter writer=response.getWriter();        Gson gson=new Gson();        Person person=new Person("张三", 22);        String single=URLEncoder.encode(gson.toJson(person), "GBK");        List<Person> persons=new ArrayList<>();        persons.add(new Person("李四", 23));        persons.add(new Person("王五", 24));        String list=URLEncoder.encode(gson.toJson(persons), "GBK");        writer.write(single+URLEncoder.encode("\r\n", "GBK")+list);        writer.flush();        writer.close();        return SUCCESS;    }}

可以看到,我们使用GSON解析这个json格式的文本只用了四行代码!

使用GSON时,需要先创建一个GSON对象"Gson gson=new Gson();",如果想把一个对象转换成json字符串,只需调用toJson方法;如果想把一个json字符串转换成对象,只需调用fromJson方法。

另外,注意解析一个对象和解析一个对象集合为json字符串需要传递不同的参数。解析对象只需要对象类型,而解析对象集合需要借助TypeToken类。

源代码

上述所有代码(包括客户端和服务端)都已上传到GitHub:
https://github.com/jzyhywxz/OKhttpTest

0 0
原创粉丝点击