最近公司在忙着做rpc的框架,期间参考了thrift、pb、avro等不少的rpc框架,在实际的项目过程中碰到了不少PB和Jersey的问题,自己动手用PB、Jersey集成Spring框架搭建了一个简单的REST实例,做个小结。
简单的准备工作:
1、pb安装:https://developers.google.com/protocol-buffers/docs/javatutorial?hl=zh-CN,这个是PB的官方文档,选择合适的版本进行安装;里面也有详细的PB教程
安装完PB,可以测试一下指令protoc --help,如果终端有提示信息打印出来,证明安装完毕。
2、下载开发java服务端程序的jar包,我是用maven pom.xml建立依赖,自动加载进来的;给出所有的jar包列表,见下图:
3、详细的配置过程和代码
【a】首先建立一个web工程,将以上的jar包加入到工程的classpath中来;修改web.xml配置,加入spring的监听和Jersey的servlet配置。
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5"> <display-name>jerseyexam</display-name> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <listener> <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <servlet> <servlet-name>JerseySpring</servlet-name> <servlet-class>com.sun.jersey.spi.spring.container.servlet.SpringServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>JerseySpring</servlet-name> <url-pattern>/rest/*</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>
【b】然后在src目录下建立spring的配置文件applicationContext.xml,注意与web.xml中的<context-param>保持一直,才能在启动时加载到配置文件。下面是spring配置文件;
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd "> <context:component-scan base-package="net.sina.com.jersey.resource"/> <!-- <bean id="studentService" class="net.sina.com.jersey.dao.impl.StudentServiceImpl" /> --> </beans>解释一下,这里有个context:component-scan的标签,表示配置文件在加载时,会扫描该包下的所有Jersey资源类,多个包用逗号隔开;
【c】新建PB message定义文件(addressbook.proto)如下,如果定义看不懂的建议看一下pb安装文档中的Guide说明:
package pb; option java_package = "net.sina.com.pb"; option java_outer_classname = "AddressBookProtos"; message Person { required string name = 1; required int32 id = 2; optional string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { required string number = 1; optional PhoneType type = 2 [default = HOME]; } repeated PhoneNumber phone = 4; } message AddressBook { repeated Person person = 1; }
利用PB的编译器指令生成java端的代码,$SRC_DIR表示proto文件所在的目录,$DST_DIR表示编译输出文件的目录,指令执行后,会在输出目录按照定义文件中声明的package和classname生成对应的java 类。然后将生成的类,连带package的路径一起拷贝到工程的src目录。下面是编译指令
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto
【d】src目录下新建资源包:net.sina.com.jersey.resource,在资源包下建立Jersey资源类,BASE_URI是用来客户端请求测试用的,注意jerseyexam与部署的工程名一致。
package net.sina.com.jersey.resource; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Response; import org.springframework.stereotype.Component; import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.WebResource; import com.sun.jersey.api.client.config.ClientConfig; import com.sun.jersey.api.client.config.DefaultClientConfig; import net.sina.com.jersey.dao.AddressBookStore; import net.sina.com.pb.AddressBookProtos.Person; @Component @Path("/addressbook") public class AddressBookResource { private static final String BASE_URI = "http://localhost:8080/jerseyexam/rest/addressbook"; @PUT @Path("put") @Produces("application/x-protobuf") public Response putPerson(Person person) { AddressBookStore.store(person); return Response.ok().build(); } @POST @Path("add") @Consumes("application/x-protobuf") @Produces("application/x-protobuf") public Person reflect(Person person) { AddressBookStore.store(person); return person; } @GET @Path("get") @Produces("application/x-protobuf") public Person getPerson() { return Person .newBuilder() .setId(1) .setName("Sam") .setEmail("sam@sampullara.com") .addPhone( Person.PhoneNumber.newBuilder() .setNumber("415-555-1212") .setType(Person.PhoneType.MOBILE).build()) .build(); } @GET @Path("{name}") @Produces("application/x-protobuf") public Person getPerson(@PathParam("name") String name) { return AddressBookStore.getPerson(name); } public static void main(String[] args) throws Exception { ClientConfig cc = new DefaultClientConfig(); cc.getClasses().add(ProtobufMessageBodyReader.class); cc.getClasses().add(ProtobufMessageBodyWriter.class); Client c = Client.create(cc); WebResource r = c.resource(BASE_URI); Person p = r.path("get").get(Person.class); System.out.println(p); // URL url = new URL(BASE_URI+"/Jack"); // URLConnection urlc = url.openConnection(); // urlc.setDoInput(true); // urlc.setRequestProperty("Accept", "application/x-protobuf"); // p = Person.newBuilder().mergeFrom(urlc.getInputStream()).build(); // System.out.println(p); //Person p2 = r.path("add").type("application/x-protobuf").post(Person.class, p); //System.out.println(p2); } }这里对Jersey不是很熟的朋友建议看一下Jersey的官方文档: http://jersey.java.net/nonav/documentation/latest/user-guide.html,有比较详细的配置教程,里面有好多测试代码,刚开始搭建其实只需要保留Person getPerson()方法,获取一个固定格式的Person对象,其他方法都可以删除,包括main方法。这里需要说明一点,Jersey通过支持多种数据格式的传输,有json、xml、html、text/plain,我们这里需要添加对protobuf的支持,还需要实现MessageBodyReader、MessageBodyWriter这两个接口(都放在和资源类一个包中,工程启动即加载):
package net.sina.com.jersey.resource; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.Map; import java.util.WeakHashMap; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.ext.MessageBodyWriter; import javax.ws.rs.ext.Provider; import org.springframework.stereotype.Component; import com.google.protobuf.Message; @Component @Provider @Produces("application/x-protobuf") public class ProtobufMessageBodyWriter implements MessageBodyWriter<Message> { /** * a cache to save the cost of duplicated call(getSize, writeTo) to one * object. */ public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { return Message.class.isAssignableFrom(type); } private Map<Object, byte[]> buffer = new WeakHashMap<Object, byte[]>(); public long getSize(Message m, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { m.writeTo(baos); } catch (IOException e) { return -1; } byte[] bytes = baos.toByteArray(); buffer.put(m, bytes); return bytes.length; } public void writeTo(Message m, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { entityStream.write(buffer.remove(m)); } }
package net.sina.com.jersey.resource; import java.io.IOException; import java.io.InputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Type; import javax.ws.rs.Consumes; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.ext.MessageBodyReader; import javax.ws.rs.ext.Provider; import org.springframework.stereotype.Component; import com.google.protobuf.GeneratedMessage; import com.google.protobuf.Message; @Component @Provider @Consumes("application/x-protobuf") public class ProtobufMessageBodyReader implements MessageBodyReader<Message> { public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { return Message.class.isAssignableFrom(type); } public Message readFrom(Class<Message> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { try { Method newBuilder = type.getMethod("newBuilder"); GeneratedMessage.Builder builder = (GeneratedMessage.Builder) newBuilder.invoke(type); return builder.mergeFrom(entityStream).build(); } catch (Exception e) { throw new WebApplicationException(e); } } }
【e】至此基本上整个工程构建完毕,将工程打包或deploy到tomcat,启动tomcat,运行资源类AddressBookResource中的main方法进行测试;控制台输出如下:
name: "Sam" id: 1 email: "sam@sampullara.com" phone { number: "415-555-1212" type: MOBILE }
【f】构建Python客户端,新建一个Python工程,在pb安装目录中,找到python目录,将其中的google整个目录拷贝到python工程 src目录(将pb模块加载到classpath),用下面指令编译生成python 代码,将生成的模块文件也加入到classpath中;
protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/addressbook.proto
在生成的addressbook_pb2.py文件中有一句from google.protobuf import descriptor_pb2,这一句其实可以注释掉,否则可能报错
【g】新建python客户端测试module
''' Created on 2012-5-24 @author: yang ''' import urllib2 import addressbook_pb2 if __name__ == '__main__': f = urllib2.urlopen("http://localhost:8080/jerseyexam/rest/addressbook/get") person = addressbook_pb2.Person() person.ParseFromString(f.read()) print("person name is:{0}/id:{1}/email:{2}".format(person.name, person.id, person.email)) print("------------------------------------------------------------------") for phoneNum in person.phone: print('phone number is :{0}/phone type is:{1}'.format(phoneNum.number, phoneNum.type))运行结果
person name is:Sam/id:1/email:sam@sampullara.com ------------------------------------------------------------------ phone number is :415-555-1212/phone type is:0
小结:两个Message处理类和服务端资源文件,都要放到scan目录,工程启动时自动加载,否则可能报Valid request或者500或MediaType Unsupported错误,很奇怪的是我开始放在两个不同的包中,两个包在spring scan目录配置中都加入了,但是还是一直报500 Internal Server error,后来没办法,放在一个包中就好了。
下面是代码的下载地址:http://download.csdn.net/detail/yangfanchao/4327120