Jersey: hardcode POST/PUT ObjectMapper, without needing Content-Type header

孤街浪徒 提交于 2019-12-23 22:09:18

问题


I have a Jersey 1.19.1 resource that implements a @PUT and @POST method. The @PUT method expects a JSON string as the input/request body, while the @POST method accepts plain text. For the JSON mapping I am using Jackson 2.8.

Since the resource is defined to work this way, I don't want the client to be required to specify a Content-Type request header, just because Jersey needs it to figure out which ObjectMapper to use on the request body.

What I want instead, is to tell Jersey "Use this ObjectMapper for this @PUT input", or "Always assume this input will have an application/json Content-Type on this method."

@Produces(MediaType.APPLICATION_JSON)
@Path("/some/endpoint/{id}")
public class MyResource {

    @PUT
    public JsonResult put(
        @PathParam("id") String id,
        // this should always be deserialized by Jackson, regardless of the `Content-Type` request header.
        JsonInput input
    ) {
        log.trace("PUT {}, {}, {}", id, input.foo, input.bar);
        return new JsonResult("PUT result");
    }

    @POST
    public JsonResult post(
        @PathParam("id") String id,
        // this should always be treated as plain text, regardless of the `Content-Type` request header.
        String input
    ) {
        log.trace("POST {}, {}", id, input);
        return new JsonResult("POST result");
    }
}

I only found this answer, but that's not what I'm looking for, as the solution there seems to be that the client should be required to add the correct Content-Type header, or otherwise do the object mapping manually.


回答1:


I managed to come up with a workaround. Instead of declaring which ObjectMapper to use on a Jersey resource method, I decided to create a ResourceFilter, corresponding ResourceFilterFactory, and an annotation type. Whenever a resource class or method is annotated with this type, the ResourceFilter will override the request's Content-Type to whatever is declared in the annotation's parameter.

Here's my code:

OverrideInputType annotation:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface OverrideInputType {
    // What the Content-Type request header value should be replaced by
    String value();

    // which Content-Type request header values should not be replaced
    String[] except() default {};
}

OverrideInputTypeResourceFilter:

public class OverrideInputTypeResourceFilter implements ResourceFilter, ContainerRequestFilter {
    private MediaType targetType;
    private Set<MediaType> exemptTypes;

    OverrideInputTypeResourceFilter(
        @Nonnull String targetType,
        @Nonnull String[] exemptTypes
    ) {
        this.targetType = MediaType.valueOf(targetType);
        this.exemptTypes = new HashSet<MediaType>(Lists.transform(
            Arrays.asList(exemptTypes),
            exemptType -> MediaType.valueOf(exemptType)
        ));
    }

    @Override
    public ContainerRequest filter(ContainerRequest request) {
        MediaType inputType = request.getMediaType();
        if (targetType.equals(inputType) || exemptTypes.contains(inputType)) {
            // unmodified
            return request;
        }

        MultivaluedMap<String, String> headers = request.getRequestHeaders();
        if (headers.containsKey(HttpHeaders.CONTENT_TYPE)) {
            headers.putSingle(HttpHeaders.CONTENT_TYPE, targetType.toString());
            request.setHeaders((InBoundHeaders)headers);
        }
        return request;
    }

    @Override
    public final ContainerRequestFilter getRequestFilter() {
        return this;
    }

    @Override
    public final ContainerResponseFilter getResponseFilter() {
        // don't filter responses
        return null;
    }
}

OverrideInputTypeResourceFilterFactory:

public class OverrideInputTypeResourceFilterFactory implements ResourceFilterFactory {

    @Override
    public List<ResourceFilter> create(AbstractMethod am) {
        // documented to only be AbstractSubResourceLocator, AbstractResourceMethod, or AbstractSubResourceMethod
        if (am instanceof AbstractSubResourceLocator) {
            // not actually invoked per request, nothing to do
            log.debug("Ignoring AbstractSubResourceLocator {}", am);
            return null;
        } else if (am instanceof AbstractResourceMethod) {
            OverrideInputType annotation = am.getAnnotation(OverrideInputType.class);
            if (annotation == null) {
                annotation = am.getResource().getAnnotation(OverrideInputType.class);
            }
            if (annotation != null) {
                return Lists.<ResourceFilter>newArrayList(
                    new OverrideInputTypeResourceFilter(annotation.value(), annotation.except()));
            }
        } else {
            log.warn("Got an unexpected instance of {}: {}", am.getClass().getName(), am);
        }
        return null;
    }

}

Example MyResource demonstrating its use:

@Produces(MediaType.APPLICATION_JSON)
@Path(/objects/{id}")
public class MyResource {
    @PUT
//  @Consumes(MediaType.APPLICATION_JSON)
    @OverrideInputType(MediaType.APPLICATION_JSON)
    public StatusResult put(@PathParam("id") int id, JsonObject obj) {
        log.trace("PUT {}", id);
        // do something with obj
        return new StatusResult(true);
    }

    @GET
    public JsonObject get(@PathParam("id") int id) {
        return new JsonObject(id);
    }
}

In Jersey 2, you could do this with a post-matching ContainerRequestFilters



来源:https://stackoverflow.com/questions/39048935/jersey-hardcode-post-put-objectmapper-without-needing-content-type-header

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!