java - Custom ObjectMapper con Jersey 2.2 y Jackson 2.1
jax-rs jersey-2.0 (7)
Estoy luchando con una aplicación REST con Grizzly, Jersey y Jackson, porque Jersey ignora mi ObjectMapper personalizado.
Dependencias POM:
<dependencies>
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-grizzly2-servlet</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-json-provider</artifactId>
<version>2.1.4</version>
</dependency>
</dependencies>
Las versiones resultantes son: Grizzly 2.3.3, Jackson 2.1.4 y Jersey 2.2.
Clase principal (quiero un registro explícito de los componentes de Jersey):
public class Main {
public static void main(String[] args) {
try {
ResourceConfig rc = new ResourceConfig();
rc.register(ExampleResource.class);
rc.register(ObjectMapperResolver.class);
HttpHandler handler = ContainerFactory.createContainer(
GrizzlyHttpContainer.class, rc);
URI uri = new URI("http://0.0.0.0:8080/");
HttpServer server = GrizzlyHttpServerFactory.createHttpServer(uri);
ServerConfiguration config = server.getServerConfiguration();
config.addHttpHandler(handler, "/");
server.start();
System.in.read();
} catch (ProcessingException | URISyntaxException | IOException e) {
throw new Error("Unable to create HTTP server.", e);
}
}
}
ContextResolver para ObjectMapper:
@Provider
@Produces(MediaType.APPLICATION_JSON)
public class ObjectMapperResolver implements ContextResolver<ObjectMapper> {
private final ObjectMapper mapper;
public ObjectMapperResolver() {
System.out.println("new ObjectMapperResolver()");
mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
}
@Override
public ObjectMapper getContext(Class<?> type) {
System.out.println("ObjectMapperResolver.getContext(...)");
return mapper;
}
}
No se llama al constructor ObjectMapperResolver
ni a getContext
. ¿Qué me estoy perdiendo? Preferiría usar Jersey 2.2 y Jackson 2.1, porque es una dependencia de otra lib.
Se puede encontrar un ejemplo completo en GitHub: https://github.com/svenwltr/example-grizzly-jersey-jackson/tree/stackoverflow
Como me tomó algunas horas lograr que esto funcione con Java EE7 y Glassfish4, esta es mi solución:
public final class RequestMappingFeature implements Feature {
@Override
public boolean configure(final FeatureContext context) {
context.register(ObjectMapperProvider.class);
// If you comment out this line, it stops working.
context.register(JacksonJaxbJsonProvider.class);
return true;
}
}
Las únicas dependencias de POM relevantes son:
public final class JacksonFeature implements Feature {
private static final ObjectMapper MAPPER;
static {
// Create the new object mapper.
MAPPER = new ObjectMapper();
// Enable/disable various configuration flags.
MAPPER.configure(
DeserializationFeature.READ_ENUMS_USING_TO_STRING, true);
// ... Add your own configurations here.
}
@Override
public boolean configure(final FeatureContext context) {
JacksonJaxbJsonProvider provider = new JacksonJaxbJsonProvider(
MAPPER, DEFAULT_ANNOTATIONS);
context.register(provider);
return true;
}
}
Con Jackson 2.7, hacer esto no funcionó:
public class MyApplication extends ResourceConfig {
public MyApplication() {
register(MyObjectMapperProvider.class);
}}
Se llamó al constructor MyObjectMapperProvider, pero nunca se llamó a getContext () .
El registro de MyObjectMapperProvider en el constructor super () lo hace funcionar:
public class MyApplication extends ResourceConfig {
public MyApplication() {
super(
// register Jackson ObjectMapper resolver
MyObjectMapperProvider.class
);
}}
De Jersey 2.17 documentos: https://jersey.github.io/documentation/2.17/user-guide.html#jackson-registration
En la aplicacion
@ApplicationPath("/")
public class MyApplication extends ResourceConfig {
public MyApplication() {
register(JacksonFeature.class);
// This is the class that you supply, Call it what you want
register(JacksonObjectMapperProvider.class);
//...
}
}
Editar, se olvidó de agregar el JacksonObjectMapperProvider que proporciona en el registro (..):
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;
@Provider
public class JacksonObjectMapperProvider implements ContextResolver<ObjectMapper>{
final ObjectMapper defaultObjectMapper;
public JacksonObjectMapperProvider() {
defaultObjectMapper = createDefaultMapper();
}
@Override
public ObjectMapper getContext(Class<?> type) {return defaultObjectMapper;}
public static ObjectMapper createDefaultMapper() {
final ObjectMapper jackson = new ObjectMapper();
// any changes to the ObjectMapper is up to you. Do what you like.
// The ParameterNamesModule is optional,
// it enables you to have immutable POJOs in java8
jackson.registerModule(new ParameterNamesModule());
jackson.enable(SerializationFeature.INDENT_OUTPUT);
jackson.disable(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS);
jackson.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
return jackson;
}
}
Encontré una solución. Tuve que instanciar el proveedor de Jackson por mi cuenta y configurar mi ObjectMapper
personalizado. Un ejemplo de trabajo se puede encontrar en GitHub: https://github.com/svenwltr/example-grizzly-jersey-jackson/tree/-answer
ObjectMapperResolver
mi ObjectMapperResolver
y modifiqué mi método main
:
public class Main {
public static void main(String[] args) {
try {
// create custom ObjectMapper
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
// create JsonProvider to provide custom ObjectMapper
JacksonJaxbJsonProvider provider = new JacksonJaxbJsonProvider();
provider.setMapper(mapper);
// configure REST service
ResourceConfig rc = new ResourceConfig();
rc.register(ExampleResource.class);
rc.register(provider);
// create Grizzly instance and add handler
HttpHandler handler = ContainerFactory.createContainer(
GrizzlyHttpContainer.class, rc);
URI uri = new URI("http://0.0.0.0:8080/");
HttpServer server = GrizzlyHttpServerFactory.createHttpServer(uri);
ServerConfiguration config = server.getServerConfiguration();
config.addHttpHandler(handler, "/");
// start
server.start();
System.in.read();
} catch (ProcessingException | URISyntaxException | IOException e) {
throw new Error("Unable to create HTTP server.", e);
}
}
}
La siguiente solución se aplica a la siguiente pila (como en ... esta es la configuración que he usado para probarla)
Jersey 2.12, Jackson 2.4.x
Estoy agregando mi mensaje con la solución que he presentado en esta publicación, ya que fue bastante relevante para las muchas búsquedas de Google que he presentado hoy ... Es una solución engorrosa para lo que creo que es una problema aún más engorroso.
1. Asegúrate de que tu configuración jackson-jaxrs-json-provider
CONTIENE la jackson-jaxrs-json-provider
:
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-json-provider</artifactId>
<version>2.4.1</version>
</dependency>
2. Asegúrate de que tu configuración de administrador NO CONTIENE la dependencia de jersey-media-json-jackson
:
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
</dependency>
3. Cree un componente com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider
que com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider
manera:
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.ext.Provider;
@Provider
@Produces(MediaType.APPLICATION_JSON)
public class CustomJsonProvider extends JacksonJaxbJsonProvider {
private static ObjectMapper mapper = new ObjectMapper();
static {
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enable(SerializationFeature.INDENT_OUTPUT);
}
public CustomJsonProvider() {
super();
setMapper(mapper);
}
}
Como puede observar, también es aquí donde definimos la instancia personalizada de com.fasterxml.jackson.databind.ObjectMapper
4. Extienda javax.ws.rs.core.Feature
través de MarshallingFeature
como tal:
import javax.ws.rs.core.Feature;
import javax.ws.rs.core.FeatureContext;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
public class MarshallingFeature implements Feature {
@Override
public boolean configure(FeatureContext context) {
context.register(CustomJsonProvider.class, MessageBodyReader.class, MessageBodyWriter.class);
return true;
}
}
5. org.glassfish.jersey.server.ResourceConfig
registrar este proveedor personalizado de esa manera, siempre que configure su aplicación a través de org.glassfish.jersey.server.ResourceConfig
manera:
import org.glassfish.jersey.server.ResourceConfig;
...
public class MyApplication extends ResourceConfig {
public MyApplication() {
...
register(MarshallingFeature.class);
...
}
}
Otras notas y observaciones:
- Esta solución se aplica tanto si usa
javax.ws.rs.core.Response
para envolver las respuestas de su controlador o no. - Asegúrese de tener en cuenta cuidadosamente (copiar / pegar) los siguientes fragmentos de código, ya que los únicos bits "no obligatorios", por así decirlo, son los que se
com.fasterxml.jackson.databind.ObjectMapper
la configuración personalizada decom.fasterxml.jackson.databind.ObjectMapper
.
@jcreason
Perdón por dejar caer la pelota en este @jcreason, espero que todavía seas curioso. Así que revisé el código del año pasado y esto es lo que surgió para proporcionar un mapeador personalizado.
El problema fue que durante la inicialización de funciones cualquier mapeador de objetos personalizado se deshabilita por algún código en
org.glassfish.jersey.jackson.JacksonFeature: 77 (jersey-media-json-jackson-2.12.jar)
// Disable other JSON providers.
context.property(PropertiesHelper.getPropertyNameForRuntime(InternalProperties.JSON_FEATURE, config.getRuntimeType()), JSON_FEATURE);
Pero esta característica solo se registra por este componente
org.glassfish.jersey.jackson.internal.JacksonAutoDiscoverable
if (!context.getConfiguration().isRegistered(JacksonFeature.class)) {
context.register(JacksonFeature.class);
}
Entonces, lo que hice fue registrar mi propia característica, que registra mi propio proveedor de mapeadores de objetos y descartar un viaje que impidió que org.glassfish.jersey.jackson.JacksonFeature se registrara y anulara mi asignador de objetos ...
import com.fasterxml.jackson.jaxrs.base.JsonMappingExceptionMapper;
import com.fasterxml.jackson.jaxrs.base.JsonParseExceptionMapper;
import org.glassfish.jersey.internal.InternalProperties;
import org.glassfish.jersey.internal.util.PropertiesHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.Feature;
import javax.ws.rs.core.FeatureContext;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
public class MarshallingFeature implements Feature {
private final static String JSON_FEATURE = MarshallingFeature.class.getSimpleName();
@Override
public boolean configure(FeatureContext context) {
context.register(JsonParseExceptionMapper.class);
context.register(JsonMappingExceptionMapper.class);
context.register(JacksonJsonProviderAtRest.class, MessageBodyReader.class, MessageBodyWriter.class);
final Configuration config = context.getConfiguration();
// Disables discoverability of org.glassfish.jersey.jackson.JacksonFeature
context.property(
PropertiesHelper.getPropertyNameForRuntime(InternalProperties.JSON_FEATURE,
config.getRuntimeType()), JSON_FEATURE);
return true;
}
}
Y aquí está el proveedor de asignador de objetos personalizado ...
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.ext.Provider;
@Provider
@Produces(MediaType.APPLICATION_JSON)
public class JacksonJsonProviderAtRest extends JacksonJaxbJsonProvider {
private static ObjectMapper objectMapperAtRest = new ObjectMapper();
static {
objectMapperAtRest.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapperAtRest.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapperAtRest.configure(SerializationFeature.INDENT_OUTPUT, true); // Different from default so you can test it :)
objectMapperAtRest.setSerializationInclusion(JsonInclude.Include.ALWAYS);
}
public JacksonJsonProviderAtRest() {
super();
setMapper(objectMapperAtRest);
}
}
Me di cuenta de esto, basándome en un poco de retoques.
El problema parece estar en el mecanismo de autodetección de funciones de Jersey. Si confía en Jersey para cargar JacksonJaxbJsonProvider, entonces se ignora el proveedor de contexto personalizado para su ObjectMapper. Si, en cambio, registras la función manualmente, funciona. Presumo que esto tiene que ver con que el proveedor detectado automáticamente se cargue en un contexto de contexto diferente, pero en cuanto a una solución, aquí es a lo que terminé. Tenga en cuenta que lo envolví en una característica, debería poder registrarlo directamente con su aplicación sin ningún problema.
@javax.ws.rs.ApplicationPath("withJackson")
public class ApplicationConfig extends Application {
private static final Logger log = java.util.logging.Logger.getLogger(ApplicationConfig.class.getName());
@Override
public Set<Object> getSingletons() {
Set<Object> set = new HashSet<>();
log.log(Level.INFO, "Enabling custom Jackson JSON provider");
set.add(new JacksonJsonProvider().configure(SerializationFeature.INDENT_OUTPUT, true));
return set;
}
@Override
public Map<String, Object> getProperties() {
Map<String, Object> map = new HashMap<>();
log.log(Level.INFO, "Disabling MOXy JSON provider");
map.put("jersey.config.disableMoxyJson.server", true);
return map;
}
@Override
public Set<Class<?>> getClasses() {
Set<Class<?>> resources = new java.util.HashSet<>();
addRestResourceClasses(resources);
return resources;
}
/**
* Do not modify addRestResourceClasses() method.
* It is automatically populated with
* all resources defined in the project.
* If required, comment out calling this method in getClasses().
*/
private void addRestResourceClasses(Set<Class<?>> resources) {
resources.add(com.fasterxml.jackson.jaxrs.base.JsonMappingExceptionMapper.class);
resources.add(com.fasterxml.jackson.jaxrs.base.JsonParseExceptionMapper.class);
resources.add(com.fasterxml.jackson.jaxrs.json.JsonMappingExceptionMapper.class);
resources.add(com.fasterxml.jackson.jaxrs.json.JsonParseExceptionMapper.class);
resources.add(de.lathspell.java_test_ee7_json.Api.class);
resources.add(de.lathspell.java_test_ee7_json.with_jackson.MyExceptionMapper.class);
}
ACTUALIZACIÓN Noviembre 2017 : Las cosas han cambiado un poco en el mundo Jersey2. Si lo anterior no funciona, intente esto:
El nuevo método para proporcionar su propio ObjectMapper ahora se ve así:
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-json-provider</artifactId>
<version>2.2.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.2.3</version>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-web-api</artifactId>
<version>7.0</version>
<scope>provided</scope>
</dependency>
Por favor hacer esto:
1) añadir dependencia pom.xml
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>2.2</version>
</dependency>
2) registrar JacksonFeature en Main.java
public class Main {
public static void main(String[] args) {
try {
ResourceConfig rc = new ResourceConfig();
rc.register(ExampleResource.class);
rc.register(ObjectMapperResolver.class);
rc.register(JacksonFeature.class);
HttpHandler handler = ContainerFactory.createContainer(
GrizzlyHttpContainer.class, rc);
URI uri = new URI("http://0.0.0.0:8080/");
HttpServer server = GrizzlyHttpServerFactory.createHttpServer(uri);
ServerConfiguration config = server.getServerConfiguration();
config.addHttpHandler(handler, "/");
server.start();
System.in.read();
} catch (ProcessingException | URISyntaxException | IOException e) {
throw new Error("Unable to create HTTP server.", e);
}
}
}
3) Usa org.codehaus.jackson.map.ObjectMapper en tu recurso
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig.Feature;
@Provider
@Produces(MediaType.APPLICATION_JSON)
public class ObjectMapperResolver implements ContextResolver<ObjectMapper> {
private final ObjectMapper mapper;
public ObjectMapperResolver() {
System.out.println("new ObjectMapperResolver()");
mapper = new ObjectMapper();
mapper.enable(Feature.INDENT_OUTPUT);
}
@Override
public ObjectMapper getContext(Class<?> type) {
System.out.println("ObjectMapperResolver.getContext(...)");
return mapper;
}
}