2016-06-15 9 views
10

Ich muss einen benutzerdefinierten Jackson Deserializer für java.lang.String zu meiner Spring 4.1.x MVC-Anwendung hinzufügen. Alle Antworten (z. B. this) beziehen sich jedoch auf die Konfiguration des ObjectMappers für die gesamte Webanwendung, und die Änderungen gelten für alle Strings in allen @RequestBody auf allen Controllern.Können Sie Spring Controller spezifische Deserialisierung von Jackson konfigurieren?

Ich möchte die benutzerdefinierte Deserialisierung nur auf @RequestBody-Argumente anwenden, die in bestimmten Controllern verwendet werden. Beachten Sie, dass ich keine @ JsonDeserialize-Annotationen für die spezifischen String-Felder verwenden kann.

Können Sie benutzerdefinierte Deserialisierung nur für bestimmte Controller konfigurieren?

+0

Was ist mit dem Schreiben eines Objekt Mappers? Ich denke, dass Sie die Deserialisierungslogik hinzufügen können, die Sie darin benötigen. – reos

+0

Das Problem erstellt keinen Objekt-Mapper. Meine Frage ist, wie kann ich einen Object Mapper auf einer Pro-Controller-Basis statt global in der Web-Anwendung konfigurieren. – Mark

+0

Ich verstehe Ihre Frage, ich schlage vor, eine Objekt-Mapper zu schreiben, die in allen Controllern verwendet werden würde, aber ich kann das Objekt deserialize abhängig von der Anforderung, die es empfängt. – reos

Antwort

-2

Sie können einen POJO für jeden anderen Anforderungstyp definieren, den Sie deserialisieren möchten. Anschließend wird der folgende Code die Werte aus dem JSON in das von Ihnen definierte Objekt übernehmen, vorausgesetzt, die Namen der Felder in Ihrem POJO stimmen mit den Namen des Feldes in der JSON-Anforderung überein.

ObjectMapper mapper = new ObjectMapper(); 
YourPojo requestParams = null; 

try { 
    requestParams = mapper.readValue(JsonBody, YourPOJO.class); 

} catch (IOException e) { 
    throw new IOException(e); 
} 
+1

Die POJOs existieren bereits. Ich möchte die Dererialisierung bestimmter Felder anpassen, aber auf einer Controller-spezifischen Basis und ohne die POJOs zu berühren. – Mark

0

Wahrscheinlich würde dies helfen, aber es ist nicht schön. Es würde AOP erfordern. Auch ich habe es nicht bestätigt. Erstellen Sie eine @CustomAnnotation.

Aktualisieren Sie Ihre Controller:

void someEndpoint(@RequestBody @CustomAnnotation SomeEntity someEntity); 

Dann implemment das AOP Teil:

@Around("execution(* *(@CustomAnnotation (*)))") 
public void advice(ProceedingJoinPoint proceedingJoinPoint) { 
    // Here you would add custom ObjectMapper, I don't know another way around it 
    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); 
    String body = request .getReader().lines().collect(Collectors.joining(System.lineSeparator())); 

    SomeEntity someEntity = /* deserialize */; 
    // This could be cleaner, cause the method can accept multiple parameters 
    proceedingJoinPoint.proceed(new Object[] {someEntity}); 
} 
+0

Wenn ich mich nicht irre, wird sich dieser Ansatz wie folgt verhalten: Spring verwendet den vorhandenen Konverter, um den Anfragetext zu lesen, und erst dann wird dieser Ratschlag fallen und das Ergebnis und die Leseanfrage wieder fallen lassen. Neben der Tatsache, dass die Anfrage zweimal konvertiert wird, hat dies zwei Nebeneffekte: Sie werden wahrscheinlich 'IllegalStateException' auf' getReader' erhalten, weil 'getInputStream' bereits aufgerufen wurde (fixierbar) und Sie' HttpMessageNotReadableException' erhalten, wenn der existierende Konverter nicht in der Lage war Anfrage lesen – chimmi

0

Sie könnten Message Converters versuchen. Sie haben einen Kontext über HTTP-Eingabeanforderung (z. B. Dokumente siehe here, JSON). Wie Sie anpassen können, können Sie here sehen. Idee, dass Sie HttpInputMessage mit speziellen URIs überprüfen können, die in Ihren Controllern verwendet werden und Zeichenfolge konvertieren, wie Sie möchten. Sie könnten dafür spezielle Annotationen erstellen, Pakete scannen und automatisch machen.

Hinweis

Wahrscheinlich brauchen Sie keine Implementierung von ObjectMappers. Sie können den einfachen Standard-ObjectMapper verwenden, um String zu parsen und dann die Zeichenfolge wie gewünscht zu konvertieren. In diesem Fall würden Sie RequestBody einmal erstellen.

+0

Was ist ein 'MessageController'? – chimmi

+0

Danke, es ist Tippfehler, korrigierte ich. – egorlitvinenko

4

Um verschiedene Deserialisierungskonfigurationen zu haben, müssen Sie verschiedene ObjectMapper Instanzen haben, aber Spring verwendet MappingJackson2HttpMessageConverter, die nur eine Instanz verwenden soll.

Ich sehe zumindest zwei Möglichkeiten:

Umzug von MessageConverter weg zu einem ArgumentResolver

erstellen @CustomRequestBody Anmerkung und ein Argument Resolver:

public class CustomRequestBodyArgumentResolver implements HandlerMethodArgumentResolver { 

    private final ObjectMapperResolver objectMapperResolver; 

    public CustomRequestBodyArgumentResolver(ObjectMapperResolver objectMapperResolver) { 
    this.objectMapperResolver = objectMapperResolver; 
    } 

    @Override 
    public boolean supportsParameter(MethodParameter methodParameter) { 
    return methodParameter.getParameterAnnotation(CustomRequestBody.class) != null; 
    } 

    @Override 
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { 
    if (this.supportsParameter(methodParameter)) { 
     ObjectMapper objectMapper = objectMapperResolver.getObjectMapper(); 
     HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); 
     return objectMapper.readValue(request.getInputStream(), methodParameter.getParameterType()); 
    } else { 
     return WebArgumentResolver.UNRESOLVED; 
    } 
    } 
} 

ObjectMapperResolver ist eine Schnittstelle Wir werden verwenden, um tatsächliche ObjectMapper Instanz zu lösen, um zu verwenden, ich werde es unten diskutieren. Wenn Sie nur einen Anwendungsfall haben, in dem Sie ein benutzerdefiniertes Mapping benötigen, können Sie Ihren Mapper hier einfach initialisieren.

@Configuration 
public class WebConfiguration extends WebMvcConfigurerAdapter { 

    @Bean 
    public CustomRequestBodyArgumentResolver customBodyArgumentResolver(ObjectMapperResolver objectMapperResolver) { 
    return new CustomRequestBodyArgumentResolver(objectMapperResolver) 
    } 

    @Override 
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {  
    argumentResolvers.add(customBodyArgumentResolver(objectMapperResolver())); 
    } 
} 

Hinweis:

Sie können benutzerdefinierte Argument Resolver mit dieser Konfiguration hinzufügen nicht @CustomRequestBody mit @RequestBody kombinieren, werden sie ignoriert.

Wrap ObjectMapper in einem Proxy, der mehrere Instanzen

MappingJackson2HttpMessageConverter wurde entwickelt, um die Arbeit mit nur einer Instanz von ObjectMapper versteckt. Wir können diese Instanz zu einem Proxy-Delegaten machen. Dies macht das Arbeiten mit mehreren Mappern transparent.

Zuerst brauchen wir einen Interzeptor, der alle Methodenaufrufe in ein zugrunde liegendes Objekt übersetzt.

public abstract class ObjectMapperInterceptor implements MethodInterceptor { 

    @Override 
    public Object invoke(MethodInvocation invocation) throws Throwable { 
    return ReflectionUtils.invokeMethod(invocation.getMethod(), getObject(), invocation.getArguments()); 
    } 

    protected abstract ObjectMapper getObject(); 

} 

Jetzt ist unser ObjectMapper Proxy-Bean wird wie folgt aussehen:

@Bean 
public ObjectMapper objectMapper(ObjectMapperResolver objectMapperResolver) { 
    ProxyFactory factory = new ProxyFactory(); 
    factory.setTargetClass(ObjectMapper.class); 
    factory.addAdvice(new ObjectMapperInterceptor() { 

     @Override 
     protected ObjectMapper getObject() { 
     return objectMapperResolver.getObjectMapper(); 
     } 

    }); 

    return (ObjectMapper) factory.getProxy(); 
} 

Hinweis: ich mit diesem Proxy auf Wildfly Klasse Ladeprobleme hatte, aufgrund seiner modularen Klasse Laden, so musste ich extend ObjectMapper (ohne etwas zu ändern) nur so kann ich Klasse von meinem Modul verwenden.

sie alle zusammen mit dieser Konfiguration gebunden:

@Configuration 
public class WebConfiguration extends WebMvcConfigurerAdapter { 

    @Bean 
    public MappingJackson2HttpMessageConverter jackson2HttpMessageConverter() { 
    return new MappingJackson2HttpMessageConverter(objectMapper(objectMapperResolver())); 
    } 

    @Override 
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { 
    converters.add(jackson2HttpMessageConverter()); 
    } 
} 

ObjectMapperResolver Implementierungen

Abschließendes Stück ist die Logik, die die Mapper bestimmt verwendet werden soll, wird es in ObjectMapperResolver Schnittstelle enthalten sein. Es enthält nur ein Look-Methode:

public interface ObjectMapperResolver { 

    ObjectMapper getObjectMapper(); 

} 

Wenn Sie können Sie einfach eine Karte von vorkonfigurierten Instanzen mit ReqeustMatcher s als Schlüssel machen nicht viele Anwendungsfälle mit benutzerdefinierten Mapper haben. Etwas wie folgt aus:

public class RequestMatcherObjectMapperResolver implements ObjectMapperResolver { 

    private final ObjectMapper defaultMapper; 
    private final Map<RequestMatcher, ObjectMapper> mapping = new HashMap<>(); 

    public RequestMatcherObjectMapperResolver(ObjectMapper defaultMapper, Map<RequestMatcher, ObjectMapper> mapping) { 
    this.defaultMapper = defaultMapper; 
    this.mapping.putAll(mapping); 
    } 

    public RequestMatcherObjectMapperResolver(ObjectMapper defaultMapper) { 
    this.defaultMapper = defaultMapper; 
    } 

    @Override 
    public ObjectMapper getObjectMapper() { 
    ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); 
    HttpServletRequest request = sra.getRequest(); 
    for (Map.Entry<RequestMatcher, ObjectMapper> entry : mapping.entrySet()) { 
     if (entry.getKey().matches(request)) { 
     return entry.getValue(); 
     } 
    } 
    return defaultMapper; 
    } 

} 

Sie auch eine Anfrage scoped ObjectMapper verwenden können, und konfigurieren Sie dann auf einer Pro-Anfrage Basis. Verwenden Sie diese Konfiguration:

@Bean 
public ObjectMapperResolver objectMapperResolver() { 
    return new ObjectMapperResolver() { 
    @Override 
    public ObjectMapper getObjectMapper() { 
     return requestScopedObjectMapper(); 
    } 
    }; 
} 


@Bean 
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) 
public ObjectMapper requestScopedObjectMapper() { 
    return new ObjectMapper(); 
} 

Dies ist am besten geeignet für die individuelle Antwort Serialisierung, da Sie es direkt im Controller-Methode konfigurieren. Für die benutzerdefinierte Deserialisierung müssen Sie auch Filter/HandlerInterceptor/ControllerAdvice verwenden, um den aktiven Mapper für die aktuelle Anforderung zu konfigurieren, bevor die Controller-Methode ausgelöst wird.

Sie können Schnittstelle, ähnlich wie ObjectMapperResolver erstellen:

public interface ObjectMapperConfigurer { 

    void configureObjectMapper(ObjectMapper objectMapper); 

} 

Dann eine Karte dieser Instanzen mit RequstMatcher s als Schlüssel machen und steckt es in einem Filter/HandlerInterceptor/ControllerAdvice ähnlich wie RequestMatcherObjectMapperResolver.

P.S.Wenn Sie die dynamische ObjectMapper Konfiguration ein bisschen weiter erforschen möchten, kann ich meine alte Antwort here vorschlagen. Es beschreibt, wie Sie zur Laufzeit dynamische @JsonFilter s machen können. Es enthält auch meinen älteren Ansatz mit erweiterten MappingJackson2HttpMessageConverter, dass ich in Kommentaren vorgeschlagen.

0

Sie können einen benutzerdefinierten Deserializer für Ihre String-Daten erstellen.

Individuelle Deserializer

public class CustomStringDeserializer extends JsonDeserializer<String> { 

    @Override 
    public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { 

    String str = p.getText(); 

    //return processed String 
    } 

}

Nehmen wir nun an der Schnur in einem POJO Verwendung @JsonDeserialize Anmerkung über die Variable vorhanden ist:

public class SamplePOJO{ 
    @JsonDeserialize(using=CustomStringDeserializer.class) 
    private String str; 
    //getter and setter 
} 

Nun, wenn Sie es als Antwort zurück Es wird auf die Weise deserialisiert, wie Sie es in CustomDeserializer getan haben.

Ich hoffe, es hilft.

+0

Ja, aber eine Anforderung war, dass ich die POJOs nicht ändere. – Mark

Verwandte Themen