Accepting a List as a parameter to a Jersey webser

2019-05-10 23:40发布

问题:

I had an existing Jersey webservice method that accepts a number of parameters via Http POST method which is designed to handle a standard form data, content type of application/x-www-form-urlencoded; one of these parameters was a list of Strings. Below is an example of the method signature I have.

@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response createItem(
        @FormParam("p1") long p1,
        @FormParam("p2") String p2,
        @FormParam("p3") List<String> p3,
        @FormParam("p4") String p4,
        @Context UriInfo uriInfo
) throws SQLException {

This was working correctly and when multiple p3 parameters are passed in the List is correctly generated by Jersey and passed into the method.

I now needed to make an alternative version of this method that would accept a multi-part request so that a file could also be uploaded along with the existing parameters. So I created a very similar method signature to consume the multi-part requests, example shown below.

@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response createItemWithFile(
        @FormDataParam("p1") long p1,
        @FormDataParam("p2") String p2,
        @FormDataParam("p3") List<String> p3,
        @FormDataParam("p4") String p4,
        @FormDataParam("file") InputStream inputStream,
        @Context UriInfo uriInfo
) throws SQLException {

I changed the FormParam annotations to FormDataParam as I believe this is needed when consuming multi-part data. I have been trying to call this method from a JUnit test using RESTAssured to make the call (the same as had been done for the original method) but I get the following error.

java.lang.IllegalArgumentException: wrong number of arguments
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at com.sun.jersey.spi.container.JavaMethodInvokerFactory$1.invoke(JavaMethodInvokerFactory.java:60)
at com.sun.jersey.server.impl.model.method.dispatch.AbstractResourceMethodDispatchProvider$ResponseOutInvoker._dispatch(AbstractResourceMethodDispatchProvider.java:205)
at com.sun.jersey.server.impl.model.method.dispatch.ResourceJavaMethodDispatcher.dispatch(ResourceJavaMethodDispatcher.java:75)
at com.sun.jersey.server.impl.uri.rules.HttpMethodRule.accept(HttpMethodRule.java:288)

Having put some break points into the Jersey code, at some of the points indentified in the stack trace, it seems that it has identified the correct method to invoke, but in the list of parameters it is trying to pass to it, p3 is omitted.

Is there something different that needs to be done in order to support accepting a List as an input when dealing with multi-part data? Given this is an optional parameter I expected it should be possible to omit it anyway, this is the case with the original method.

The RESTAssured code in the test being used to call the method is as follows.

Response response = given()                    
                .header("my_header", "xyz")
                .param("p1", "8000040")
                .param("p2", "sample string") 
                .param("p3", "first_value")
                .param("p4", "abcde")
                .multiPart("file", myFile1, inputStream)
                .expect()

I have also tried when using formParam in the RESTAssured test code in place of param, but get the same result.

Thanks in advance, any help would be appreciated.

回答1:

Having stepped through some more of the jersey code, my conclusion is that I can not have a parameter of type List on my method when using multi-part. At one point in the process Jersey loops through each paramter on the method finding an Injectable to read the value for each parameter (sorry probably not a great explaination, but I have debugged as much as I needed to), within the class com.sun.jersey.multipart.impl.FormDataMultiPartDispatchProvider in the getInjectables method is the following code:

 private List<Injectable> getInjectables(AbstractResourceMethod method) {
    List<Injectable> list = new ArrayList<Injectable>(method.getParameters().size());
    for (int i = 0; i < method.getParameters().size(); i++) {
        Parameter p = method.getParameters().get(i);
        if (Parameter.Source.ENTITY == p.getSource()) {
            if (FormDataMultiPart.class.isAssignableFrom(p.getParameterClass())) {
                list.add(new FormDataMultiPartInjectable());
            } else {
                list.add(null);
            }
        } else if (p.getAnnotation().annotationType() == FormDataParam.class) {
            if (Collection.class == p.getParameterClass() || List.class == p.getParameterClass()) {
                Class c = ReflectionHelper.getGenericClass(p.getParameterType());
                if (FormDataBodyPart.class == c) {
                    list.add(new ListFormDataBodyPartMultiPartInjectable(p.getSourceName()));
                } else if (FormDataContentDisposition.class == c) {
                    list.add(new ListFormDataContentDispositionMultiPartInjectable(p.getSourceName()));
                }
            } else if (FormDataBodyPart.class == p.getParameterClass()) {
                list.add(new FormDataBodyPartMultiPartInjectable(p.getSourceName()));
            } else if (FormDataContentDisposition.class == p.getParameterClass()) {
                list.add(new FormDataContentDispositionMultiPartInjectable(p.getSourceName()));
            } else {
                list.add(new FormDataMultiPartParamInjectable(p));
            }
        } else {
            Injectable injectable = getInjectableProviderContext().getInjectable(p, ComponentScope.PerRequest);
            list.add(injectable);
        }
    }
    return list;
}

So where it sees the parameter type is a List or Collection it will ignore it where the generic type is anything other than FormDataBodyPart or FormDataContentDisposition.

To get round the issue I have just changed my method to accept a comma delimited String for p3 in place of a List.



回答2:

I came across another solution to this which may be better (and simpler) than having to manually process a comma separated version of the list etc. Posted belatedly to help others that find this post.

Change the multipart parameter from:

@FormDataParam("p3") List<String> p3,

to

@FormDataParam("p3") List<FormDataBodyPart> p3,

and then you have your list in P3 where you can get the parameter value for each FormDataBodyPart element using getValue().

Source: receiving-arrays-from-form-elements-with-jersey