protobuf接口调用报错:java.nio.charset.MalformedInputException: Input length = 1

  使用 protobuf 定义的接口 api 发起 http 请求报错,日志如下:

[2018-04-16 17:35:16] ERROR ServletRequestParser:46 - IOException:
request=/wlf-hello-war/wlf.hello.helloService/getHelloRank, ex=java.nio.charset.MalformedInputException: Input length = 1

  我们来看 ServletRequestParser 的报错代码行:

import java.io.IOException;
import java.math.BigDecimal;
import javax.servlet.http.HttpServletRequest;
import javax.xml.bind.DatatypeConverter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import com.google.protobuf.ByteString;
import com.google.protobuf.Descriptors;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
import com.google.protobuf.util.JsonFormat;
public class ServletRequestParser {
private static final Logger logger = LoggerFactory.getLogger(ServletRequestParser.class);

public static Message parse(HttpServletRequest request, Message prototype) {
String requestMethod = request.getMethod();
Descriptors.Descriptor inputType = prototype.getDescriptorForType();
Message.Builder builder = prototype.newBuilderForType();
if (HttpMethod.POST.matches(requestMethod)) {
MediaType contentType = ContentType.BINARY;
try {
contentType = MediaType.valueOf(request.getContentType());
} catch (Exception ex) {
}
try {
if (ContentType.isProtobuf(contentType) || ContentType.isBinary(contentType)) {
return builder.mergeFrom(request.getInputStream()).build();
} else if (ContentType.isJson(contentType)) {
request.setCharacterEncoding("utf-8");
JsonFormat.parser().merge(request.getReader(), builder);
return builder.build();
} else {
logger.error("invalid content-type: {}", contentType);
}
} catch (InvalidProtocolBufferException ex) {
logger.error("InvalidProtocolBuffer: request={}, ex={}", request.getRequestURI(), ex);
} catch (IOException ex) {
logger.error("IOException: request={}, ex={}", request.getRequestURI(), ex);
}
} else if (HttpMethod.GET.matches(requestMethod)) {
for (Descriptors.FieldDescriptor field : inputType.getFields()) {
String[] values = request.getParameterValues(field.getName());
if (null != values && values.length > 0) {
if (!field.isRepeated()) {
Object o = parseFieldValue(field, values[0], builder);
if (null != o) builder.setField(field, o);
} else {
for (String value : values) {
Object o = parseFieldValue(field, value, builder);
if (null != o) builder.addRepeatedField(field, o);
}
}
}
}
return builder.build();
}
return null;
}

  这里调用了 com.google.protobuf.util.JsonFormat 的内部类 Parser 的 merge 方法进行转换时报错,因为异常只在这里抛出,所以只能怀疑获取 Reader 对象的这个 request.getReader() 方法,跟进代码:

/**
 * Wrapper object for the Coyote request.
 *
 * @author Remy Maucherat
 * @author Craig R. McClanahan
 */
public class Request implements org.apache.catalina.servlet4preview.http.HttpServletRequest {
/**
     * Read the Reader wrapping the input stream for this Request.  The
     * default implementation wraps a <code>BufferedReader</code> around the
     * servlet input stream returned by <code>createInputStream()</code>.
     *
     * @return a buffered reader for the request
     * @exception IllegalStateException if <code>getInputStream()</code>
     *  has already been called for this request
     * @exception IOException if an input/output error occurs
     */
    @Override
    public BufferedReader getReader() throws IOException {
    if (usingInputStream) {
        throw new IllegalStateException
            (sm.getString("coyoteRequest.getReader.ise"));
    }

    usingReader = true;
    inputBuffer.checkConverter();
    if (reader == null) {
        reader = <span style="background-color: rgba(255, 255, 0, 1)">new CoyoteReader(inputBuffer)</span>;
    }
    return reader;

}

   这里调用的是 catalina 的 Requst 对象的 getReader 方法,返回的是一个 BufferedReader 对象,这里最终实例化出了一个 CoyoteReader 对象。

    /**
     * Reader.
     */
    protected CoyoteReader reader = new CoyoteReader(inputBuffer);

  我们来看下 CoyoteReader 对象:  

/**
 * Coyote implementation of the buffered reader.
 *
 * @author Remy Maucherat
 */
public class CoyoteReader
    extends BufferedReader {
// -------------------------------------------------------------- Constants


private static final char[] LINE_SEP = { '\r', '\n' };
private static final int MAX_LINE_LENGTH = 4096;


// ----------------------------------------------------- Instance Variables


protected <span style="background-color: rgba(255, 255, 0, 1)">InputBuffer ib</span>;


protected char[] lineBuffer = null;


// ----------------------------------------------------------- Constructors


public CoyoteReader(InputBuffer ib) {
    <span style="background-color: rgba(255, 255, 0, 1)">super(ib, 1);</span>
    this.ib = ib;
}

}

  再看下它里面的 InputBuffer 对象:

/**
 * The buffer used by Tomcat request. This is a derivative of the Tomcat 3.3
 * OutputBuffer, adapted to handle input instead of output. This allows
 * complete recycling of the facade objects (the ServletInputStream and the
 * BufferedReader).
 *
 * @author Remy Maucherat
 */
public class InputBuffer extends Reader
    implements ByteChunk.ByteInputChannel, ApplicationBufferHandler {
/**
 * The string manager for this package.
 */
protected static final StringManager sm = StringManager.getManager(InputBuffer.class);

private static final Log log = LogFactory.getLog(InputBuffer.class);

public static final int DEFAULT_BUFFER_SIZE = 8 * 1024;

// The buffer can be used for byte[] and char[] reading
// ( this is needed to support ServletInputStream and BufferedReader )
public final int INITIAL_STATE = 0;
public final int CHAR_STATE = 1;
public final int BYTE_STATE = 2;


/**
 * Encoder cache.
 */
private static final ConcurrentMap&lt;Charset, SynchronizedStack&lt;B2CConverter&gt;&gt; encoders = new ConcurrentHashMap&lt;&gt;();

// ----------------------------------------------------- Instance Variables

/**
 * The byte buffer.
 */
private ByteBuffer bb;


/**
 * The char buffer.
 */
private CharBuffer cb;


/**
 * State of the output buffer.
 */
private int state = 0;


/**
 * Flag which indicates if the input buffer is closed.
 */
private boolean closed = false;


/**
 * Encoding to use.
 */
private String enc;


/**
 * Current byte to char converter.
 */
protected B2CConverter conv;


/**
 * Associated Coyote request.
 */
private Request coyoteRequest;


/**
 * Buffer position.
 */
private int markPos = -1;


/**
 * Char buffer limit.
 */
private int readLimit;


/**
 * Buffer size.
 */
private final int size;


// ----------------------------------------------------------- Constructors


/**
 * Default constructor. Allocate the buffer with the default buffer size.
 */
public InputBuffer() {

    this(DEFAULT_BUFFER_SIZE);

}


/**
 * Alternate constructor which allows specifying the initial buffer size.
 *
 * @param size Buffer size to use
 */
public InputBuffer(int size) {

    this.size = size;
    <span style="background-color: rgba(255, 255, 0, 1)">bb = ByteBuffer.allocate(size)</span>;
    clear(bb);
    cb = CharBuffer.allocate(size);
    clear(cb);
    readLimit = size;

}

}

  通过调试发现,当我的请求里没有中文时,CoyoteReader 对象的 InputBuffer 属性的 ByteBuffer 的 bb 属性是能取到请求消息体的,而包含了中文则无法获取,protobuf 直接转换报错了。