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

Posted

技术标签:

【中文标题】使用 RestController 接受字符串和 XML 数据【英文标题】:Accept Strings and XML data with RestController 【发布时间】:2019-02-06 15:38:39 【问题描述】:

我想创建接受 XML 请求和纯文本到不同控制器的 REST 服务器。我试图实现这一点:

@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);
    

检查 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);
    

控制器类:

@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);
        
    

正如您所见,在某些方法中我收到 XML,而在其他方法中我收到 key=value&amp;..... 形式的字符串

如何配置 Spring 以接受这两种类型? 我还应该将 Rest 控制器拆分为不同的文件吗?

编辑:

示例 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>

示例 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>

示例通知请求:

uniqueid=23434&type=sale&status=33

示例通知响应:它应该只返回 HTTP 状态 OK。

我用:

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

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

关于我使用的 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 配置:https://pastebin.com/zXqYhDH3

【问题讨论】:

接受这两种格式是什么意思? 我的意思是:实现的 Rest 服务器应该接受 XML 请求和简单的键和值到请求正文中。 如何实现request interceptor,它可以在请求到达您的控制器或RequestBodyAdvice 之前对其进行操作?通过这种方式,您可以只有一个控制器来处理 XML 输入,在拦截器/通知中,您可以操纵请求正文以能够转发预期的 XML。 @m4gic 你能用工作示例粘贴官方答案吗? 如果您希望我这样做,请提供查询字符串版本和 XML 版本的示例请求。请求参数和请求正文也很重要。请显示预期的输出(例如 XML,应该从请求中解析)。我也想知道 spring-boot 版本和 JDK 版本。但是我只能在晚上创建一个示例。 【参考方案1】:

更新此解决方案适用于 pre-2.x Spring-boot 版本。 另一件要考虑的事情是,在我的测试期间,我在我的 DTO(JacksonXmlRootElement、JacksonXmlProperty)上使用了 Jackson 的 XML 注释,也许 FormHttpMessageConverter 可以处理带有标准 JAXB 注释的 DTO(请参阅我对 Spring 2.0.4-RELEASE 的回答) - 你可以如果可以的话,最好朝着那个方向前进(或者至少在应用草图解决方案之前尝试一下)。

这是我的解决方案。我放弃了 RequestIntereptor(因为那是为了检查请求而不是修改它)和 RequestBodyAdvice(因为事实证明有更好的方法。

如果您查看available MessageConverters,您会发现读取已发布表单数据的唯一MessageConverter 是FormHttpMessageConverter。 这个类的问题在于返回类型,即Multivaluemap

但是,使用这个类作为基础,我创建了一个抽象类,它将表单数据读取到这个 Multivaluemap,并且只有一个您必须在子类中实现的抽象功能:它将根据值创建一个对象存储在多值映射中。

不幸的是,我不得不在您想阅读的 DTO 上引入一个接口(因为我保留了编写部分的原始实现,只是采用它)。

总而言之,我的工作解决方案:

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);
    

我修改了一点你的控制器功能:

    @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://***.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);
    

(下一部分导入包是有意义的,一些邮件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);
            
        
    

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;
    

最后是 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;
    

** 最后我的答案:**

如何配置 Spring 以接受这两种类型?

如您所见,您的 bean 转换器必须有自己的表单数据。 (不要忘记从表单数据映射时必须使用@ModelAttribute,而不是@RequestBody。)

我还应该将 Rest 控制器拆分为不同的文件吗?

不,这不是必需的,只需注册您的转换器。

【讨论】:

唯一的问题是,这不会发生在“幕后”,你必须在 AbstractRequestBodyFormHttpMessageConverter 子类中实现你的映射并且你必须注册它才能使用。但至少它有效。 另外我必须告诉你,我只测试了 application/x-www-form-urlencoded 标头,而不是 multipart/form-data。【参考方案2】:

对于 Spring boot 2.0.4-RELEASE,看来你不需要做很多事情。

我做了这个配置:

@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);
    

我用过你的 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 + "]";
    

控制器:

@RestController
public class MyController 

    /**
     * https://***.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);
    
 

唯一要记住的主要事情是,似乎用解析的数据填充 DTO 是通过反射发生的:

供您参考

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

我收到了这个回复(查看我的控制器):


"transactionType": "received: null",
"transactionId": null

但是当我更改为DTO的字段名称时,它开始起作用(根元素无关紧要,有趣):

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

结果:


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

查询字符串也是如此。我不知道要更改什么才能让 spring 使用 @XmlRootElement/@XmlElement 中定义的名称来解析 xml。

【讨论】:

让我们continue this discussion in chat.【参考方案3】:

这是另一种解决方案(对我来说效果很好),使用较少的 Spring 魔法并使用 HttpServletRequestWrapper 的旧方法。

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);

其他一切都发生在这个(servlet)过滤器实现中:

@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() 

    

我的测试控制器中没有任何更改,因此方法的签名保持不变:

@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);

【讨论】:

我必须在 handleNotifications 上保留 APPLICATION_FORM_URLENCODED_VALUE 和 ModelAttribute,可能如果我在过滤器中将请求的 Content-type 修改为“application/xml”,它的工作方式与 handleMessage 相同(使用 RequestBody 和使用消耗 = MediaType.APPLICATION_XML_VALUE)。 非常感谢您的努力。从长远来看,您会推荐哪种解决方案?请查看更新后的帖子,了解您的上述问题。 我会选择对 Spring 更友好的方法,因为它应该更快(您不必创建稍后将解析的 xml)。如果 Spring 友好版本由于某种原因(类路径、jaxb/marhalling 问题)不起作用,我将使用第二种方法作为备用计划。在 servlet 包装器中,我使用了 XmlMapper,我认为它可以很好地与 Jackson 相关的 xml 注释一起使用,所以如果你使用它,也许你应该更改序列化。后者的一个优点是更通用并且不需要修改您的 DTO(无需使用接口进行扩展)。 你的意思是第一种方法? 是的,我会使用自定义 MessageConverter。为了让它工作,你需要在项目依赖项中添加一个 mail-api jar(你的 spring-boot 版本使用 1.6.1 版本)。

以上是关于使用 RestController 接受字符串和 XML 数据的主要内容,如果未能解决你的问题,请参考以下文章

spring RESTcontroller 接受 dataURI

Spring @RestController,spring-boot 出现意外错误(类型=不可接受,状态=406)

Java - 如何使用邮递员插入数据

Spring 的@Controller 和@RestController的区别

小白面试题:@Controller和@RestController的区别

Springboot @RestController 请求不跳转返回字符串?