Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow specifying additional modules in @JacksonFeatures #6525

Merged
merged 2 commits into from
Nov 17, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Allow specifying additional modules in @JacksonFeatures
This patch adds an `additionalModules` property to the `@JacksonFeatures` annotation that allows the user to specify custom modules. These modules can then be used for further customization of the `ObjectMapper`.
`ClientScopeSpec` has also been changed to do proper random port binding and is enabled for github actions again.
Resolves #5233
  • Loading branch information
yawkat committed Nov 16, 2021
commit 02ecd73afa6fde56fc0f937646de0e3c84ab56da
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
package io.micronaut.http.client

import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.databind.module.SimpleModule
import io.micronaut.context.ApplicationContext
import io.micronaut.context.annotation.ConfigurationProperties
import io.micronaut.context.annotation.Requires
Expand All @@ -40,8 +43,6 @@ import spock.lang.Specification
* @author Graeme Rocher
* @since 1.0
*/
@Retry(mode = Retry.Mode.SETUP_FEATURE_CLEANUP)
@IgnoreIf({env["GITHUB_WORKFLOW"]})
class ClientScopeSpec extends Specification {

@Retry
Expand All @@ -50,14 +51,23 @@ class ClientScopeSpec extends Specification {
EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, [
'spec.name': 'ClientScopeSpec',
'from.config': '/',
'micronaut.server.port':'${random.port}',
'micronaut.http.services.my-service.url': 'http://localhost:${micronaut.server.port}',
'micronaut.http.services.my-service-declared.url': 'http://my-service-declared:${micronaut.server.port}',
'micronaut.http.services.my-service-declared.path': "/my-declarative-client-path",
'micronaut.http.services.other-service.url': 'http://localhost:${micronaut.server.port}',
'micronaut.http.services.other-service.path': "/scope"])
'micronaut.server.port': -1])
embeddedServer.start()

def applicationContext = embeddedServer.applicationContext
def applicationContext = ApplicationContext.run([
'spec.name': 'ClientScopeSpec',
'from.config': '/',
'micronaut.server.port': -1,
'micronaut.http.client.url': embeddedServer.URI,
'micronaut.http.services.my-service.url': embeddedServer.URI,
'micronaut.http.services.my-service-declared.url': embeddedServer.URI,
'micronaut.http.services.my-service-declared.path': "/my-declarative-client-path",
'micronaut.http.services.other-service.url': embeddedServer.URI,
'micronaut.http.services.other-service.path': "/scope",
'micronaut.http.services.other-service-2.url': embeddedServer.URI,
'micronaut.http.services.other-service-2.path': "/scope"])
def embeddedServer2 = applicationContext.getBean(EmbeddedServer)
embeddedServer2.start()
MyService myService = applicationContext.getBean(MyService)

when:
Expand Down Expand Up @@ -96,16 +106,22 @@ class ClientScopeSpec extends Specification {
client.name()

then:
thrown(HttpClientException)
//thrown(HttpClientException)
Flux.from(((DefaultHttpClient) myJavaService.client)
.resolveRequestURI(HttpRequest.GET("/foo"))).blockFirst().toString() == "http://localhost:${embeddedServer.port}/foo"
.resolveRequestURI(HttpRequest.GET("/foo"))).blockFirst().toString() == "http://localhost:${embeddedServer2.port}/foo"

when:"test service definition with declarative client with jackson features"
MyServiceJacksonFeatures jacksonFeatures = applicationContext.getBean(MyServiceJacksonFeatures)

then:
jacksonFeatures.name() == "success"

when:"test service definition with declarative client with jackson features: additional module"
MyServiceJacksonModule jacksonFeatures2 = applicationContext.getBean(MyServiceJacksonModule)

then:
jacksonFeatures2.bean().getFooBar() == "baz"

when:"test no base path with the declarative client"
NoBasePathService noBasePathService = applicationContext.getBean(NoBasePathService)

Expand Down Expand Up @@ -156,6 +172,7 @@ class ClientScopeSpec extends Specification {

cleanup:
embeddedServer.close()
embeddedServer2.close()
}

@Controller('/scope')
Expand All @@ -164,6 +181,11 @@ class ClientScopeSpec extends Specification {
String index() {
return "success"
}

@Get(produces = MediaType.APPLICATION_JSON, value = '/json')
String json() {
return '{"foo_bar":"baz"}'
}
}

@Singleton
Expand Down Expand Up @@ -310,6 +332,35 @@ class ClientScopeSpec extends Specification {
String name()
}

@Requires(property = 'spec.name', value = "ClientScopeSpec")
@Client(id = 'other-service-2')
@JacksonFeatures(additionalModules = [CustomizingModule])
static interface MyServiceJacksonModule {

@Get(consumes = MediaType.APPLICATION_JSON, value = '/json')
Bean bean()
}

static class CustomizingModule extends SimpleModule {
@Override
void setupModule(SetupContext context) {
super.setupModule(context)
((ObjectMapper) context.getOwner()).propertyNamingStrategy = PropertyNamingStrategies.SNAKE_CASE
}
}

static class Bean {
private String fooBar;

String getFooBar() {
return fooBar
}

void setFooBar(String fooBar) {
this.fooBar = fooBar
}
}

@Requires(property = 'spec.name', value = "ClientScopeSpec")
@Client
static interface NoBasePathService {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,9 @@
* @return The disabled serialization features
*/
DeserializationFeature[] disabledDeserializationFeatures() default {};

/**
* @return Additional modules to add to the jackson mapper
*/
Class<? extends com.fasterxml.jackson.databind.Module>[] additionalModules() default {};
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@
package io.micronaut.jackson.codec;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.SerializationFeature;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.Internal;
import io.micronaut.json.JsonFeatures;

import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

Expand All @@ -37,13 +40,15 @@ public final class JacksonFeatures implements JsonFeatures {

private final Map<SerializationFeature, Boolean> serializationFeatures;
private final Map<DeserializationFeature, Boolean> deserializationFeatures;
private final List<Class<? extends Module>> additionalModules;

/**
* Empty jackson features.
*/
public JacksonFeatures() {
this.serializationFeatures = new EnumMap<>(SerializationFeature.class);
this.deserializationFeatures = new EnumMap<>(DeserializationFeature.class);
this.additionalModules = new ArrayList<>();
}

public static JacksonFeatures fromAnnotation(AnnotationValue<io.micronaut.jackson.annotation.JacksonFeatures> jacksonFeaturesAnn) {
Expand Down Expand Up @@ -80,6 +85,14 @@ public static JacksonFeatures fromAnnotation(AnnotationValue<io.micronaut.jackso
}
}

@SuppressWarnings("unchecked")
Class<? extends Module>[] additionalModules = jacksonFeaturesAnn.get("additionalModules", Class[].class).orElse(null);
yawkat marked this conversation as resolved.
Show resolved Hide resolved
if (additionalModules != null) {
yawkat marked this conversation as resolved.
Show resolved Hide resolved
for (Class<? extends Module> additionalModule : additionalModules) {
jacksonFeatures.addModule(additionalModule);
}
}

return jacksonFeatures;
}

Expand Down Expand Up @@ -107,6 +120,18 @@ public JacksonFeatures addFeature(DeserializationFeature deserializationFeature,
return this;
}


/**
* Add a jackson module feature.
*
* @param moduleClass The module to load
* @return This object.
*/
public JacksonFeatures addModule(Class<? extends Module> moduleClass) {
yawkat marked this conversation as resolved.
Show resolved Hide resolved
additionalModules.add(moduleClass);
return this;
}

/**
* Serialization features.
*
Expand All @@ -125,6 +150,15 @@ public Map<DeserializationFeature, Boolean> getDeserializationFeatures() {
return this.deserializationFeatures;
}

/**
* Additional modules to load.
*
* @return List of additional modules to load.
*/
public List<Class<? extends Module>> getAdditionalModules() {
yawkat marked this conversation as resolved.
Show resolved Hide resolved
return additionalModules;
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.micronaut.context.annotation.BootstrapContextCompatible;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.beans.BeanIntrospection;
import io.micronaut.core.beans.BeanIntrospector;
import io.micronaut.core.type.Argument;
import io.micronaut.jackson.JacksonConfiguration;
import io.micronaut.jackson.ObjectMapperFactory;
Expand All @@ -44,6 +47,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.Optional;
import java.util.function.Consumer;

Expand Down Expand Up @@ -133,6 +137,20 @@ public void updateValueFromTree(Object value, @NonNull JsonNode tree) throws IOE
ObjectMapper objectMapper = this.objectMapper.copy();
jacksonFeatures.getDeserializationFeatures().forEach(objectMapper::configure);
jacksonFeatures.getSerializationFeatures().forEach(objectMapper::configure);
for (Class<? extends Module> moduleClass : jacksonFeatures.getAdditionalModules()) {
Optional<? extends BeanIntrospection<? extends Module>> introspection = BeanIntrospector.SHARED.findIntrospection(moduleClass);
Module module;
if (introspection.isPresent()) {
module = introspection.get().instantiate();
} else {
try {
module = moduleClass.getConstructor().newInstance();
} catch (Exception e) {
throw new IllegalArgumentException("Failed to instantiate configured additional module " + moduleClass.getName());
}
}
yawkat marked this conversation as resolved.
Show resolved Hide resolved
objectMapper.registerModule(module);
}

return new JacksonDatabindMapper(objectMapper);
}
Expand Down