最近公司在忙着做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