au.net.causal.shoelaces.jersey.ShoelacesJerseyAutoConfiguration Maven / Gradle / Ivy
package au.net.causal.shoelaces.jersey;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
import org.glassfish.hk2.api.Rank;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.ClassUtils;
import javax.ws.rs.client.Client;
import javax.ws.rs.ext.ContextResolver;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Shoelaces custom configuration for Jersey that allows customization of the Jackson object mapper used by Jersey
* through the use of {@link JerseyObjectMapperConfigurer}s.
*/
@AutoConfiguration
@ConditionalOnClass(Client.class)
public class ShoelacesJerseyAutoConfiguration
{
//Mostly a copy from JerseyAutoConfiguration but with changes to support our own object mapper flavor
/**
* The Shoelaces custom context resolver is registered on the Jersey resource config with this priority to ensure
* that it gets priority over the default Spring Boot Jersey one.
*
*
* For user code or other libraries that want to use their own object mapper context resolver on the Jersey resource
* config, use a priority higher than this value when registering your own context resolver.
*/
public static final int RESOURCE_CONFIG_CUSTOM_OBJECT_MAPPER_CONTEXT_RESOLVER_PRIORITY = 100;
@ConditionalOnClass(JacksonFeature.class)
@Configuration
static class JacksonResourceConfigCustomizer
{
private static final String JAXB_ANNOTATION_INTROSPECTOR_CLASS_NAME = "com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector";
/**
* Take the 'primary' object mapper bean from Spring Boot, copy it, and configure it with any available Jersey
* object mapper configurer beans.
*
* @param objectMapperConfigurers all Jersey object mapper configurers available to Spring.
* @param primaryObjectMapper the standard object mapper bean in Spring boot that is used for everything.
*
* @return a new object mapper to use specifically for Jersey, copied from the primary and configured with the
* configurers.
*/
@Bean
@JerseyConfigured
public ObjectMapper customJerseyObjectMapper(Optional> objectMapperConfigurers,
ObjectMapper primaryObjectMapper)
{
//Make a copy of the object mapper in case it's used in other things besides Jersey
//The 'primary' object mapper being injected here is the default object mapper injected by Spring Boot
//for *anything* that wants a Jackson ObjectMapper, be it Jersey or something else
//By using a copy we configure Jersey's object mapper with custom stuff without affecting everything else
ObjectMapper objectMapper = primaryObjectMapper.copy();
objectMapperConfigurers.orElse(List.of()).forEach(configurer -> configurer.configure(objectMapper));
return objectMapper;
}
/**
* Customize the Jersey resource config by configuring our own custom object mapper into it instead of using
* the default one. The custom object mapper will have been configured by any registered Jersey object mapper
* configurer beans.
*
* @param objectMapper our custom object mapper.
*
* @return a Jersey resource config customizer that makes Jersey use our custom object mapper instead of the
* default one.
*/
@Bean
public ResourceConfigCustomizer shoelacesResourceConfigCustomizer(@JerseyConfigured ObjectMapper objectMapper)
{
addJaxbAnnotationIntrospectorIfPresent(objectMapper);
return (ResourceConfig config) ->
{
config.register(JacksonFeature.class);
//The map ensures our resolver is registered with higher priority than the default one that is
//created in JerseyAutoConfiguration - this is important since we want Jersey to always use our one
//instead of Spring Boot's default one
config.register(new ObjectMapperContextResolver(objectMapper),
Map.of(ContextResolver.class, RESOURCE_CONFIG_CUSTOM_OBJECT_MAPPER_CONTEXT_RESOLVER_PRIORITY));
};
}
private void addJaxbAnnotationIntrospectorIfPresent(ObjectMapper objectMapper)
{
if (ClassUtils.isPresent(JAXB_ANNOTATION_INTROSPECTOR_CLASS_NAME, getClass().getClassLoader()))
new ObjectMapperCustomizer().addJaxbAnnotationIntrospector(objectMapper);
}
}
private static final class ObjectMapperCustomizer
{
private void addJaxbAnnotationIntrospector(ObjectMapper objectMapper)
{
JaxbAnnotationIntrospector jaxbAnnotationIntrospector = new JaxbAnnotationIntrospector(objectMapper.getTypeFactory());
objectMapper.setAnnotationIntrospectors(createPair(objectMapper.getSerializationConfig(), jaxbAnnotationIntrospector),
createPair(objectMapper.getDeserializationConfig(), jaxbAnnotationIntrospector));
}
private AnnotationIntrospector createPair(MapperConfig> config, JaxbAnnotationIntrospector jaxbAnnotationIntrospector)
{
return AnnotationIntrospector.pair(config.getAnnotationIntrospector(), jaxbAnnotationIntrospector);
}
}
//The @Rank is important to ensure that this object mapper is used before the default one from Spring Boot's JerseyAutoConfiguration
//otherwise order is arbitrary and you'll get random object mappers being resolved each time the app is run
@Rank(RESOURCE_CONFIG_CUSTOM_OBJECT_MAPPER_CONTEXT_RESOLVER_PRIORITY)
private static class ObjectMapperContextResolver implements ContextResolver
{
private final ObjectMapper objectMapper;
private ObjectMapperContextResolver(ObjectMapper objectMapper)
{
this.objectMapper = objectMapper;
}
@Override
public ObjectMapper getContext(Class> type)
{
return this.objectMapper;
}
}
}