简体   繁体   English

使用RestController接受字符串和XML数据

[英]Accept Strings and XML data with RestController

I want to create REST Server which accepts XML requests and plain text into different controllers. 我想创建REST服务器,它接受XML请求和纯文本到不同的控制器。 I tried to implement this: 我试图实现这个:

@SpringBootApplication
public class Application extends SpringBootServletInitializer implements WebMvcConfigurer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(Application.class);
    }
    ..............

    private BasicAuthenticationInterceptor basicAuthenticationInterceptor;

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.removeIf(converter -> converter instanceof MappingJackson2XmlHttpMessageConverter);
        converters.removeIf(converter -> converter instanceof MappingJackson2HttpMessageConverter);
        converters.add(new MappingJackson2XmlHttpMessageConverter(
                ((XmlMapper) createObjectMapper(Jackson2ObjectMapperBuilder.xml()))
                        .enable(ToXmlGenerator.Feature.WRITE_XML_DECLARATION)));
        converters.add(new MappingJackson2HttpMessageConverter(createObjectMapper(Jackson2ObjectMapperBuilder.json())));
    }

    private ObjectMapper createObjectMapper(Jackson2ObjectMapperBuilder builder) {
        builder.indentOutput(true);
        builder.modules(new JaxbAnnotationModule());
        builder.serializationInclusion(JsonInclude.Include.NON_NULL);
        builder.defaultUseWrapper(false);
        return builder.build();
    }

    @Autowired
    public void setBasicAuthenticationInterceptor(BasicAuthenticationInterceptor basicAuthenticationInterceptor) {
        this.basicAuthenticationInterceptor = basicAuthenticationInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(basicAuthenticationInterceptor);
    }
}

Check for XML proper formatting: 检查XML格式正确:

@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex,
                                                                  HttpHeaders headers, HttpStatus status, WebRequest request) {
        PaymentTransaction response;
        if (ex.getMessage().contains("Required request body")) {
            response = new PaymentTransaction(PaymentTransaction.Response.failed_response, 350,
                    "Invalid XML message: No XML data received", "XML request parsing failed!");
        } else {
            response = new PaymentTransaction(PaymentTransaction.Response.failed_response, 351,
                    "Invalid XML message format", null);
        }
        return ResponseEntity.badRequest().body(response);
    }
}

Controller Class: 控制器类:

@RestController()
public class HomeController {

    @Autowired
    public HomeController(Map<String, MessageProcessor> processors, Map<String, ReconcileProcessor> reconcileProcessors,
            @Qualifier("defaultProcessor") MessageProcessor defaultProcessor,
            AuthenticationService authenticationService, ClientRepository repository,
            @Value("${request.limit}") int requestLimit) {
        // Here I receive XML 
    }

    @GetMapping(value = "/v1/*")
    public String message() {
        return "REST server";
    }

    @PostMapping(value = "/v1/{token}", consumes = { MediaType.APPLICATION_XML_VALUE,
            MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_XML_VALUE,
                    MediaType.APPLICATION_JSON_VALUE })
    public PaymentResponse handleMessage(@PathVariable("token") String token,
            @RequestBody PaymentTransaction transaction, HttpServletRequest request) throws Exception {
        // Here I receive XML 
    }

    @PostMapping(value = "/v1/notification")
    public ResponseEntity<String> handleNotifications(@RequestBody Map<String, String> keyValuePairs) {
         // Here I receive key and value in request body
    }

    @PostMapping(value = "/v1/summary/by_date/{token}", consumes = { MediaType.APPLICATION_XML_VALUE,
            MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_XML_VALUE,
                    MediaType.APPLICATION_JSON_VALUE })
    public PaymentResponses handleReconcile(@PathVariable("token") String token, @RequestBody Reconcile reconcile,
            HttpServletRequest request) throws Exception {
         // Here I receive XML 
    }

    @ResponseStatus(value = HttpStatus.UNAUTHORIZED)
    public static class UnauthorizedException extends RuntimeException {
        UnauthorizedException(String message) {
            super(message);
        }
    }
}

As you can see in some methods I receive XML and in other I receive String in form of key=value&..... 正如你在某些方法中看到的那样,我收到了XML,而在其他方法中,我收到了key=value&.....形式的String

How I configure Spring to accept both types? 我如何配置Spring接受这两种类型? Also should I split the Rest controller into different files? 我还应该将Rest控制器拆分成不同的文件吗?

EDIT: 编辑:

Sample XML request: 示例XML请求:

<?xml version="1.0" encoding="UTF-8"?>
<payment_transaction>
  <transaction_type>authorize</transaction_type>
  <transaction_id>2aeke4geaclv7ml80</transaction_id>
  <amount>1000</amount>
  <currency>USD</currency>
  <card_number>22</card_number>
  <shipping_address>
    <first_name>Name</first_name>    
  </shipping_address>
</payment_transaction>

Sample XML response: XML响应示例:

<?xml version="1.0" encoding="UTF-8"?>
<payment_response>
    <transaction_type>authorize</transaction_type>
    <status>approved</status>
    <unique_id>5f7edd36689f03324f3ef531beacfaae</unique_id>
    <transaction_id>asdsdlddea4sdaasdsdsa4dadasda</transaction_id>
    <code>500</code>
    <amount>101</amount>
    <currency>EUR</currency>
</payment_response>

Sample Notification request: 样品通知请求:

uniqueid=23434&type=sale&status=33

Sample Notification response: It should return only HTTP status OK. 示例通知响应:它应该只返回HTTP状态OK。

I use: 我用:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.4.RELEASE</version>
        <relativePath />
    </parent>

Java version: "10.0.2" 2018-07-17 Java版本:“10.0.2”2018-07-17

About the XML generation I use: 关于我使用的XML生成:

@XmlRootElement(name = "payment_transaction")
public class PaymentTransaction {

    public enum Response {
        failed_response, successful_response
    }

    @XmlElement(name = "transaction_type")
    public String transactionType;
    @XmlElement(name = "transaction_id")
    public String transactionId;
    @XmlElement(name = "usage")

POM Configuration: https://pastebin.com/zXqYhDH3 POM配置: https//pastebin.com/zXqYhDH3

Update this solution works for pre-2.x Spring-boot versions. 更新此解决方案适用于2.x之前的Spring-boot版本。 Another thing to consider that during my tests I used Jackson's XML annotations on my DTOs (JacksonXmlRootElement, JacksonXmlProperty) and maybe FormHttpMessageConverter can handle DTOs with standard JAXB annotations (see my answer for Spring 2.0.4-RELEASE) - so may you'd better to go to that direction if you can (or at least give it a try before you apply the sketched solution). 另外需要考虑的是,在我的测试中,我在我的DTO上使用了Jackson的XML注释(JacksonXmlRootElement,JacksonXmlProperty),也许FormHttpMessageConverter可以使用标准的JAXB注释处理DTO(请参阅我对Spring 2.0.4-RELEASE的回答) - 所以你可能会更好如果你可以去那个方向(或者至少在你应用草图解决方案之前尝试一下)。

This is my solution. 这是我的解决方案。 I dropped the RequestIntereptor (because that is rather for inspect the request not for modifying it) and the RequestBodyAdvice too (because it turned out that there is a better way. 我删除了RequestIntereptor(因为这是为了检查请求而不是修改它)和RequestBodyAdvice(因为它证明有更好的方法)。

If you have a look for the available MessageConverters you can see that the only MessageConverter that reads the posted form data is the FormHttpMessageConverter. 如果您查看可用的MessageConverters,您会发现读取已发布表单数据的唯一MessageConverter是FormHttpMessageConverter。 The problem with this class is the return type, which is Multivaluemap 此类的问题是返回类型,即Multivaluemap

But, using this class as a base, I have created an abstract class that reads the form data to this Multivaluemap, and have only one abstract funtion that you have to implement in the subclass: that will create an object from the values stored in the multivaluemap. 但是,使用这个类作为基础,我创建了一个抽象类,它将表单数据读取到这个Multivaluemap,并且只有一个必须在子类中实现的抽象函数:这将从存储在子类中的值创建一个对象multivaluemap。

Unfortunately I had to introduce an interface (because I kept the original implementation of the writing part just adopt it) on the DTO you would like to read. 不幸的是,我不得不在你想要阅读的DTO上介绍一个接口(因为我保留了写作部分的原始实现只是采用它)。

All in all, my working solution: 总而言之,我的工作解决方案:

In the WebMvcConfigurerAdapter class, I have this config: WebMvcConfigurerAdapter类中,我有这个配置:

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        MyRequestBodyHttpMessageConverter converter = new MyRequestBodyHttpMessageConverter();
        //FormHttpMessageConverter converter = new FormHttpMessageConverter();
        MediaType utf8FormEncoded = new MediaType("application","x-www-form-urlencoded", Charset.forName("UTF-8"));
        //MediaType mediaType = MediaType.APPLICATION_FORM_URLENCODED; maybe UTF-8 is not needed
        //converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_FORM_URLENCODED));
        converter.setSupportedMediaTypes(Arrays.asList(utf8FormEncoded));
        converters.add(converter);
        converters.add(new MappingJackson2HttpMessageConverter());
        converters.add(new MappingJackson2XmlHttpMessageConverter());
        super.configureMessageConverters(converters);
    }

I modified a bit your controller functions: 我修改了一下你的控制器功能:

    @PostMapping(value = "/v1/{token}",
        consumes =  MediaType.APPLICATION_XML_VALUE,
        produces = MediaType.APPLICATION_JSON_VALUE)
    public @ResponseBody  MyResponseBody handleMessage(@PathVariable("token") String token, @RequestBody MyRequestBody transaction, HttpServletRequest request) throws  Exception {
       MyResponseBody body = new MyResponseBody();
       body.setId(transaction.getId());
       body.setName("received " + transaction.getName());
       return body;
     }

// check @ModelAttribute workaround https://stackoverflow.com/questions/4339207/http-post-with-request-content-type-form-not-working-in-spring-mvc-3

    @PostMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, value = "/v1/notification")
    public ResponseEntity<String> handleNotifications(@ModelAttribute MyRequestBody transaction) {
       return new ResponseEntity<String>(HttpStatus.OK);
    }

(in the next part the import packages are meaningful, some mail api classes can be found somewhere else) (在下一部分中,导入包是有意义的,一些邮件api类可以在其他地方找到)

import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.mail.internet.MimeUtility;

import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.StreamingHttpOutputMessage;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;


/**
 * based on {@link org.springframework.http.converter.FormHttpMessageConverter
 *
 * it uses the readed MultiValueMap to build up the DTO we would like to get from the request body.
 */

public abstract class AbstractRequestBodyFormHttpMessageConverter<T extends RequestParamSupport> implements HttpMessageConverter<T> {

    /**
    * This is the only method you have to implement for your DTO class
    * the class must implement RequestParamSupport
    */    
    protected abstract T buildObject(MultiValueMap<String, Object> valueMap);

    public interface RequestParamSupport{
        MultiValueMap<String, Object> getRequestParams();
    }


    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    private List<MediaType> supportedMediaTypes = new ArrayList<MediaType>();

    private List<HttpMessageConverter<?>> partConverters = new ArrayList<HttpMessageConverter<?>>();

    private Charset charset = DEFAULT_CHARSET;

    private Charset multipartCharset;

    private Class<T> bodyClass;

    public AbstractRequestBodyFormHttpMessageConverter(Class<T> bodyClass) {
        this.bodyClass = bodyClass;
        this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
        this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);

        StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
        stringHttpMessageConverter.setWriteAcceptCharset(false);  // see SPR-7316

        this.partConverters.add(new ByteArrayHttpMessageConverter());
        this.partConverters.add(stringHttpMessageConverter);
        this.partConverters.add(new ResourceHttpMessageConverter());

        applyDefaultCharset();
    }

    /**
     * Set the character set to use when writing multipart data to encode file
     * names. Encoding is based on the encoded-word syntax defined in RFC 2047
     * and relies on {@code MimeUtility} from "javax.mail".
     * <p>If not set file names will be encoded as US-ASCII.
     * @since 4.1.1
     * @see <a href="http://en.wikipedia.org/wiki/MIME#Encoded-Word">Encoded-Word</a>
     */
    public void setMultipartCharset(Charset charset) {
        this.multipartCharset = charset;
    }

    /**
     * Apply the configured charset as a default to registered part converters.
     */
    private void applyDefaultCharset() {
        for (HttpMessageConverter<?> candidate : this.partConverters) {
            if (candidate instanceof AbstractHttpMessageConverter) {
                AbstractHttpMessageConverter<?> converter = (AbstractHttpMessageConverter<?>) candidate;
                // Only override default charset if the converter operates with a charset to begin with...
                if (converter.getDefaultCharset() != null) {
                    converter.setDefaultCharset(this.charset);
                }
            }
        }
    }


    @Override
    public boolean canRead(Class<?> clazz, MediaType mediaType) {
        if (!bodyClass.isAssignableFrom(clazz)) {
            return false;
        }
        if (mediaType == null) {
            return true;
        }
        for (MediaType supportedMediaType : getSupportedMediaTypes()) {
            // We can't read multipart....
            if (!supportedMediaType.equals(MediaType.MULTIPART_FORM_DATA) && supportedMediaType.includes(mediaType)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        if (!bodyClass.isAssignableFrom(clazz)) {
            return false;
        }
        if (mediaType == null || MediaType.ALL.equals(mediaType)) {
            return true;
        }
        for (MediaType supportedMediaType : getSupportedMediaTypes()) {
            if (supportedMediaType.isCompatibleWith(mediaType)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Set the list of {@link MediaType} objects supported by this converter.
     */
    public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) {
        this.supportedMediaTypes = supportedMediaTypes;
    }

    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return Collections.unmodifiableList(this.supportedMediaTypes);
    }

    @Override
    public T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {
        MediaType contentType = inputMessage.getHeaders().getContentType();
        Charset charset = (contentType.getCharset() != null ? contentType.getCharset() : this.charset);
        String body = StreamUtils.copyToString(inputMessage.getBody(), charset);

        String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
        MultiValueMap<String, Object> result = new LinkedMultiValueMap<String, Object>(pairs.length);
        for (String pair : pairs) {
            int idx = pair.indexOf('=');
            if (idx == -1) {
                result.add(URLDecoder.decode(pair, charset.name()), null);
            }
            else {
                String name = URLDecoder.decode(pair.substring(0, idx), charset.name());
                String value = URLDecoder.decode(pair.substring(idx + 1), charset.name());
                result.add(name, value);
            }
        }
        return buildObject(result);
    }

    @Override
    public void write(T object, MediaType contentType,
            HttpOutputMessage outputMessage) throws IOException,
            HttpMessageNotWritableException {
        if (!isMultipart(object, contentType)) {
            writeForm(object.getRequestParams(), contentType, outputMessage);
        }
        else {
            writeMultipart(object.getRequestParams(), outputMessage);
        }
    }

    private boolean isMultipart(RequestParamSupport object, MediaType contentType) {
        if (contentType != null) {
            return MediaType.MULTIPART_FORM_DATA.includes(contentType);
        }
        MultiValueMap<String, Object> map = object.getRequestParams();
        for (String name : map.keySet()) {
            for (Object value : map.get(name)) {
                if (value != null && !(value instanceof String)) {
                    return true;
                }
            }
        }
        return false;
    }

    private void writeForm(MultiValueMap<String, Object> form, MediaType contentType,
            HttpOutputMessage outputMessage) throws IOException {

        Charset charset;
        if (contentType != null) {
            outputMessage.getHeaders().setContentType(contentType);
            charset = (contentType.getCharset() != null ? contentType.getCharset() : this.charset);
        }
        else {
            outputMessage.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED);
            charset = this.charset;
        }
        StringBuilder builder = new StringBuilder();
        for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext();) {
            String name = nameIterator.next();
            for (Iterator<Object> valueIterator = form.get(name).iterator(); valueIterator.hasNext();) {
                String value = (String) valueIterator.next();
                builder.append(URLEncoder.encode(name, charset.name()));
                if (value != null) {
                    builder.append('=');
                    builder.append(URLEncoder.encode(value, charset.name()));
                    if (valueIterator.hasNext()) {
                        builder.append('&');
                    }
                }
            }
            if (nameIterator.hasNext()) {
                builder.append('&');
            }
        }
        final byte[] bytes = builder.toString().getBytes(charset.name());
        outputMessage.getHeaders().setContentLength(bytes.length);

        if (outputMessage instanceof StreamingHttpOutputMessage) {
            StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
            streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
                @Override
                public void writeTo(OutputStream outputStream) throws IOException {
                    StreamUtils.copy(bytes, outputStream);
                }
            });
        }
        else {
            StreamUtils.copy(bytes, outputMessage.getBody());
        }
    }

    private void writeMultipart(final MultiValueMap<String, Object> parts, HttpOutputMessage outputMessage) throws IOException {
        final byte[] boundary = generateMultipartBoundary();
        Map<String, String> parameters = Collections.singletonMap("boundary", new String(boundary, "US-ASCII"));

        MediaType contentType = new MediaType(MediaType.MULTIPART_FORM_DATA, parameters);
        HttpHeaders headers = outputMessage.getHeaders();
        headers.setContentType(contentType);

        if (outputMessage instanceof StreamingHttpOutputMessage) {
            StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
            streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
                @Override
                public void writeTo(OutputStream outputStream) throws IOException {
                    writeParts(outputStream, parts, boundary);
                    writeEnd(outputStream, boundary);
                }
            });
        }
        else {
            writeParts(outputMessage.getBody(), parts, boundary);
            writeEnd(outputMessage.getBody(), boundary);
        }
    }

    private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException {
        for (Map.Entry<String, List<Object>> entry : parts.entrySet()) {
            String name = entry.getKey();
            for (Object part : entry.getValue()) {
                if (part != null) {
                    writeBoundary(os, boundary);
                    writePart(name, getHttpEntity(part), os);
                    writeNewLine(os);
                }
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void writePart(String name, HttpEntity<?> partEntity, OutputStream os) throws IOException {
        Object partBody = partEntity.getBody();
        Class<?> partType = partBody.getClass();
        HttpHeaders partHeaders = partEntity.getHeaders();
        MediaType partContentType = partHeaders.getContentType();
        for (HttpMessageConverter<?> messageConverter : this.partConverters) {
            if (messageConverter.canWrite(partType, partContentType)) {
                HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os);
                multipartMessage.getHeaders().setContentDispositionFormData(name, getFilename(partBody));
                if (!partHeaders.isEmpty()) {
                    multipartMessage.getHeaders().putAll(partHeaders);
                }
                ((HttpMessageConverter<Object>) messageConverter).write(partBody, partContentType, multipartMessage);
                return;
            }
        }
        throw new HttpMessageNotWritableException("Could not write request: no suitable HttpMessageConverter " +
                "found for request type [" + partType.getName() + "]");
    }


    /**
     * Generate a multipart boundary.
     * <p>This implementation delegates to
     * {@link MimeTypeUtils#generateMultipartBoundary()}.
     */
    protected byte[] generateMultipartBoundary() {
        return MimeTypeUtils.generateMultipartBoundary();
    }

    /**
     * Return an {@link HttpEntity} for the given part Object.
     * @param part the part to return an {@link HttpEntity} for
     * @return the part Object itself it is an {@link HttpEntity},
     * or a newly built {@link HttpEntity} wrapper for that part
     */
    protected HttpEntity<?> getHttpEntity(Object part) {
        return (part instanceof HttpEntity ? (HttpEntity<?>) part : new HttpEntity<Object>(part));
    }

    /**
     * Return the filename of the given multipart part. This value will be used for the
     * {@code Content-Disposition} header.
     * <p>The default implementation returns {@link Resource#getFilename()} if the part is a
     * {@code Resource}, and {@code null} in other cases. Can be overridden in subclasses.
     * @param part the part to determine the file name for
     * @return the filename, or {@code null} if not known
     */
    protected String getFilename(Object part) {
        if (part instanceof Resource) {
            Resource resource = (Resource) part;
            String filename = resource.getFilename();
            if (filename != null && this.multipartCharset != null) {
                filename = MimeDelegate.encode(filename, this.multipartCharset.name());
            }
            return filename;
        }
        else {
            return null;
        }
    }


    private void writeBoundary(OutputStream os, byte[] boundary) throws IOException {
        os.write('-');
        os.write('-');
        os.write(boundary);
        writeNewLine(os);
    }

    private static void writeEnd(OutputStream os, byte[] boundary) throws IOException {
        os.write('-');
        os.write('-');
        os.write(boundary);
        os.write('-');
        os.write('-');
        writeNewLine(os);
    }

    private static void writeNewLine(OutputStream os) throws IOException {
        os.write('\r');
        os.write('\n');
    }


    /**
     * Implementation of {@link org.springframework.http.HttpOutputMessage} used
     * to write a MIME multipart.
     */
    private static class MultipartHttpOutputMessage implements HttpOutputMessage {

        private final OutputStream outputStream;

        private final HttpHeaders headers = new HttpHeaders();

        private boolean headersWritten = false;

        public MultipartHttpOutputMessage(OutputStream outputStream) {
            this.outputStream = outputStream;
        }

        @Override
        public HttpHeaders getHeaders() {
            return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers);
        }

        @Override
        public OutputStream getBody() throws IOException {
            writeHeaders();
            return this.outputStream;
        }

        private void writeHeaders() throws IOException {
            if (!this.headersWritten) {
                for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
                    byte[] headerName = getAsciiBytes(entry.getKey());
                    for (String headerValueString : entry.getValue()) {
                        byte[] headerValue = getAsciiBytes(headerValueString);
                        this.outputStream.write(headerName);
                        this.outputStream.write(':');
                        this.outputStream.write(' ');
                        this.outputStream.write(headerValue);
                        writeNewLine(this.outputStream);
                    }
                }
                writeNewLine(this.outputStream);
                this.headersWritten = true;
            }
        }

        private byte[] getAsciiBytes(String name) {
            try {
                return name.getBytes("US-ASCII");
            }
            catch (UnsupportedEncodingException ex) {
                // Should not happen - US-ASCII is always supported.
                throw new IllegalStateException(ex);
            }
        }
    }


    /**
     * Inner class to avoid a hard dependency on the JavaMail API.
     */
    private static class MimeDelegate {

        public static String encode(String value, String charset) {
            try {
                return MimeUtility.encodeText(value, charset, null);
            }
            catch (UnsupportedEncodingException ex) {
                throw new IllegalStateException(ex);
            }
        }
    }
}

The bean converter implementation bean转换器实现

public class MyRequestBodyHttpMessageConverter extends
        AbstractRequestBodyFormHttpMessageConverter<MyRequestBody> {

    public MyRequestBodyHttpMessageConverter() {
        super(MyRequestBody.class);
    }

    @Override
    protected MyRequestBody buildObject(MultiValueMap<String, Object> valueMap) {
        MyRequestBody parsed = new MyRequestBody();
        parsed.setId(Long.valueOf((String)valueMap.get("id").get(0)));
        parsed.setName((String)valueMap.get("name").get(0));
        parsed.setRequestParams(valueMap);
        return parsed;
    }
}

And finally the MyRequestBody DTO (the MyRequestBody was the same just with different name) 最后是MyRequestBody DTO(MyRequestBody和其他名字一样)

@JacksonXmlRootElement
public class MyRequestBody implements RequestParamSupport, Serializable {

    @JsonIgnore
    private transient MultiValueMap<String, Object> requestParams;

    @JacksonXmlProperty
    private Long id;
    @JacksonXmlProperty
    private String name;

    //empty constructor, getters, setters, tostring, etc

    @Override
    public MultiValueMap<String, Object> getRequestParams() {
        return requestParams;
    }
}

** Finally my answers: ** **最后我的回答:**

How I configure Spring to accept both types? 我如何配置Spring接受这两种类型?

As you can see, you have to have your own form-data to your bean converter. 如您所见,您必须拥有自己的表单数据到bean转换器。 (Do not forget that you have to use @ModelAttribute when you are mapping from form data and not @RequestBody.) (不要忘记在从表单数据映射而不是@RequestBody时必须使用@ModelAttribute。)

Also should I split the Rest controller into different files? 我还应该将Rest控制器拆分成不同的文件吗?

No, that is not necessary, just register your converter. 不,这不是必需的,只需注册您的转换器。

This is an another solution (it worked well for me) with less Spring magic and using the good old way of HttpServletRequestWrapper. 这是另一种解决方案(对我来说效果很好),使用较少的Spring魔法并使用HttpServletRequestWrapper的旧方法。

In the WebMvcConfigurerAdapter class, now we don't need the MessageConverter: WebMvcConfigurerAdapter类中,现在我们不需要MessageConverter:

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    //MyRequestBodyHttpMessageConverter converter = new MyRequestBodyHttpMessageConverter();
    //FormHttpMessageConverter converter = new FormHttpMessageConverter();
    //MediaType utf8FormEncoded = new MediaType("application","x-www-form-urlencoded", Charset.forName("UTF-8"));
    //MediaType mediaType = MediaType.APPLICATION_FORM_URLENCODED; maybe UTF-8 is not needed
    //converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_FORM_URLENCODED));
    //converter.setSupportedMediaTypes(Arrays.asList(utf8FormEncoded));
    //converters.add(converter);
    converters.add(new MappingJackson2HttpMessageConverter());
    converters.add(new MappingJackson2XmlHttpMessageConverter());
    super.configureMessageConverters(converters);
}

And everything else happens in this (servlet) Filter implementation: 其他一切都发生在这个(servlet)Filter实现中:

@WebFilter("/v1/notification")
public class MyRequestBodyFilter implements Filter {

    private static class MyServletInputStream extends ServletInputStream {

        private ByteArrayInputStream buffer;

        public MyServletInputStream(byte[] contents) {
            this.buffer = new ByteArrayInputStream(contents);
        }

        @Override
        public int read() throws IOException {
            return buffer.read();
        }

        @Override
        public boolean isFinished() {
            return buffer.available() == 0;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setReadListener(ReadListener listener) {
            throw new RuntimeException("Not implemented");
        }
    }

    private class MyHttpServletRequestWrapper extends HttpServletRequestWrapper{

        MyHttpServletRequestWrapper(HttpServletRequest request) {
            super(request);
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            // converting the request parameters to the pojo and serialize it to XML
            // the drawback of this way that the xml will be parsed again somewhere later
            long id = Long.parseLong(getRequest().getParameter("id"));
            String name = getRequest().getParameter("name");
            MyRequestBody body = new MyRequestBody();
            body.setId(id);
            body.setName(name);
            return new MyServletInputStream(new XmlMapper().writeValueAsBytes(body));
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        chain.doFilter(new MyHttpServletRequestWrapper(httpRequest), response);
    }

    @Override
    public void destroy() {

    }
}

I have changed nothing in my test controller, so the signature of the methods remained the same: 我在测试控制器中没有改变任何内容,因此方法的签名保持不变:

@PostMapping(value = "/v1/{token}",
        consumes =  MediaType.APPLICATION_XML_VALUE,
        produces = MediaType.APPLICATION_JSON_VALUE)
public @ResponseBody  MyResponseBody handleMessage(@PathVariable("token") String token, @RequestBody MyRequestBody transaction, HttpServletRequest request) throws     Exception {
           MyResponseBody body = new MyResponseBody();
           body.setId(transaction.getId());
           body.setName("received " + transaction.getName());
           return body;
}

@PostMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, value = "/v1/notification")
public ResponseEntity<String> handleNotifications(@ModelAttribute MyRequestBody transaction) {
       return new ResponseEntity<String>(HttpStatus.OK);
}

For Spring boot 2.0.4-RELEASE, it seems you don't have to do a lot. 对于Spring boot 2.0.4-RELEASE,似乎你不需要做很多事情。

I made this configuration: 我做了这个配置:

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        //MyRequestBodyHttpMessageConverter converter = new MyRequestBodyHttpMessageConverter();
        FormHttpMessageConverter converter = new FormHttpMessageConverter();
        //MediaType utf8FormEncoded = new MediaType("application","x-www-form-urlencoded", Charset.forName("UTF-8"));
        //MediaType mediaType = MediaType.APPLICATION_FORM_URLENCODED; maybe UTF-8 is not needed
        converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_FORM_URLENCODED));
        //converter.setSupportedMediaTypes(Arrays.asList(utf8FormEncoded));
        converters.add(converter);
        MappingJackson2HttpMessageConverter conv1 = new MappingJackson2HttpMessageConverter();
        conv1.getObjectMapper().registerModule(new JaxbAnnotationModule());
        converters.add(conv1);

        MappingJackson2XmlHttpMessageConverter conv = new MappingJackson2XmlHttpMessageConverter();
        // required by jaxb annotations
        conv.getObjectMapper().registerModule(new JaxbAnnotationModule());
        converters.add(conv);
    }
}

I used about your DTO: 我用过你的DTO:

@XmlRootElement(name = "payment_transaction")
public class PaymentTransaction {

    @XmlElement(name = "transaction_type")
    public String transactionType;
    @XmlElement(name = "transaction_id")
    public String transactionId;

    public String getTransactionType() {
        return transactionType;
    }
    public void setTransactionType(String transactionType) {
        this.transactionType = transactionType;
    }
    public String getTransactionId() {
        return transactionId;
    }
    public void setTransactionId(String transactionId) {
        this.transactionId = transactionId;
    }
    @Override
    public String toString() {
        return "PaymentTransaction [transactionType=" + transactionType
                + ", transactionId=" + transactionId + "]";
    }
}

The controller: 控制器:

@RestController
public class MyController {

    /**
     * https://stackoverflow.com/questions/34782025/http-post-request-with-content-type-application-x-www-form-urlencoded-not-workin/38252762#38252762
    */

    @PostMapping(value = "/v1/{token}",
            consumes =  MediaType.APPLICATION_XML_VALUE,
            produces = MediaType.APPLICATION_JSON_VALUE)
    public @ResponseBody  PaymentTransaction handleMessage(@PathVariable("token") String token,
            @RequestBody PaymentTransaction transaction, HttpServletRequest request) throws Exception {
           System.out.println("handleXmlMessage");
           System.out.println(transaction);
           PaymentTransaction body = new PaymentTransaction();
           body.setTransactionId(transaction.getTransactionId());
           body.setTransactionType("received: " + transaction.getTransactionType());
           return body;
    }

    @PostMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, value = "/v1/notification")
    public ResponseEntity<String> handleNotifications(@ModelAttribute PaymentTransaction transaction) {
           System.out.println("handleFormMessage");
           System.out.println(transaction);
           return new ResponseEntity<String>(HttpStatus.OK);
    }
 }

The only main thing to remember that it seems the filling of the DTO with the parsed data happens by reflection: 唯一要记住的是,似乎用解析数据填充DTO是通过反射来实现的:

For your input 供您参考

<payment_transaction>
  <transaction_id>1</transaction_id>
  <transaction_type>name</transaction_type>
</payment_transaction>

I got this response (see my controller): 我收到了这个回复(见我的控制器):

{
"transactionType": "received: null",
"transactionId": null
}

But when I changed to the name of the fields of the DTO, it started to work (the root element did not matter, interesting): 但是当我改为DTO字段的名称时,它开始工作(根元素无关紧要,有趣):

<payment_transaction>
  <transactionId>1</transactionId>
  <transactionType>name</transactionType>
</payment_transaction>

result: 结果:

{
"transactionType": "received: name",
"transactionId": "1"
}

The same is true for the querystring. 查询字符串也是如此。 I don't know what to change to get spring to parse the xmls using the defined names in @XmlRootElement/@XmlElement. 我不知道要改变什么来让Spring使用@ XmlRootElement / @ XmlElement中定义的名称来解析xmls。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM