JavaEE 使用OKhttp和Action进行通信

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>

0000

这张图片是上传某个文件(图片)时抓获的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类。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值