【转载】在Jersey JAX-RS 处理泛型List等Collection
在Java中,从1.5开始,我们就可以使用泛型了(generic),这看上去很像C++ Template,但是实际上它们是不同的。在这里我不想过多的描述细节,你可以从Google上搜索一下。 但是,泛型已经变得如此复杂,以至于已经有500多页的 FAQ。
我们长话短说:泛型提供了编译时类型安全,所以也消除了类型转换的(cast)的需要。它是通过被称为类型消除(type erasure)的编译时技术来实现的。 泛型FAQ解释了所有的细节,对我来说它就是Java泛型的圣经。
在有些情况下,我们需要从JAXRS的Response类的资源方法中饭后参数化的类型。 因为类型消除,使得此种情况下Jersey运行环境需要特殊的处理来决定为泛型类型选择相应的MessageBodyWriter。
对于使用Jersey的JAX-RS的开发者而言有很多可供选择的技术,下面我将详细的讨论其中的几个。
让我们看一个简单的域模型:Employee。
1 package net.javasight; 2 3 import javax.xml.bind.annotation.XmlRootElement; 4 5 @XmlRootElement(name = "employee") 6 public class EmployeeBean { 7 private Long id; 8 private String firstName; 9 private String lastName; 10 11 public EmployeeBean() { 12 // required for JAXB 13 } 14 15 public EmployeeBean(Long id, String firstName, String lastName) { 16 this.id = id; 17 this.firstName = firstName; 18 this.lastName = lastName; 19 } 20 21 public Long getId() { 22 return id; 23 } 24 25 public void setId(Long id) { 26 this.id = id; 27 } 28 29 public String getFirstName() { 30 return firstName; 31 } 32 33 public void setFirstName(String firstName) { 34 this.firstName = firstName; 35 } 36 37 public String getLastName() { 38 return lastName; 39 } 40 41 public void setLastName(String lastName) { 42 this.lastName = lastName; 43 } 44 }
employee资源的一个实现如下:
1 @Path("/employees") 2 public class EmployeeResource { 3 @GET 4 public Collection<EmployeeBean> getEmployees() { 5 EmployeeBean emp = new EmployeeBean(1L, "John", "Doe"); 6 return Collections.singletonList(emp); 7 } 8 }
在这种情况下,我们从资源方法中返回了EmployeeBean的Collection集合。如果你从如下地址(http://localhost:9998/employees)访问该资源,将会产生如下的XML输出:
1 <? xml version = "1.0" encoding = "UTF-8" standalone = "yes" ?> 2 <employeeBeans> 3 <employee> 4 <firstName>John</firstName> 5 <id>1</id> 6 <lastName>Doe</lastName> 7 </employee> 8 </employeeBeans>
我希望看到employee在<employees>标签中,而不是在<employeeBeans>中。这里需要做一些额外的配置来产生该格式。那么让我们修改下Employee的POJO来包含该集合。
1 package net.javasight; 2 3 import java.util.List; 4 5 import javax.xml.bind.annotation.XmlRootElement; 6 7 @XmlRootElement 8 public class Employees { 9 private List<EmployeeBean> employee; 10 11 public Employees(List<EmployeeBean> employee) { 12 this.employee = employee; 13 } 14 15 public Employees() { 16 // required for JAXB 17 } 18 19 public List<EmployeeBean> getEmployee() { 20 return employee; 21 } 22 23 public void setEmployee(List<EmployeeBean> employee) { 24 this.employee = employee; 25 } 26 }
让我们完成EmployeeResource的方法来使用自定义的POJO来产生相关的XML。
1 package net.javasight; 2 3 @Path("/employees") 4 public class EmployeeResource { 5 @GET 6 @Path("test1") 7 public Employees getEmployees1() { 8 EmployeeBean emp = new EmployeeBean(1L, "John", "Doe"); 9 Employees employees = new Employees(Collections.singletonList(emp)); 10 return employees; 11 } 12 13 @GET 14 @Path("test2") 15 public Response getEmployees2() { 16 EmployeeBean emp = new EmployeeBean(1L, "John", "Doe"); 17 Employees employees = new Employees(Collections.singletonList(emp)); 18 return Response.ok(employees).build(); 19 } 20 }
现在访问http://localhost:9988/employees/test1或http://localhost:9998/employees/test2应该会产生如下的XML:
1 <? xml version = "1.0" encoding = "UTF-8" standalone = "yes" ?> 2 <employees> 3 <employee> 4 <firstName>John</firstName> 5 <id>1</id> 6 <lastName>Doe</lastName> 7 </employee> 8 </employees>
但是,难道我们需要这样糟糕的方法来产生该输出吗?已经不需要了,在Jersey1.2发布后有了一些改善。可以启用资源配置的FEATURE_XMLROOTELEMENT_PROCESSING 特性后,应当可以自动的产生该输出。因此,访问http://localhost:9998/employees/test1应该产生这种的格式XML。该属性默认是禁用的。
现在让我们看看在JAX-RS Response中返回参数化类型会遇到的实际的问题。我在EmployeeResource中添加了另一个方法。
@GET @Path("test3") @Produces(MediaType.APPLICATION_XML) public Response getEmployees3() { EmployeeBean emp = new EmployeeBean(1L, "John", "Doe"); List<EmployeeBean> list = new ArrayList<EmployeeBean>(); list.add(emp); return Response.ok(list).build(); }
现在通过http://localhost:9998/employees/test3访问该方法应该会产生如下异常。我相信该异常在Jersey/JAX-RS用户中应该很常见。
1 SEVERE: A message body writer for Java class java.util.ArrayList, and Java type class java.util.ArrayList, and MIME media type application/xml was not found 2 Jul 24 , 2010 11 : 58 : 55 PM com.sun.jersey.spi.container.ContainerResponse write 3 SEVERE: The registered message body writers compatible with the MIME media type are: 4 application/xml -> 5 com.sun.jersey.core.impl.provider.entity.XMLJAXBElementProvider$App 6 com.sun.jersey.core.impl.provider.entity.DocumentProvider 7 com.sun.jersey.core.impl.provider.entity.SourceProvider$SourceWriter 8 com.sun.jersey.core.impl.provider.entity.XMLRootElementProvider$App 9 com.sun.jersey.core.impl.provider.entity.XMLListElementProvider$App 10 */* -> 11 com.sun.jersey.core.impl.provider.entity.FormProvider 12 com.sun.jersey.server.impl.template.ViewableMessageBodyWriter 13 com.sun.jersey.core.impl.provider.entity.StringProvider 14 com.sun.jersey.core.impl.provider.entity.ByteArrayProvider 15 com.sun.jersey.core.impl.provider.entity.FileProvider 16 com.sun.jersey.core.impl.provider.entity.InputStreamProvider 17 com.sun.jersey.core.impl.provider.entity.DataSourceProvider 18 com.sun.jersey.core.impl.provider.entity.XMLJAXBElementProvider$General 19 com.sun.jersey.core.impl.provider.entity.ReaderProvider 20 com.sun.jersey.core.impl.provider.entity.DocumentProvider 21 com.sun.jersey.core.impl.provider.entity.StreamingOutputProvider 22 com.sun.jersey.core.impl.provider.entity.SourceProvider$SourceWriter 23 com.sun.jersey.core.impl.provider.entity.XMLRootElementProvider$General 24 com.sun.jersey.core.impl.provider.entity.XMLListElementProvider$General 25 Jul 24 , 2010 11 : 58 : 55 PM com.sun.jersey.spi.container.ContainerResponse traceException 26 SEVERE: Mapped exception to response: 500 (Internal Server Error) 27 javax.ws.rs.WebApplicationException
要修复该问题,我们需要告诉JAX-RS运行时响应实体的类型,在我们的情况下是Employee的集合。JAX-RS APIGenericEntity正是用于该目的。GenericEntity可以用于反映响应实体中的泛型类型。 EmployeeResource方法被更新在返回集合类型时使用GenericEntity。
@GET @Path("test4") @Produces(MediaType.APPLICATION_XML) public Response getEmployees4() { EmployeeBean emp = new EmployeeBean(1L, "John", "Doe"); List<EmployeeBean> list = new ArrayList<EmployeeBean>(); list.add(emp); GenericEntity entity = new GenericEntity<List<EmployeeBean>>(list) { }; return Response.ok(entity).build(); }
访问http://localhost:9998/employees/test4应该会产生我们所需要的输出。
除了这种方式,Jersey1.2引入了新的APIJResponse来支持这个,并且更优。JResponse是类型安全的Response,它保留了响应实体的类型信息,因此不需要额外的使用GenericEntity。
使用JResponse更新后的方法如下:
1 @GET 2 @Path("test5") 3 @Produces(MediaType.APPLICATION_XML) 4 public JResponse<List<EmployeeBean>> getEmployees5() { 5 EmployeeBean emp = new EmployeeBean(1L, "John", "Doe"); 6 List<EmployeeBean> list = new ArrayList<EmployeeBean>(); 7 list.add(emp); 8 return JResponse.ok(list).build(); 9 }
访问http://localhost:9998/employees/test5应该会产生我们所需要的输出.
这两种方式实现起来都很容易。主要的不同是GenericEntity是JAX-RS API然而JResponse是Jersey API,它可能不能在其他的JAX-RS实现中使用,因此不具备移植性。 如果你仅仅使用Jersey,那么JResponse是更好的选择,因为它是类型安全的并与Response兼容。
下面是完整的服务端代码:
1 package net.javasight; 2 3 import com.sun.grizzly.http.SelectorThread; 4 import com.sun.jersey.api.container.grizzly.GrizzlyWebContainerFactory; 5 import com.sun.jersey.core.util.FeaturesAndProperties; 6 import javax.ws.rs.core.UriBuilder; 7 import java.io.IOException; 8 import java.net.URI; 9 import java.util.HashMap; 10 import java.util.Map; 11 12 public class Main { 13 private static int getPort(int defaultPort) { 14 String port = System.getenv("JERSEY_HTTP_PORT"); 15 if (null != port) { 16 try { 17 return Integer.parseInt(port); 18 } catch (NumberFormatException e) { 19 } 20 } 21 return defaultPort; 22 } 23 24 private static URI getBaseURI() { 25 return UriBuilder.fromUri("http://localhost/").port(getPort(9998)) 26 .build(); 27 } 28 29 public static final URI BASE_URI = getBaseURI(); 30 31 protected static SelectorThread startServer() throws IOException { 32 final Map<String, String> initParams = new HashMap<String, String>(); 33 initParams.put("com.sun.jersey.config.property.packages", 34 "com.employee.resources"); 35 initParams.put(FeaturesAndProperties.FEATURE_XMLROOTELEMENT_PROCESSING, 36 "true"); 37 System.out.println("Starting grizzly..."); 38 SelectorThread threadSelector = GrizzlyWebContainerFactory.create( 39 BASE_URI, initParams); 40 return threadSelector; 41 } 42 43 public static void main(String[] args) throws IOException { 44 SelectorThread threadSelector = startServer(); 45 System.out 46 .println(String 47 .format( 48 "Jersey app started with WADL available at " 49 + "%sapplication.wadl\nTry out %shelloworld\nHit enter to stop it...", 50 BASE_URI, BASE_URI)); 51 System.in.read(); 52 threadSelector.stopEndpoint(); 53 } 54 }
以上内容转自http://javasight.net/2011/05/generified-collections-in-jersey/,感谢作者,但是作者只提供了服务端的泛型处理,并没有提供客户端如何使用泛型。
个人解决客户端接受泛型方法:
服务端代码:
@GET @Path("test4") @Produces(MediaType.APPLICATION_JSON) public Response getEmployees4() { EmployeeBean emp = new EmployeeBean(1L, "John", "Doe"); EmployeeBean emp1 = new EmployeeBean(2L, "zhangsan", "lisi"); List<EmployeeBean> list = new ArrayList<EmployeeBean>(); list.add(emp); list.add(emp1); GenericEntity<List<EmployeeBean>> entity = new GenericEntity<List<EmployeeBean>>(list) { }; return Response.ok(entity).build(); }
客户端代码:
1 GenericType<List<EmployeeBean>> gt = new GenericType<List<EmployeeBean>>(){}; 2 Object obj = wr.path("/message/test4").get(gt); 3 System.out.println(obj);
加红字体是rest服务地址 wr是webResource对象。