-
Notifications
You must be signed in to change notification settings - Fork 40.7k
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
CodecsAutoConfiguration may overwrite existing customizations of the default codes #25152
Comments
I've taken a look at implementing this and I don't understand the benefit for a couple of reasons. With the change in Framework 5.3.4 it's possible to build on Boot's configuration of the encoder and decoder without any changes in Boot. Boot sets the Jackson 2 JSON encoder and decoder in a The javadoc for |
Thanks for taking a look. First one clarification. The I see your point that Spring HATEOAS can register its own CodecCustomizer at a lower priority. If I recall correctly from discussing with @odrotbohm, the challenge was that the customization is done with a I thought of this as a relatively straight-forward change when I suggested it but maybe I'm not seeing something. Boot currently relies on the |
The problem with our previous approach (a custom converter / codec ordered before the default ones) was that the default one would end up opting in rendering Thus, the HATEOAS components registering the support for the hypermedia types still consume a potentially Boot configured I might be missing a part of the story but isn't what's suggested to exchange the
Anything downstream of Spring Boot that is, yes.
Spring HATEOAS unfortunately can't as the interface is part of Boot, not Spring Framework. |
Thanks, Rossen. That goes a long way to addressing my concerns. I've just re-read the javadoc and managed not to skip over "for the given class" this time. Sorry for the noise there.
I think the order is undefined at the moment. Boot's
Yes. The problem is that, theoretically at least, there could be someone relying upon the complete replacement. I do think it's quite unlikely, and could probably be worked around with some changes to their app, but, as with #25302 that I've just opened, we need to be a little bit wary of breaking something in a maintenance release. If we fix #25302, we may not need to do anything here as Boot's |
We've only rolled those changes into the upcoming 1.3 of Spring HATEOAS, i.e. I think it'd be fine to only tweak this for Boot 2.5. |
I've taken another look at this and have been trying to write a test that checks that existing customizations aren't overwritten. I've failed to do so because Unfortunately, that leaves me back to where I was earlier not understanding the need for this change. What have I missed this time please, @rstoyanchev? FWIW, I'm also struggling to understand the motivation for the codec customization being as complex as it is. I suspect there are some use cases of which I am unaware, but it does seem quite complicated even for something that felt like it should be reasonably simple. |
Apologies for the slow reply. For how to test, I imagine something like this: List<HttpMessageReader<?>> readers = codecConfigurer.getReaders();
List<HttpMessageWriter<?>> writers = codecConfigurer.getWriters();
// Inspect codecs before...
codecConfigurer.defaultCodecs().configureDefaultCodec(codec -> {
// customize...
});
readers = codecConfigurer.getReaders();
writers = codecConfigurer.getWriters();
// Inspect codecs after... For the second question on why replace the Jackson encoder and decoder instances vs just set the I suppose if it becomes possible to order |
Thanks, Rossen. I don't think I did a very good job of explaining the problem. The crux of it is that it doesn't seem to matter what Boot does as the end result is always the same. Here are four tests that hopefully illustrate this: import java.util.List;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.http.codec.CodecConfigurer;
import org.springframework.http.codec.DecoderHttpMessageReader;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.json.Jackson2CodecSupport;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.codec.support.DefaultServerCodecConfigurer;
import org.springframework.util.MimeType;
import static org.assertj.core.api.Assertions.assertThat;
class CodecCustomizationTests {
private final ObjectMapper bootObjectMapper = new ObjectMapper();
private final ObjectMapper hateoasObjectMapper = new ObjectMapper();
private final CodecConfigurer codecConfigurer = new DefaultServerCodecConfigurer();
@Test
void hateoasCustomizesThenBootReplaces() {
hateoasCustomization(this.codecConfigurer);
bootReplacement(this.codecConfigurer);
verifyJacksonCodecs(this.codecConfigurer);
}
@Test
void bootReplacesThenHateoasCustomizes() {
bootReplacement(this.codecConfigurer);
hateoasCustomization(this.codecConfigurer);
verifyJacksonCodecs(this.codecConfigurer);
}
@Test
void hateoasCustomizesThenBootCustomizes() {
hateoasCustomization(this.codecConfigurer);
bootCustomization(this.codecConfigurer);
verifyJacksonCodecs(this.codecConfigurer);
}
@Test
void bootCustomizesThenHateoasCustomizes() {
bootCustomization(this.codecConfigurer);
hateoasCustomization(this.codecConfigurer);
verifyJacksonCodecs(this.codecConfigurer);
}
private void bootReplacement(CodecConfigurer codecConfigurer) {
codecConfigurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(this.bootObjectMapper));
codecConfigurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(this.bootObjectMapper));
}
private void bootCustomization(CodecConfigurer codecConfigurer) {
codecConfigurer.defaultCodecs().configureDefaultCodec((codec) -> {
if (codec instanceof Jackson2JsonDecoder || codec instanceof Jackson2JsonEncoder) {
((Jackson2CodecSupport) codec).setObjectMapper(this.bootObjectMapper);
}
});
}
private void hateoasCustomization(CodecConfigurer codecConfigurer) {
codecConfigurer.defaultCodecs().configureDefaultCodec((codec) -> {
if (codec instanceof Jackson2JsonDecoder || codec instanceof Jackson2JsonEncoder) {
((Jackson2CodecSupport) codec).registerObjectMappersForType(CustomType.class, (registrar) -> {
registrar.put(MimeType.valueOf("application/custom+json"), this.hateoasObjectMapper);
});
}
});
}
private void verifyJacksonCodecs(CodecConfigurer codecConfigurer) {
List<Jackson2CodecSupport> readerDecoders = codecConfigurer.getReaders().stream()
.filter(DecoderHttpMessageReader.class::isInstance).map(DecoderHttpMessageReader.class::cast)
.map(DecoderHttpMessageReader::getDecoder).filter(Jackson2JsonDecoder.class::isInstance)
.map(Jackson2CodecSupport.class::cast).collect(Collectors.toList());
verifyJacksonCodecs(readerDecoders);
List<Jackson2CodecSupport> writerEncoders = codecConfigurer.getWriters().stream()
.filter(EncoderHttpMessageWriter.class::isInstance).map(EncoderHttpMessageWriter.class::cast)
.map(EncoderHttpMessageWriter::getEncoder).filter(Jackson2JsonEncoder.class::isInstance)
.map(Jackson2CodecSupport.class::cast).collect(Collectors.toList());
verifyJacksonCodecs(writerEncoders);
}
private void verifyJacksonCodecs(List<Jackson2CodecSupport> jacksonCodecs) {
assertThat(jacksonCodecs).isNotEmpty();
assertThat(jacksonCodecs).allSatisfy((codec) -> {
assertThat(codec.getObjectMappersForType(CustomType.class).get(MimeType.valueOf("application/custom+json")))
.isEqualTo(this.hateoasObjectMapper);
assertThat(codec.getObjectMapper()).isEqualTo(this.bootObjectMapper);
});
}
static class CustomType {
}
} As far as I can tell, it makes no difference if Boot goes before or after HATEOAS or if it customises the existing codec's As I said above, I may well have missed something as I'm struggling to understand the need for the various different ways of doings things. Perhaps there's a subtlety here that the tests above don't cover. |
I see now what you meant. On closer look, indeed the intention is that the order in which a default codec might be replaced and when some other default codec setting might change, doesn't matter. The In other words, you're right that as long as HATEOAS uses this callback it is guaranteed to be applied. That makes this a non-issue at least as defined, in the context of HATEOAS. Given this, Boot can still choose to use the callback and only update the |
As things stand, I'm not sure that it's worth making this change. As demonstrated above, even if Boot only updates the |
Prior to this commit, there was no easy way to restrict what types could be loaded from a YAML document in subclasses of YamlProcessor such as YamlPropertiesFactoryBean and YamlMapFactoryBean. This commit introduces a setSupportedTypes(Class<?>...) method in YamlProcessor in order to address this. If no supported types are configured, all types encountered in YAML documents will be supported. If an unsupported type is encountered, an IllegalStateException will be thrown when the corresponding YAML node is processed. Closes spring-projectsgh-25152
Currently
CodecsAutoConfiguration
replaces Jackson codecs even though all it means to do is set theObjectMapper
but until now that's all the underlying API allowed. In Spring Framework 5.3.4 it will be possible to set theObjectMapper
without replacing the Jackson codec instances andCodecsAutoConfiguration
should take advantage of that.This will help with the changes for spring-projects/spring-framework#26212 after which Spring HATEOAS will be able to register additional
ObjectMapper
instances on the same default Jackson codecs for its ownRepresentationalModel
class without interfering with Boot trying to set the defaultObjectMapper
.The text was updated successfully, but these errors were encountered: