spring mvc 4.1支持protobuf converters

#简介
本文旨在向读者分享springMVC项目中Protocol Buffers的一个使用技巧,前提是需要具备对springMVC和Protocol Buffers的基本了解。
Protocol Buffers(以下简称PB)是谷歌推出的一种数据交换格式,高效、易扩展、跨语言(C++, Java, Python)。将它用于网络传输是一个不错的选择。我们服务器与客户端的通讯就是使用它来做序列化与反序列化的。
#Spring原生支持
spring 4.1以后开始支持protobuf HttpMessageConverter 详细的配置如下:
##pom.xml配置

<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>2.5.0</version>
</dependency>
<dependency>
    <groupId>com.googlecode.protobuf-java-format</groupId>
    <artifactId>protobuf-java-format</artifactId>
    <version>1.2</version>
</dependency>

还有要引入spring核心包及spring mvc包 版本4.1.6.RELEASE,如果不知道怎么引入,百度一下

##配置portoc插件
也可以不用这个插件,得需要自己用protoc.exe生产java文件

<plugin>
    <groupId>com.google.protobuf.tools</groupId>
    <artifactId>maven-protoc-plugin</artifactId>
    <version>0.1.10</version>
    <executions>
        <execution>
            <id>generate-sources</id>
            <goals>
                <goal>compile</goal>
            </goals>
            <phase>generate-sources</phase>
            <configuration>
                <protoSourceRoot>${basedir}/src/main/proto/</protoSourceRoot>
                <includes>
                    <param>**/*.proto</param>
                </includes>
            </configuration>
        </execution>
    </executions>
    <configuration>
        <protocExecutable>D:/dev/protoc.exe</protocExecutable><!--protoc.exe文件地址,使用2.5版本-->
    </configuration>
</plugin>

##springmvc-servlet.xml配置

<mvc:annotation-driven>
    <mvc:message-converters>
    <!--看了一下源码,客户端请求类型必须设置成application/x-protobuf采用用这个类来解析实体

        <bean class="org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter">
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>

##user.proto文件

目录/src/main/proto/和protoc插件配置路径保持一致,如果不想用插件可以自己用protoc.exe生成java文件

 package com.my.pb;

 option java_package = "com.my.pb";
 option java_outer_classname = "UserProto";

 message User {
   optional int64 id = 1;
   optional string name = 2;
   message PhoneNumber {
       required string number = 1;
     }
     repeated PhoneNumber phone = 4;
 }

##controller类

@Controller
public class ProtobufController{

    @RequestMapping(value = "/proto/write1",method= RequestMethod.POST)
    public ResponseEntity<UserProto.User> protoWrite1(RequestEntity<UserProto.User> requestEntity) {
        //System.out.println("server===\n");
        UserProto.User user =  requestEntity.getBody();
        System.out.println("server===\n" + user);
        return ResponseEntity.ok(user);
    }
}

##客户端测试代码,使用Apache的httpclient进行测试

@Test
public void testWrite() throws IOException {
    CloseableHttpClient httpclient = HttpClients.createDefault();
    HttpPost httpPost = new HttpPost(baseUri+"/proto/write1");

    UserProto.User user =   UserProto.User.newBuilder().setId(1).setName("zhangsan").addPhone(UserProto.User.PhoneNumber.newBuilder().setNumber("18611163408")).build();//构造

    ByteArrayInputStream inputStream = new ByteArrayInputStream(user.toByteArray());
    InputStreamEntity inputStreamEntity = new InputStreamEntity(inputStream);
    //这两行很重要的,是告诉springmvc客户端请求和响应的类型,指定application/x-protobuf类型,spring会用ProtobufHttpMessageConverter类来解析请求和响应的实体
    httpPost.addHeader("Content-Type","application/x-protobuf");
    httpPost.addHeader("Accept", "application/x-protobuf");

httpPost.setEntity(inputStreamEntity);
CloseableHttpResponse response2 = httpclient.execute(httpPost);

    try {
        System.out.println(response2.getStatusLine());
        HttpEntity entity2 = response2.getEntity();

        ByteArrayOutputStream buf = new ByteArrayOutputStream();
        entity2.writeTo(buf);
        System.out.println(new String(buf.toByteArray())+"#################");
        UserProto.User user2 = UserProto.User.parseFrom(buf.toByteArray());
        System.out.println(user2);
    } finally {
        response2.close();
    }
}

##总结
当时调试的时候试了很多次,响应总是被转成xml类型,最后看了源码才发现客户端要设置httpPost.addHeader(“Accept”, “application/x-protobuf”);
其实也可以自己实现HttpMessageConverter,也不是很麻烦。


#自己实现HttpMessageConverter

PB的使用方法大致是这样的:

1.服务器与客户端约定要交换的数据格式,然后用PB的语法将其定义为.proto文件。

2.用PB自带的编译器编译.proto文件,生成对应语言的代码文件,比如java的类文件。

3.生成好的代码包含了对象定义、对象的序列化与反序列化等功能。

PB的详细使用方法不是本文的讨论范畴,如需学习请移步【PB官网】【博客A】

我曾使用过一款与PB类似的开源项目Hessian,spring中集成了Hessian的服务出口类,相当于一个特殊的servlet,需要配置Web.xml和对应的-servlet.xml文件,它可以完成方法参数和返回值的自动转换。客户端通过代理工厂HessianProxyFactory访问服务器上的接口,就像访问本地方法一样方便,是货真价实的RPC(远程过程调用)。详细参考【Hessian官网】【博客B】
受惯性思想影响,我在初次使用PB时到处寻找其与springMVC结合的配置方法。几经折腾后才发现,PB的用法与Hessian很不一样。PB的用法更像是使用工具类,我们需要调用它的parseFrom、writeTo、toByteArray等方法实现对象与流的转换。因此有这些方法就够了,我们无需任何配置。在controller中的使用举例如下:

InputStream in = request.getInputStream(); 
UseClientInfo useClientInfo = UseClientInfo.parseFrom(in); 

说明:这是从输入流中反序列化一个PB对象UseClientInfo

byte[] content = useClientInfoBuilder.build().toByteArray(); 
response.setContentType("application/x-protobuf"); 
response.setContentLength(content.length); 
response.setCharacterEncoding("utf-8"); 
OutputStream out = response.getOutputStream(); 
out.write(content); 
out.flush(); 
out.close(); 

说明:这是往输出流中写序列化后的对象

客户端也是类似的用法,从这里可以看出PB算不上是一个完整的RPC,它只是提供了序列化与反序列化的方法。与Hessian相比的优势是无需配置、易用、易扩展,劣势是集成度不够高,需要用户参与序列化/反序列化工作,在各个controller的各个接口中频繁重复这个工作着实让人厌烦。有没有一种方式可以让controller从这些工作中脱离,接口的入参和返回值直接使用PB对象就可以了呢?

答案是肯定的,接下来讲解通过扩展springMVC的AbstractHttpMessageConverter完成PB对象和流之间的自动转换。(ps:终于到重点了,我也知道有点啰嗦,不过我希望讲清楚一件事的来龙去脉_)。

【首先】扩展AbstractHttpMessageConverter,实现我们自己的转换器PBMessageConverter。

package ……保密……; 

import java.io.IOException; 
import java.io.InputStream; 
import java.io.OutputStream; 
import java.lang.reflect.InvocationTargetException; 
import java.lang.reflect.Method; 
import java.nio.charset.Charset; 

import org.springframework.http.HttpInputMessage; 
import org.springframework.http.HttpOutputMessage; 
import org.springframework.http.MediaType; 
import org.springframework.http.converter.AbstractHttpMessageConverter; 
import org.springframework.http.converter.HttpMessageNotReadableException; 
import org.springframework.http.converter.HttpMessageNotWritableException; 
import com.google.protobuf.GeneratedMessage; 

/**
 * pb对象转换,自动完成pb对象的序列化与反序列化,让controller的接口脱离这些重复工作
 * @author shannon
 * @time Dec 6, 2012 12:16:11 PM
 * @email shannonchou@126.com
 */ 
public class PBMessageConverter extends AbstractHttpMessageConverter<GeneratedMessage> { 


    public PBMessageConverter() { 
        //设置该转换器支持的媒体类型 
        super(new MediaType("application", "x-protobuf", Charset.forName("UTF-8"))); 
    } 

    @Override 
    protected GeneratedMessage readInternal( 
            Class<? extends GeneratedMessage> arg0, HttpInputMessage arg1) 
            throws IOException, HttpMessageNotReadableException { 
        Method parseMethod; 
        try { 
            //利用反射机制获得parseFrom方法 
            parseMethod = arg0.getDeclaredMethod("parseFrom", InputStream.class); 
        } catch (SecurityException e) { 
            e.printStackTrace(); 
            return null; 
        } catch (NoSuchMethodException e) { 
            e.printStackTrace(); 
            return null; 
        } 
        try { 
            //调用parseFrom方法从InputStream中反序列化PB对象 
            return (GeneratedMessage) parseMethod.invoke(arg0, arg1.getBody()); 
        } catch (IllegalArgumentException e) { 
            e.printStackTrace(); 
            return null; 
        } catch (IllegalAccessException e) { 
            e.printStackTrace(); 
            return null; 
        } catch (InvocationTargetException e) { 
            e.printStackTrace(); 
            return null; 
        } 
    } 

    @Override 
    protected boolean supports(Class<?> arg0) { 
        //如果是GeneratedMessage的子类则支持 
        return GeneratedMessage.class.isAssignableFrom(arg0); 
    } 

    @Override 
    protected void writeInternal(GeneratedMessage arg0, HttpOutputMessage arg1) 
            throws IOException, HttpMessageNotWritableException { 
        //不用自己设置type,父类会根据构造函数中给的type设置 
        //arg1.getHeaders().setContentType(new MediaType("application","x-protobuf", Charset.forName("UTF-8"))); 
        OutputStream outputStream = arg1.getBody(); 

        //自己直接设置contentLength会导致异常,覆盖父类的getContentLength()方法才是标准做法 
        //arg1.getHeaders().setContentLength(bytes.length); 
        arg0.writeTo(outputStream); 
        outputStream.flush(); 
        outputStream.close(); 
    } 

    @Override 
    protected Long getContentLength(GeneratedMessage t, MediaType contentType) { 
        return Long.valueOf(t.toByteArray().length); 
    } 
} 

说明:
1.我们给AbstractHttpMessageConverter的泛型变量是GeneratedMessage,而它是所有生成的PB对象的父类,我们可以通过它来识别PB对象。

2.覆盖父类构造函数的同时需要给出支持的媒体类型,我这里设定的是"application/x-protobuf",这要求客户端在建立连接时设置同样的类型。如下:

URL target = new URL(url);     
HttpURLConnection conn = (HttpURLConnection) target.openConnection();         
conn.setRequestMethod("GET");     
conn.setRequestProperty("Content-Type", "application/x-protobuf");     
conn.setRequestProperty("Accept", "application/x-protobuf"); 

3.我的PBMessageConverter利用了泛型和反射机制,已经实现了所有PB对象的转换,没有与项目耦合的任何代码,如果需要的话可以直接copy去用,不用重写。
【其次】配置*-servlet.xml文件

<mvc:annotation-driven> 
    <mvc:message-converters> 
        <bean class="com.example.…保密….common.converter.PBMessageConverter" /> 
    </mvc:message-converters> 
</mvc:annotation-driven> 

说明:这样就将PBMessageConverter注册到了springMVC的转换器列表中。注意这里需要引入springMVC的“mvc”命名空间。
【最后】controller中通过如下方式使用

@RequestMapping("connected") 
public void connected(@RequestBody UseClientInfo useClientInfo, HttpServletRequest request, HttpServletResponse response) throws IOException { 
    log.info(useClientInfo); 
} 

@ResponseBody 
@RequestMapping("get_useclientinfo") 
public UseClientInfo getUseClientInfo(HttpServletResponse response) throws IOException{ 
    UseClientInfo.Builder useClientInfoBuilder = UseClientInfo.newBuilder(); 
    ……业务逻辑略…… 
    return useClientInfoBuilder.build(); 
}

说明:
1.PB参数前加上@RequestBody注解,如果返回值是PB对象则加上@ResponseBody注解。

至此,我们的controller直接操作对象,再也不需要重复的进行流与对象的转换了。

【PB官网】:http://code.google.com/p/protobuf/

【博客A】:http://blog.csdn.NET/farmmer/article/details/2689049

【Hessian官网】:http://hessian.caucho.com/

【博客B】:http://blog.csdn.net/chenweitang123/article/details/6334097

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值