When I created an API with the DTO class described later as the return value and performed an API test, the following exception occurred and the test did not end normally.
javax.ws.rs.ProcessingException: Error reading entity from input stream.
at org.glassfish.jersey.message.internal.InboundMessageContext.readEntity(InboundMessageContext.java:889)
at org.glassfish.jersey.message.internal.InboundMessageContext.readEntity(InboundMessageContext.java:808)
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.time.LocalDateTime` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$UnCloseableInputStream); line: 1, column: 38](through reference chain: my.controller.MyDto["localDateTime"])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1452)
It was configured with Spring Boot.
Since Dto was written in Kotlin's data class, I thought it was an exception that a new instance could not be created by the newInstance method because the constructor property is not allowed to be null, but even if I rewrite it in java as shown below, it is the same. Exception occured.
MyDto.java
package my.controller;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import java.time.LocalDateTime;
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class MyDto {
public final String word;
public final LocalDateTime localDateTime;
/**
*Argumentless constructor for Jackson to do a newInstance
*/
private MyDto() {
this(null, null);
}
public MyDto(String word, LocalDateTime localDateTime) {
this.word = word;
this.localDateTime = localDateTime;
}
public String value() {
return word + localDateTime.toString();
}
}
The resource class that takes this DTO as an argument and returns a value is as follows.
MyJerseyResource.java
package my.controller;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.time.LocalDateTime;
@Path("/my")
public class MyJerseyResource {
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public MyDto post(MyDto myDto) {
return new MyDto(
myDto.value() + " finishes!",
LocalDateTime.of(2019, 1, 1, 12, 0, 0)
);
}
@GET
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public MyDto get() {
return new MyDto(
"finish!",
LocalDateTime.of(2019, 1, 1, 12, 0, 0)
);
}
}
Also, when it was started as a Spring Boot application as usual, it was able to send and receive requests and responses normally when executed with Post Man.
I made a test class by setting this page. The get test ended with the above log, but the post test ended with a status of 400. The results will be different, but the cause of the test failure is the same as described below.
MyJerseyResourceTest.java
package my.controller;
import my.ConfigureTest;
import my.jersey.MyJerseyTest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.time.LocalDateTime;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.core.Is.*;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {ConfigureTest.class, MyJerseyTest.class})
public class MyJerseyResourceTest {
@Autowired
MyJerseyTest jerseyTest;
@Before
public void setUp() throws Exception {
this.jerseyTest.setUp();
}
@Test
Test public void post() {
Response response = jerseyTest.webTarget("/my").request()
.accept(MediaType.APPLICATION_JSON_TYPE)
.post(
Entity.json(new MyDto(
"start!",
LocalDateTime.of(2018, 1, 1, 12, 1, 1)
)
)
);
assertThat(response.getStatus(), is(200));
MyDto content = response.readEntity(MyDto.class);
assertThat(content.word, is("start!2018-01-01T12:01:01 finishes!"));
assertThat(content.localDateTime.toString(), is(LocalDateTime.of(2019, 1, 1, 12, 0, 0).toString()));
}
@Test
Test public void get() {
Response response = jerseyTest.webTarget("/my").request()
.accept(MediaType.APPLICATION_JSON_TYPE)
.get();
assertThat(response.getStatus(), is(200));
MyDto content = response.readEntity(MyDto.class);
assertThat(content.word, is("finish!"));
assertThat(content.localDateTime.toString(), is(LocalDateTime.of(2019, 1, 1, 12, 0, 0).toString()));
}
}
The cause seems to be that the ObjectMapper used by __Jersey does not support JSR310 __. Perhaps Jersey's default behavior is to choose Jackson's ObjectMapper as the Json serializer / deserializer. The configuration itself is the same as Spring MVC, but unlike the test of __Spring MVC, ObjectMapper does not support JSR310 __, so it seems that such an error occurs. To resolve this issue, I had to make some additional settings __ that apply only when running __tests.
The ObjectMapper registered in the container with Spring Boot supports JSR310, but JerseyTest doesn't seem to use it. JSR310 serializer, deserializer [JavaTimeModule](https://github.com/FasterXML/jackson-datatype-jsr310/blob/master/src/main/java/com/fasterxml/jackson/datatype/jsr310/JavaTimeModule Register via .java). In the constructor of this class, the serializer and deserializer corresponding to the JSR310 class group are registered. One thing to note is the registration of the deserializer. I just couldn't solve the JSR310 deserialization problem described below. Therefore, it was necessary to prepare a custom deserializer. This time, I prepared only LocalDateTime, but I predict that if you use other APIs such as ZonedDateTime, you need to prepare a deserializer as well.
ConfigureTest.java
package my;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Configuration
public class ConfigureTest {
@Bean
public ObjectMapper customObjectMapper(Environment environment) {
JavaTimeModule m = new JavaTimeModule();
m.addDeserializer(LocalDateTime.class, new CustomLocalDateTimeDeserializer(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
return Jackson2ObjectMapperBuilder.json()
.modulesToInstall(m)
//If you do not disable it, it will be formatted in Unix Time.
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.featuresToEnable(SerializationFeature.INDENT_OUTPUT)
.build();
}
}
I was able to convert the ObjectMapper to a bean, but as it is, this beanized ObjectMapper will not be used by Jersey. There seem to be several ways this ObjectMapper can be used, but ContextResolver To make it available.
MyJacksonConfigurer.java
package my.jersey;
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;
@Provider
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class MyJacksonConfigurator implements ContextResolver<ObjectMapper> {
private final ObjectMapper objectMapper;
public MyJacksonConfigurator(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public ObjectMapper getContext(Class<?> type) {
return this.objectMapper;
}
}
I'm not sure because there is no explanation, but from the situation, if the type specified in the type parameter of the implementation destination ContextResolver is used in Jersey, that context will be obtained via the getContext method of this class. is. Since the bean-ized ObjectMapper is injected in the constructor of this class, you can use the ObjectMapper corresponding to JSR310.
I have defined a context, but this context is not registered with Jersey. If you want to register some resources for Jersey's server-side processing, you need to register with ResourceConfig and restore the settings with JerseyTest's configure method. By registering the above ContextResolver together with the Jersey resource class, the registered ObjectMapper can be used when processing the request on the server side.
MyJerseyTest.java
@Override
protected ResourceConfig configure() {
return new ResourceConfig(MyJerseyResource.class)
.register(new MyJacksonConfigurator(objectMapper))
.property("contextConfig", applicationContext);
}
Jersey seems to make a strict distinction between server-side and client-side settings. Server-side settings are the process of handling requests. The processing on the client side is the processing for handling the response. The deserializer on the server side could be registered, but the deserializer on the client side could not be registered. This eliminates the 400 error when posting test code. However, when I try to execute Response.readEntity, it seems that ObjectMapper that does not support JSR310 is used, and the error quoted at the beginning occurs. To eliminate this error, register a ContextResolver with the client used by Jersey (in this case, the instance that makes the request and receives the response when running the test).
MyJerseyTest.java
@Override
public Client getClient() {
return JerseyClientBuilder.createClient()
.register(new MyJacksonConfigurator(objectMapper));
}
You have now registered an ObjectMapper for JSR310. The class in which JerseyTest is set is as follows.
MyJerseyTest.java
package my.jersey;
import com.fasterxml.jackson.databind.ObjectMapper;
import my.controller.MyJerseyResource;
import org.glassfish.jersey.client.JerseyClientBuilder;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.glassfish.jersey.test.JerseyTest;
import org.glassfish.jersey.test.ServletDeploymentContext;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.glassfish.jersey.test.spi.TestContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.FormContentFilter;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.filter.RequestContextFilter;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.WebTarget;
@Component
public class MyJerseyTest {
final JerseyTest jerseyTest;
boolean start = false;
public void setUp() throws Exception{
if (!start) {
this.jerseyTest.setUp();
}
start = true;
}
public WebTarget webTarget(String url) {
return this.jerseyTest.target(url);
}
public MyJerseyTest(ApplicationContext applicationContext, ObjectMapper objectMapper) {
this.jerseyTest = new JerseyTest() {
@Override
public Client getClient() {
return JerseyClientBuilder.createClient()
.register(new MyJacksonConfigurator(objectMapper));
}
@Override
protected ResourceConfig configure() {
return new ResourceConfig(MyJerseyResource.class)
.register(new MyJacksonConfigurator(objectMapper))
.property("contextConfig", applicationContext);
}
@Override
protected ServletDeploymentContext configureDeployment() {
return ServletDeploymentContext
.forServlet(new ServletContainer(configure()))
.addFilter(HiddenHttpMethodFilter.class, HiddenHttpMethodFilter.class.getSimpleName())
.addFilter(FormContentFilter.class, FormContentFilter.class.getSimpleName())
.addFilter(RequestContextFilter.class, RequestContextFilter.class.getSimpleName())
.build();
}
@Override
public TestContainerFactory getTestContainerFactory() {
return new GrizzlyWebTestContainerFactory();
}
};
}
}
As mentioned above, I registered a LocalDateTime deserializer with ObjectMapper, but this had to be customized. This is because the default deserializer cannot deserialize the strings generated by Jersey. It can be processed with the specified deserializer There may be a way, but I just couldn't find an alternative. We have adopted the following for this solution.
CustomLocalDateDeserializer.java
package my;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonTokenId;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class CustomLocalDateTimeDeserializer extends LocalDateTimeDeserializer {
public CustomLocalDateTimeDeserializer(DateTimeFormatter pattern) {
super(pattern);
}
@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
if (
p.hasTokenId(JsonTokenId.ID_STRING) ||
p.isExpectedStartArrayToken()) {
return super.deserialize(p, ctxt);
} else {
ObjectNode node = p.getCodec().readTree(p);
return LocalDateTime.of(
node.get("year").asInt(),
node.get("monthValue").asInt(),
node.get("dayOfMonth").asInt(),
node.get("hour").asInt(),
node.get("minute").asInt(),
node.get("second").asInt()
);
}
}
}
The reason for this is that when the Response.readEntity method is executed in the test class, the string of the LocalDateTime value that can be obtained by the default Jackson LocalDateTime deserializer that is executed in the innermost part is It's not the format Jackson expects. At the timing when the response object was created, the format of the LocalDateTime string was changed to the following format no matter what.
.json
{
"localDateTime": {
"dayOfYear": 1,
"dayOfWeek": "WEDNESDAY",
"month": "JANUARY",
"dayOfMonth": 1,
"year": 2018,
"monthValue": 1,
"hour": 1,
"minute": 0,
"second": 0,
"nano": 0,
"chronology": {
"id": "ISO",
"calendarType": "iso8601"
}
}
}
What do you mean? Each member object of LocalDateTime has been carefully serialized into JSON format. Instead of doing this, I should at least obediently do LocalDateTime.toString (). You'll have to deserialize yourself to do this extra thing. When deserializing, Jackson stores the character string information to be deserialized as a character string in JsonParser, the first argument of the deserialize method, but when deserializing to LocalDateTime, this format character string is stored in LocalDateTime. Does not have the ability to deserialize to. The first branch in the above code,
If a format other than either of the above comes, an exception to the quote at the beginning will occur. In this case, the string apparently starts with "{", so the character to be deserialized is considered an object and cannot be deserialized. [^ 1] If you get a format that Jackson doesn't support, you'll have to deserialize it yourself, so I had no choice but to use this implementation. I was wondering why it came in this format in the first place, so I debugged the Jersey code, but decided that it was too complicated and wasted time to decipher it, so I didn't go deeper. .. ..
Spring Boot is doing a lot of work, but it seems that some parts of Jersey are out of reach. With Spring MVC, you can even test seamlessly with Spring Boot, so this problem does not occur. You can test without thinking about anything. And JAX-RS seems to force you to understand the details of the specification before using it. The time available for us is limited. If you spend your time remembering something that can't be done, you want to spend your time doing other things. Originally, the framework should do its best to reduce such time, but it is also a good point to fall over. After all, when I chose Spring Boot, I realized that there was no merit in choosing Jersey (JAX-RS). ~~ In the first place, the benefits of building a web application other than Spring Boot in Java are not clear at this stage, so it seems that there is no need to use JAX-RS. ~~
I found two methods other than using the Beanized ObjectMapper with ContextResolver, so I will write them down. In both cases, it is necessary to register the instance on the server side and the client side.
By default, ObjectMapper is used for serializing and deserializing Json in Jersey, but it seems that this behavior is achieved by using a class called JacksonJaxbJsonProvider. Therefore, override this class.
CustomJacksonJsonProvider.java
package my;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider;
import javax.annotation.Priority;
import javax.ws.rs.ext.Provider;
@Provider
@Priority(0)
public class CustomJacksonJsonProvider extends JacksonJaxbJsonProvider {
public CustomJacksonJsonProvider(ObjectMapper objectMapper) {
super();
super.setMapper(objectMapper);
}
}
The body content that comes into the Jersey response seems to go into the implementation of the title class. It seems to read and write the body by solving this class, and you can hook serialization and deserialization here. However, this method is not recommended at all because it requires extra implementation, is complicated, and it is unclear whether it is really practical in the first place. For reference only.
MyMessageBody.java
package my;
import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
@Provider
@Produces(value ={MediaType.APPLICATION_JSON})
@Consumes(value ={MediaType.APPLICATION_JSON})
public class MyMessageBody<T extends Serializable> implements MessageBodyReader<T>, MessageBodyWriter<T> {
@Override
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return true;
}
@Override
public T readFrom(Class<T> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException, WebApplicationException {
//Content to serialize
return null;
}
@Override
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return true;
}
@Override
public void writeTo(T t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException {
//Content to deserialize
}
}
[^ 1]: As an aside, JsonParser has the information to deserialize itself as a string. If the string that JsonParse has does not start with "{", "[", it is a string (ID_STRING), if it starts with "{", it is an object (START_OBJECT), if it starts with "[", it is an array ( It seems to be judged as START_ARRAY).
Recommended Posts