Search code examples
javadynamicreflectioncastingfeign

Java8 - Object Type - Dynamically add more property


I want to get the header value from the reply of http call, store it in cache and use this value in next requests by passing it to header of the http requests in order to achieve sticky session. My project is in Java 8.

I have feign http client builder like this which decode using JacksonDecoder.

   public <T> T buildClient(Class<T> type, String url) {
            return Feign.builder()
                    .contract(new JAXRSContract())
                    .encoder(new JacksonEncoder(mapper))
                    .decoder(new CustomDecoder(mapper))
                    .logger(new Slf4jLogger(Constants.FFA_LOGGER_NAME))
                    .logLevel(Level.FULL)
                    .options(new Options(connectTimeout, readTimeout))
                    .retryer(Retryer.NEVER_RETRY)
                    .requestInterceptor(auth2FeignRequestInterceptor)
                    .invocationHandlerFactory(factory)
                    .target(type, url);
        }

The default decoder from jackson decode only body not header so I am implementing my own CustomDecoder.

What I want to achieve is get the value of the resonse.headers and map it to body or add the property in the object responseBodyObject dynamically after it gets the value from the mapper.

public final class CustomDecoder extends JacksonDecoder implements Decoder {
    @Override
      public Object decode(Response response, Type type) throws IOException {
          //Here the default decoder only decoding body.
          Reader reader = response.body().asReader();
          Object responseBodyObject = mapper.readValue(reader, mapper.constructType(type));

Solution

  • If I understand it right, this is a case of constructed type having additional properties that should be populated from header. Then, there are several approaches here. First, the one I would prefer, is having a special interface kinds that your decoder knows about, and recoded objects can implement to receive the values:

    public interface SessionTokenAware {
    
        void setSessionToken(String value);
    }
    

    And then in your decoder:

    @Override
    public Object decode(Response response, Type type) throws IOException {
        Object parsedResponse = mapper.readValue(
            response.body().asReader(),
            mapper.constructType(type)
        );
        if (parsedResponse instanceof SessionTokenAware) {
            SessionTokenAware sessionAware = (SessionTokenAware) parsedResponse;
            Collection<String> sessionHeader = response.headers().get("sessionId");
            if (sessionHeader != null && !sessionHeader.isEmpty()) {
                sessionAware.setSessionToken(sessionHeader.iterator().next());
            }
        }
        return parsedResponse;
    }
    

    When your response implements the interface, then it will get the session token value set by decoder without having to hack the body stream and adding anything to it:

    public class MyResponseBody implements SesionTokenAware {
        private String token;
    
        @Override
        public void setSessionToken(String value) {
            token = value;
        }
    
        public String getSessionToken() {
            return token;
        }
    }
    

    Altenatively, if you don't like having separate interfaces for every kind of possible headers you would want to receive, you can make a catch-all interface and let every response implementation sort out headers itself:

    public interface HeadersAware {
    
        void onHeaders(Map<String, Collection<String>> headerValues);
    }
    
    
    public Object decode(Response response, Type type) throws IOException {
        Object parsedResponse = ....
        if (parsedResponse instanceof HeadersAware) {
            HeaderAware headerAware = (HeaderAware) parsedResponse;
            headerAware.onHeaders(response.headers());
        }
        return parsedResponse;
    }
    
    public class MyResponse implements HeaderAware {
        private String token;
    
        public void onHeaders(Map<String, Collection<String>> headers) {
            Collection<String> sessionHeader = headers.getOrDefault("sessionId", emptySet());
            if (!sessionHeader.isEmpty()) {
                token = sessionHeader.iterator().next();
            }
        }
    }
    

    I don't like the latter approach for two reasons:

    1. The response may get to see values of headers it has no business seeing, because it receives all the values instead of just what's needed for implementing a particular response POJO.
    2. If you have more than one distinct hierarchy of session-aware responses (so no common superclass is possible) - you will then have to copy-paste the sessionId handling code into each of them, instead of having that code neatly located in the CustomDecoder class.