利用Jersey和Google ProtoBuf 集成Spring搭建REST服务

时间:2020-12-12 19:33:51

最近公司在忙着做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包列表,见下图:

利用Jersey和Google ProtoBuf 集成Spring搭建REST服务


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