I've implemented a asynchronous Servlet, which needs to parse the body of request and store the parsed result in cache. Should I implement the parseBody() function in Servlet or implement a new class, which will do the parsing? What is the best practice?
Here is my current code snippet:
public class DocFeedServlet extends FeedServlet {
private static final Logger LOGGER = LoggerFactory.getLogger(DocFeedServlet.class);
private static final ObjectMapper OBJECTMAPPER = new ObjectMapper();
public void init(ServletConfig config) throws ServletException {
super.init(config);
}
#Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
}
#Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
final AsyncContext asyncContext = req.startAsync();
asyncContext.start(new Runnable() {
#Override
public void run() {
String bodyStr = getBody(req);
if (bodyStr.isEmpty()) {
resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
asyncContext.complete();
return;
}
int ignoreTime = Integer.parseInt(req.getParameter(Constant.PARAM_IGNORE_TIME));
List<MockDocCacheKeyVal> mockDocCacheKeyVals = new ArrayList<>();
List<String> docUpdateFields = new ArrayList<>();
List<List<String>> docKeepFields = new ArrayList<List<String>>();
List<String> uuidsToRemove = new ArrayList<>();
int parseRet = parseBody(bodyStr, mockDocCacheKeyVals, docUpdateFields, docKeepFields, uuidsToRemove, ignoreTime);
if (parseRet != 0) {
resp.setStatus(HttpServletResponse.SC_OK);
} else {
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
asyncContext.complete();
}
});
}
protected int parseBody(String body, List<MockDocCacheKeyVal> mockDocCacheKeyVals, List<String> docUpdateFields, List<List<String>> docKeepFields, List<String> uuidsToRemove, int ignoreTime) {
try {
ObjectReader reader = OBJECTMAPPER.reader(new TypeReference<List<Document>>() { });
List<Document> documents = reader.readValue(body);
for (Document doc : documents) {
if (doc.getAction() != null && doc.getAction().equalsIgnoreCase(Constant.DOC_FEED_ACTION_DELETE)) {
if (doc.getUuid() != null) {
uuidsToRemove.add(doc.getUuid());
}
continue;
}
if (doc.getA() != null) {
} else if (doc.getB() != null) {
} else {
DocumentUtils.pruneWeightSet(doc.getC(), cPruneSize);
DocumentUtils.pruneWeightSet(doc.getD(), dPruneSize);
DocumentUtils.pruneWeightSet(doc.getE(), ePruneSize);
}
}
return documents.size();
} catch (Exception e) {
LOGGER.error(e.getMessage());
}
return 0;
}
}
Thanks.
Asynchronous Request Body read is accomplished with the HttpServletRequest.getInputStream().setReadListener(ReadListener) concepts introduced in Servlet 3.1
You will only read based on events from your ReadListener, and you will only read enough to not block. (so no reading multi-megabyte buffers!).
This API is what you are looking for, however there be land mines here, so be sure you fully understand the API before you finish it.
Related
I have this code:
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
logger.info("Filter start...");
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String ba = getBaId(getBody(httpRequest));
if (ba == null) {
logger.error("Wrong XML");
httpResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST);
} else {
if (!clients.containsKey(ba)) {
clients.put(ba, 1);
logger.info("Client map : init...");
} else {
clients.put(ba, clients.get(ba).intValue() + 1);
logger.info("Threads for " + ba + " = " + clients.get(ba).toString());
}
chain.doFilter(request, response);
}
}
and this web.xml (packages are shortened and names changed, but it looks the same)
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app>
<filter>
<filter-name>TestFilter</filter-name>
<filter-class>pkg.TestFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>TestFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>WEB-INF/applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>Name</servlet-name>
<display-name>Name</display-name>
<servlet-class>pkg.Name</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Name</servlet-name>
<url-pattern>/services/*</url-pattern>
</servlet-mapping>
</web-app>
I want to invoke the Servlet after the Filter. I was hoping chain.doFilter(...) could do the trick, but i always get this error on the line with chain.doFilter(...):
java.lang.IllegalStateException: getInputStream() can't be called after getReader()
at com.caucho.server.connection.AbstractHttpRequest.getInputStream(AbstractHttpRequest.java:1933)
at org.apache.cxf.transport.http.AbstractHTTPDestination.setupMessage(AbstractHTTPDestination.java:249)
at org.apache.cxf.transport.servlet.ServletDestination.invoke(ServletDestination.java:82)
at org.apache.cxf.transport.servlet.ServletController.invokeDestination(ServletController.java:283)
at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:166)
at org.apache.cxf.transport.servlet.AbstractCXFServlet.invoke(AbstractCXFServlet.java:174)
at org.apache.cxf.transport.servlet.AbstractCXFServlet.doPost(AbstractCXFServlet.java:152)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:153)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:91)
at com.caucho.server.dispatch.ServletFilterChain.doFilter(ServletFilterChain.java:103)
at pkg.TestFilter.doFilter(TestFilter.java:102)
at com.caucho.server.dispatch.FilterFilterChain.doFilter(FilterFilterChain.java:87)
at com.caucho.server.webapp.WebAppFilterChain.doFilter(WebAppFilterChain.java:187)
at com.caucho.server.dispatch.ServletInvocation.service(ServletInvocation.java:265)
at com.caucho.server.http.HttpRequest.handleRequest(HttpRequest.java:273)
at com.caucho.server.port.TcpConnection.run(TcpConnection.java:682)
at com.caucho.util.ThreadPool$Item.runTasks(ThreadPool.java:743)
at com.caucho.util.ThreadPool$Item.run(ThreadPool.java:662)
at java.lang.Thread.run(Thread.java:619)
Working code based on the accepted answer.
public class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
private static final Logger logger = Logger.getLogger(CustomHttpServletRequestWrapper.class);
private final String body;
public CustomHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
try {
InputStream inputStream = request.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
char[] charBuffer = new char[128];
int bytesRead = -1;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
} else {
stringBuilder.append("");
}
} catch (IOException ex) {
logger.error("Error reading the request body...");
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException ex) {
logger.error("Error closing bufferedReader...");
}
}
}
body = stringBuilder.toString();
}
#Override
public ServletInputStream getInputStream () throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
ServletInputStream inputStream = new ServletInputStream() {
public int read () throws IOException {
return byteArrayInputStream.read();
}
};
return inputStream;
}
}
You probably start consuming the HttpServletRequest using getReader() in :
String ba = getBaId(getBody(httpRequest));
Your servlet tries to call getInputStream() on the same request, which is not allowed. What you need to do is use a ServletRequestWrapper to make a copy of the body of the request, so you can read it with multiple methods. I dont have the time to find a complete example right know ... sorry ...
This worked for me. It implements getInputStream.
private class MyHttpServletRequestWrapper extends HttpServletRequestWrapper {
private byte[] body;
public MyHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
try {
body = IOUtils.toByteArray(request.getInputStream());
} catch (IOException ex) {
body = new byte[0];
}
}
#Override
public ServletInputStream getInputStream() throws IOException {
return new ServletInputStream() {
ByteArrayInputStream bais = new ByteArrayInputStream(body);
#Override
public int read() throws IOException {
return bais.read();
}
};
}
}
Then you use in your method:
//copy body
servletRequest = new MyHttpServletRequestWrapper(servletRequest);
For Servlet 3.1
class MyHttpServletRequestWrapper extends HttpServletRequestWrapper {
private byte[] body;
public MyHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
try {
body = IOUtils.toByteArray(request.getInputStream());
} catch (IOException ex) {
body = new byte[0];
}
}
#Override
public ServletInputStream getInputStream() throws IOException {
return new DelegatingServletInputStream(new ByteArrayInputStream(body));
}
}
public class DelegatingServletInputStream extends ServletInputStream {
private final InputStream sourceStream;
private boolean finished = false;
/**
* Create a DelegatingServletInputStream for the given source stream.
*
* #param sourceStream the source stream (never {#code null})
*/
public DelegatingServletInputStream(InputStream sourceStream) {
this.sourceStream = sourceStream;
}
/**
* Return the underlying source stream (never {#code null}).
*/
public final InputStream getSourceStream() {
return this.sourceStream;
}
#Override
public int read() throws IOException {
int data = this.sourceStream.read();
if (data == -1) {
this.finished = true;
}
return data;
}
#Override
public int available() throws IOException {
return this.sourceStream.available();
}
#Override
public void close() throws IOException {
super.close();
this.sourceStream.close();
}
#Override
public boolean isFinished() {
return this.finished;
}
#Override
public boolean isReady() {
return true;
}
#Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
}
inputStream in servlet request can only be used once because of it is stream,you can store it and then get it from a byte array,this can resolve.
public class HttpServletRequestWrapper extends javax.servlet.http.HttpServletRequestWrapper {
private final byte[] body;
public HttpServletRequestWrapper(HttpServletRequest request)
throws IOException {
super(request);
body = StreamUtil.readBytes(request.getReader(), "UTF-8");
}
#Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
#Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
return new ServletInputStream() {
#Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
#Override
public boolean isFinished() {
return false;
}
#Override
public boolean isReady() {
return false;
}
#Override
public void setReadListener(ReadListener arg0) {
}
};
}
}
in filter:
ServletRequest requestWrapper = new HttpServletRequestWrapper(request);
request.getInputStream() is allowed to read only one time. In order to use this method many times, we need to do extra the custom task to HttpServletReqeustWrapper class. see my sample wrapper class below.
public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
private ByteArrayOutputStream cachedBytes;
public MultiReadHttpServletRequest(HttpServletRequest request) {
super(request);
}
#Override
public ServletInputStream getInputStream() throws IOException {
if (cachedBytes == null)
cacheInputStream();
return new CachedServletInputStream();
}
#Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
private void cacheInputStream() throws IOException {
/*
* Cache the inputstream in order to read it multiple times. For convenience, I use apache.commons IOUtils
*/
cachedBytes = new ByteArrayOutputStream();
IOUtils.copy(super.getInputStream(), cachedBytes);
}
/* An inputstream which reads the cached request body */
public class CachedServletInputStream extends ServletInputStream {
private ByteArrayInputStream input;
public CachedServletInputStream() {
/* create a new input stream from the cached request body */
input = new ByteArrayInputStream(cachedBytes.toByteArray());
}
#Override
public int read() throws IOException {
return input.read();
}
}
}
In my case, I trace all incoming requests into the log. I created a Filter
public class TracerRequestFilter implements Filter {
private static final Logger LOG = LoggerFactory.getLogger(TracerRequestFilter.class);
#Override
public void destroy() {
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
ServletException {
final HttpServletRequest req = (HttpServletRequest) request;
try {
if (LOG.isDebugEnabled()) {
final MultiReadHttpServletRequest wrappedRequest = new MultiReadHttpServletRequest(req);
// debug payload info
logPayLoad(wrappedRequest);
chain.doFilter(wrappedRequest, response);
} else {
chain.doFilter(request, response);
}
} finally {
LOG.info("end-of-process");
}
}
private String getRemoteAddress(HttpServletRequest req) {
String ipAddress = req.getHeader("X-FORWARDED-FOR");
if (ipAddress == null) {
ipAddress = req.getRemoteAddr();
}
return ipAddress;
}
private void logPayLoad(HttpServletRequest request) {
final StringBuilder params = new StringBuilder();
final String method = request.getMethod().toUpperCase();
final String ipAddress = getRemoteAddress(request);
final String userAgent = request.getHeader("User-Agent");
LOG.debug(String.format("============debug request=========="));
LOG.debug(String.format("Access from ip:%s;ua:%s", ipAddress, userAgent));
LOG.debug(String.format("Method : %s requestUri %s", method, request.getRequestURI()));
params.append("Query Params:").append(System.lineSeparator());
Enumeration<String> parameterNames = request.getParameterNames();
for (; parameterNames.hasMoreElements();) {
String paramName = parameterNames.nextElement();
String paramValue = request.getParameter(paramName);
if ("password".equalsIgnoreCase(paramName) || "pwd".equalsIgnoreCase(paramName)) {
paramValue = "*****";
}
params.append("---->").append(paramName).append(": ").append(paramValue).append(System.lineSeparator());
}
LOG.debug(params.toString());
/** request body */
if ("POST".equals(method) || "PUT".equals(method)) {
try {
LOG.debug(IOUtils.toString(request.getInputStream()));
} catch (IOException e) {
LOG.error(e.getMessage(), e);
}
}
LOG.debug(String.format("============End-debug-request=========="));
}
#Override
public void init(FilterConfig arg0) throws ServletException {
}
}
It works for me both Servlet 2.5 and 3.0. I see all request params both form-encoded and request json body.
This question already has answers here:
How to use java.net.URLConnection to fire and handle HTTP requests
(12 answers)
Making http calls from swing application to a servlet, session not saved
(1 answer)
Closed 1 year ago.
Server-side I have an HttpSession object. Each time the client starts the connection to the Servlet, the session changes.
Here I have a simplified version of my Servlet code:
//import ...
#WebServlet(name = "ServletController", urlPatterns = {"/ServletController"})
public class ServletController extends HttpServlet {
public void init(ServletConfig conf) {
//...
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//...
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/plain");
HttpSession s = request.getSession();
PrintWriter out = response.getWriter();
try {
String action = request.getParameter("action");
switch (action) {
case "login":
s.setAttribute("account", "John");
out.println("Logged in successfully. Session: " + s);
out.flush();
break;
case "account":
String account = (String) s.getAttribute("account");
out.println(account + ". Session: " + s);
out.flush();
break;
default:
break;
}
} catch (Exception x) {
System.out.println(x);
}
}
}
And here the simplified Android one:
//import ...
public class Operation {
public static Executor e = Executors.newSingleThreadExecutor();
public static void main(String[] args) {
Button login_btn = findViewById(R.id.login);
Button account_btn = findViewById(R.id.account);
login_btn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
e.execute(() -> {
String login = Operation.operation("?action=login");
});
}
});
account_btn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
e.execute(() -> {
String account = Operation.operation("?action=account");
});
}
});
System.out.println(login);
System.out.println(account);
}
public static String operation(String urlParameters) {
HttpURLConnection conn = null;
try {
System.out.println(urlParameters);
URL url = new URL("http://10.0.2.2:8080/progettoTweb/ServletController" + urlParameters);
conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(1000);
conn.setConnectTimeout(1500);
conn.setRequestMethod("GET");
conn.setDoInput(true);
conn.connect();
int response = conn.getResponseCode();
return readIt(conn.getInputStream());
} catch (Exception ex) {
System.out.println(ex);
return null;
} finally {
if (conn != null) {
conn.disconnect();
}
}
}
//building the output as a String
private static String readIt(InputStream stream) throws IOException, UnsupportedEncodingException {
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
String line;
StringBuilder result = new StringBuilder();
while ((line = reader.readLine()) != null) {
result.append(line).append("\n");
}
return result.toString();
}
}
As the System.out.println in the Android app show, I obtain a different session for each Operation.operation call I make.
In the original code I use SharedPreferences in order to save my data, but it does not solve the problem since I do not know how to use the session, gained from the interaction with the server-side, to obtain the required values.
Indeed, in the Servlet code I use s.getAttribute() but, since it creates a new HttpSession object each time, It cannot give back the requested values.
I need to make about 60 HTTP requests.
In the first case, I did not use an asynchronous request and the speed was about 1.5 minutes.
In the second case, I used an asynchronous request and the speed did not change either and was about 1.5 minutes.
Please see my code. Maybe I'm not doing the asynchronous request correctly or is there some other way to quickly make a lot of HTTP requests?
public class Main {
public static int page = 0;
public static void main(String[] args) throws IOException {
int totalPages = Util.getTotalPages();
page = 0;
while(page < totalPages) {
// Function does not work
new GetAuctions();
page++;
}
}
}
public class Util {
public static final String API_KEY = "***";
public static final OkHttpClient httpClient = new OkHttpClient();
public static final List<JSONObject> auctions = new ArrayList<>();
public static int getTotalPages() throws IOException {
Request request = new Request.Builder().url("https://api.hypixel.net/skyblock/auctions?key=" + Util.API_KEY + "&page=0").build();
Response response = httpClient.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Error: " + response);
assert response.body() != null;
String jsonData = response.body().string();
JSONObject object = new JSONObject(jsonData);
return object.getInt("totalPages");
}
}
public class GetAuctions {
public static void main(String[] args) throws Exception {
new GetAuctions().run();
}
public void run() throws Exception {
Request request = new Request.Builder().url("https://api.hypixel.net/skyblock/auctions?key=" + Util.API_KEY + "&page=" + Main.page).build();
Util.httpClient.newCall(request).enqueue(new Callback() {
#Override public void onFailure(#NotNull Call call, #NotNull IOException e) {
e.printStackTrace();
}
#Override public void onResponse(#NotNull Call call, #NotNull Response response) throws IOException {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
assert response.body() != null;
String jsonData = response.body().string();
JSONObject object = new JSONObject(jsonData);
JSONArray array = object.getJSONArray("auctions");
for (int i=0; i<array.length(); i++) {
JSONObject jsonObject = array.getJSONObject(i);
if (jsonObject.has("bin")) {
Util.auctions.add(jsonObject);
}
}
System.out.println(Util.auctions.size());
}
});
}
}
It doesn't look like your example is asynchronous at all. Look at the example from https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/AsynchronousGet.java and try with that.
Specifically you should be calling enqueue instead of execute.
client.newCall(request).enqueue(new Callback() {
#Override public void onFailure(Call call, IOException e) {
e.printStackTrace();
}
#Override public void onResponse(Call call, Response response) throws IOException {
try (ResponseBody responseBody = response.body()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
Headers responseHeaders = response.headers();
for (int i = 0, size = responseHeaders.size(); i < size; i++) {
System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
}
System.out.println(responseBody.string());
}
}
});
}
I would like to get the XML data from request and response and use it into Rest controller. I tried this:
#RestController()
public class HomeController {
#PostMapping(value = "/v1")
public Response handleMessage(#RequestBody Transaction transaction, HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest request, HttpServletResponse response
System.out.println("!!!!!!! InputStream");
System.out.println(request.getInputStream());
System.out.println(response.getOutputStream());
InputStream in = request.getInputStream();
String readLine;
BufferedReader br = new BufferedReader(new InputStreamReader(in));
while (((readLine = br.readLine()) != null)) {
System.out.println(readLine);
}
}
}
But I get java.io.IOException: UT010029: Stream is closed
What is the proper way to get the content into String variable?
EDIT: I also tried solution with Filter but I'm not aware how to use the request payload into rest controller:
Read request payload:
#Component
public class HttpLoggingFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(HttpLoggingFilter.class);
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
ResettableStreamHttpServletRequest wrappedRequest = new ResettableStreamHttpServletRequest((HttpServletRequest) request);
wrappedRequest.getInputStream().read();
String body = IOUtils.toString(wrappedRequest.getReader());
System.out.println("!!!!!!!!!!!!!!!!!! " + body);
wrappedRequest.resetInputStream();
chain.doFilter(request, response);
}
public class ResettableStreamHttpServletRequest extends HttpServletRequestWrapper {
private byte[] rawData;
private HttpServletRequest request;
private ResettableServletInputStream servletStream;
ResettableStreamHttpServletRequest(HttpServletRequest request) {
super(request);
this.request = request;
this.servletStream = new ResettableServletInputStream();
}
void resetInputStream() {
servletStream.stream = new ByteArrayInputStream(rawData);
}
#Override
public ServletInputStream getInputStream() throws IOException {
if (rawData == null) {
rawData = IOUtils.toByteArray(this.request.getInputStream());
servletStream.stream = new ByteArrayInputStream(rawData);
}
return servletStream;
}
#Override
public BufferedReader getReader() throws IOException {
if (rawData == null) {
rawData = IOUtils.toByteArray(this.request.getInputStream());
servletStream.stream = new ByteArrayInputStream(rawData);
}
String encoding = getCharacterEncoding();
if (encoding != null) {
return new BufferedReader(new InputStreamReader(servletStream, encoding));
} else {
return new BufferedReader(new InputStreamReader(servletStream));
}
}
private class ResettableServletInputStream extends ServletInputStream {
private InputStream stream;
#Override
public int read() throws IOException {
return stream.read();
}
#Override
public boolean isFinished() {
// TODO Auto-generated method stub
return false;
}
#Override
public boolean isReady() {
// TODO Auto-generated method stub
return false;
}
#Override
public void setReadListener(ReadListener readListener) {
// TODO Auto-generated method stub
}
}
}
}
Rest endpoint:
#RestController()
public class HomeController {
#PostMapping(value = "/v1")
public Response handleMessage(#RequestBody Transaction transaction, HttpServletRequest request, org.zalando.logbook.HttpRequest requestv, HttpServletResponse response) throws Exception {
// Get here request and response and log it into DB
}
}
How I can call HttpLoggingFilter into the Java method handleMessage and get the request as body String? Probably I can make it service and Inject it? Can you give me some advice how I can assess the code?
Here are a bunch of classes to do it. This is a once a OncePerRequestFilter implementation, check here https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/filter/OncePerRequestFilter.html. Basically the problem is that in the chain filter, the request stream and response stream can be read just once. So, need to wrap these 2 streams inside something that can be read more than once.
In the first 2 lines I wrapped request and response inside requestToUse and responseToUse. ResettableStreamHttpServletRequest and ResettableStreamHttpServletResponse are wrapper classes that keeps the original string body inside of them, and every time the stream is needed they return a new stream.Then from there, you forget about request and response and start using requestToUse and responseToUse.
I took this from an old project I did. Actually there are more clases, but I extracted the main parts for you. This may not compile right away. Give it a try and let me know and I will help you to make it work.
public class RequestResponseLoggingFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//here you wrap the request and response into some resetable istream class
HttpServletRequest requestToUse = new ResettableStreamHttpServletRequest(request);
HttpServletResponse responseToUse = new ResettableStreamHttpServletResponse(response);
//you read the request to log it
byte[] payload = IOUtils.toByteArray(requestToUse.getReader(), requestToUse.getCharacterEncoding());
String body = new String(payload, requestToUse.getCharacterEncoding());
//here you log the body request
log.(body);
//let the chain continue
filterChain.doFilter(requestToUse, responseToUse);
// Here we log the response
String response = new String(responseToUse.toString().getBytes(), responseToUse.getCharacterEncoding());
//since you can read the stream just once, you will need it again for chain to be able to continue, so you reset it
ResettableStreamHttpServletResponse responseWrapper = WebUtils.getNativeResponse(responseToUse, ResettableStreamHttpServletResponse.class);
if (responseWrapper != null) {
responseWrapper.copyBodyToResponse(true);
}
}
}
public class ResettableStreamHttpServletRequest extends HttpServletRequestWrapper {
private byte[] rawData;
private ResettableServletInputStream servletStream;
public ResettableStreamHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
rawData = IOUtils.toByteArray(request.getInputStream());
servletStream = new ResettableServletInputStream();
servletStream.setStream(new ByteArrayInputStream(rawData));
}
#Override
public ServletInputStream getInputStream() throws IOException {
servletStream.setStream(new ByteArrayInputStream(rawData));
return servletStream;
}
#Override
public BufferedReader getReader() throws IOException {
servletStream.setStream(new ByteArrayInputStream(rawData));
return new BufferedReader(new InputStreamReader(servletStream));
}
}
public class ResettableStreamHttpServletResponse extends HttpServletResponseWrapper {
private ByteArrayServletOutputStream byteArrayServletOutputStream = new ByteArrayServletOutputStream();
public ResettableStreamHttpServletResponse(HttpServletResponse response) throws IOException {
super(response);
}
/**
* Copy the cached body content to the response.
*
* #param complete whether to set a corresponding content length for the complete cached body content
* #since 4.2
*/
public void copyBodyToResponse(boolean complete) throws IOException {
byte[] array = byteArrayServletOutputStream.toByteArray();
if (array.length > 0) {
HttpServletResponse rawResponse = (HttpServletResponse) getResponse();
if (complete && !rawResponse.isCommitted()) {
rawResponse.setContentLength(array.length);
}
rawResponse.getOutputStream().write(byteArrayServletOutputStream.toByteArray());
if (complete) {
super.flushBuffer();
}
}
}
/**
* The default behavior of this method is to return getOutputStream() on the wrapped response object.
*/
#Override
public ServletOutputStream getOutputStream() throws IOException {
return byteArrayServletOutputStream;
}
/**
* The default behavior of this method is to return getOutputStream() on the wrapped response object.
*/
#Override
public String toString() {
String response = new String(byteArrayServletOutputStream.toByteArray());
return response;
}
}
You dont need to do anything special here, Spring framework will do it for you.
All you need is:
Create a Pojo or Bean which represents your XML data.
Add xml data format dependency to Gradle/Maven which will bind the request xml to your pojo.
compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.9.9'
Tell your request handler to accept XML like this:
#RequestMapping(value = "/xmlexample", method = RequestMethod.POST,consumes = "application/xml;charset=UTF-8")
public final boolean transactionHandler(#Valid #RequestBody Transaction transaction) {
log.debug("Received transaction request with data {}", transaction);
return true;
}
And voila, you will have your transaction bean populated with your XML data.
I am trying to intercept and log all the request-responses. To make requests i am using RestTemplate.exchange().
When i make a GET request and get an 4** error i can call the ClientHttpResponse.getBody() and can access the response body but for PUT and POST requests ClientHttpResponse.getBody() method throws an exception.
What might be causing this and how can i get the response body for POST and PUT requests as well?
This is where i make the request:
apiResponse = restTemplate.exchange(url, vCloudRequest.getHttpMethod(), entity, responseType);
This is the part of the interceptor that gets the exception:
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
ClientHttpResponse response = execution.execute(request, body);
String requestString = new String(body);
String responseString = new
// Below line throws exception
String(ByteStreams.toByteArray(response.getBody()), Charset.forName("UTF-8"));
This is the stack.
Caused by: java.io.IOException: Server returned HTTP response code: 403 for URL: https://176.235.57.11/api/admin/org/bd154aaf-2e7c-446d-91be-f0a45138476b/users
at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1876)
at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1474)
at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:254)
at org.springframework.http.client.SimpleClientHttpResponse.getBody(SimpleClientHttpResponse.java:85)
at org.springframework.http.client.BufferingClientHttpResponseWrapper.getBody(BufferingClientHttpResponseWrapper.java:69)
at roma.api_utils.model.Interceptors.RequestLoggingInterceptor.intercept(RequestLoggingInterceptor.java:39)
at org.springframework.http.client.InterceptingClientHttpRequest$InterceptingRequestExecution.execute(InterceptingClientHttpRequest.java:86)
at org.springframework.http.client.InterceptingClientHttpRequest.executeInternal(InterceptingClientHttpRequest.java:70)
at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48)
at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:53)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:652)
Update :
When i call response.getStatusCode() before calling response.getBody() it doesn't throw IOException.
Basic knowledge:
HttpURLConnection has two similar fields, errorStream and inputStream. When we invoke its getInputSteam method, it checks whether the response has an error code. If so, it throws an IOException and records it- that's why you got the exception. Furthermore, it also copies the contents in inputStream to errorStream, thus we can get its response body by invoking its getErrorStream method. This is exactly what SimpleClientHttpResponse does with its getBody method:
#Override
public InputStream getBody() throws IOException {
InputStream errorStream = this.connection.getErrorStream();
this.responseStream =
(errorStream != null ? errorStream : this.connection.getInputStream());
return this.responseStream;
}
It first checks if errorStream is not null. If true, it returns it. If false, it calls connection.getInputStream() and returns that.
Now here are the answers
Why does calling response.getBody() not throw an IOException after you called response.getStatusCode()? It is because getStatusCode calls getInputStream internally. Thus, errorStream will be not null when getBody is called.
Why does it not throw an exception when the http method is GET?
See method org.springframework.http.client.SimpleBufferingClientHttpRequest#executeInternal.
.
#Override
protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput)
throws IOException {
addHeaders(this.connection, headers);
// JDK <1.8 doesn't support getOutputStream with HTTP DELETE
if (HttpMethod.DELETE == getMethod() && bufferedOutput.length == 0) {
this.connection.setDoOutput(false);
}
if (this.connection.getDoOutput() && this.outputStreaming) {
this.connection.setFixedLengthStreamingMode(bufferedOutput.length);
}
this.connection.connect();
if (this.connection.getDoOutput()) {
FileCopyUtils.copy(bufferedOutput, this.connection.getOutputStream());
}
else {
// Immediately trigger the request in a no-output scenario as well
this.connection.getResponseCode();
}
return new SimpleClientHttpResponse(this.connection);
}
It eagerly executes this.connection.getResponseCode(); when the http method is GET.
I had a similar requirement of logging every request and response. I wrote a filter and hooked into the filter chain.
The code looks like something below:
public class CustomRequestFilter implements Filter {
#Override
public void init(FilterConfig filterConfig) throws ServletException {
//No custom initialisation required
}
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
try {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
Map<String, String> requestMap = this
.getTypesafeRequestMap(httpServletRequest);
BufferedRequestWrapper bufferedRequest = new BufferedRequestWrapper(
httpServletRequest);
BufferedResponseWrapper bufferedResponse = new BufferedResponseWrapper(
httpServletResponse);
final StringBuilder logMessage = new StringBuilder(
"REST Request - ").append("[HTTP METHOD:")
.append(httpServletRequest.getMethod())
.append("] [PATH INFO:")
.append(httpServletRequest.getServletPath())
.append("] [REQUEST PARAMETERS:").append(requestMap)
.append("] [REQUEST BODY:")
.append(bufferedRequest.getRequestBody())
.append("] [REMOTE ADDRESS:")
.append(httpServletRequest.getRemoteAddr()).append("]");
log.info("=======================REQUEST PAYLOAD=================================");
log.info(bufferedRequest.getRequestBody());
log.info("========================================================");
filterChain.doFilter(bufferedRequest, bufferedResponse);
logMessage.append(" [RESPONSE:")
.append(bufferedResponse.getContent()).append("]");
log.info("=======================REST RESPONSE=================================");
log.info(bufferedResponse.getContent());
log.info("========================================================");
} catch (Exception a) {
log.error("Error while filtering ", a);
}
}
private Map<String, String> getTypesafeRequestMap(HttpServletRequest request) {
Map<String, String> typesafeRequestMap = new HashMap<>();
Enumeration<?> requestParamNames = request.getParameterNames();
while (requestParamNames.hasMoreElements()) {
String requestParamName = (String) requestParamNames.nextElement();
String requestParamValue;
if ("password".equalsIgnoreCase(requestParamName)) {
requestParamValue = "********";
} else {
requestParamValue = request.getParameter(requestParamName);
}
typesafeRequestMap.put(requestParamName, requestParamValue);
}
return typesafeRequestMap;
}
#Override
public void destroy() {
//not yet implemented
}
private static final class BufferedRequestWrapper extends
HttpServletRequestWrapper {
private ByteArrayInputStream bais = null;
private ByteArrayOutputStream baos = null;
private BufferedServletInputStream bsis = null;
private byte[] buffer = null;
public BufferedRequestWrapper(HttpServletRequest req)
throws IOException {
super(req);
// Read InputStream and store its content in a buffer.
InputStream is = req.getInputStream();
this.baos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int read;
while ((read = is.read(buf)) > 0) {
this.baos.write(buf, 0, read);
}
this.buffer = this.baos.toByteArray();
}
#Override
public ServletInputStream getInputStream() {
this.bais = new ByteArrayInputStream(this.buffer);
this.bsis = new BufferedServletInputStream(this.bais);
return this.bsis;
}
String getRequestBody() throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(
this.getInputStream()));
String line;
StringBuilder inputBuffer = new StringBuilder();
do {
line = reader.readLine();
if (null != line) {
inputBuffer.append(line.trim());
}
} while (line != null);
reader.close();
return inputBuffer.toString().trim();
}
}
private static final class BufferedServletInputStream extends
ServletInputStream {
private ByteArrayInputStream bais;
public BufferedServletInputStream(ByteArrayInputStream bais) {
this.bais = bais;
}
#Override
public int available() {
return this.bais.available();
}
#Override
public int read() {
return this.bais.read();
}
#Override
public int read(byte[] buf, int off, int len) {
return this.bais.read(buf, off, len);
}
#Override
public boolean isFinished() {
return false;
}
#Override
public boolean isReady() {
return true;
}
#Override
public void setReadListener(ReadListener readListener) {
//No specific readListener changes required
}
}
public class TeeServletOutputStream extends ServletOutputStream {
private final TeeOutputStream targetStream;
public TeeServletOutputStream(OutputStream one, OutputStream two) {
targetStream = new TeeOutputStream(one, two);
}
#Override
public void write(int arg0) throws IOException {
this.targetStream.write(arg0);
}
#Override
public void flush() throws IOException {
super.flush();
this.targetStream.flush();
}
#Override
public void close() throws IOException {
super.close();
this.targetStream.close();
}
#Override
public boolean isReady() {
return false;
}
#Override
public void setWriteListener(WriteListener writeListener) {
//not yet implemented
}
}
public class BufferedResponseWrapper implements HttpServletResponse {
HttpServletResponse original;
TeeServletOutputStream tee;
ByteArrayOutputStream bos;
public BufferedResponseWrapper(HttpServletResponse response) {
original = response;
}
public String getContent() {
return bos.toString();
}
#Override
public PrintWriter getWriter() throws IOException {
return original.getWriter();
}
#Override
public ServletOutputStream getOutputStream() throws IOException {
if (tee == null) {
bos = new ByteArrayOutputStream();
tee = new TeeServletOutputStream(original.getOutputStream(),
bos);
}
return tee;
}
#Override
public String getCharacterEncoding() {
return original.getCharacterEncoding();
}
#Override
public String getContentType() {
return original.getContentType();
}
#Override
public void setCharacterEncoding(String charset) {
original.setCharacterEncoding(charset);
}
#Override
public void setContentLength(int len) {
original.setContentLength(len);
}
#Override
public void setContentLengthLong(long l) {
original.setContentLengthLong(l);
}
#Override
public void setContentType(String type) {
original.setContentType(type);
}
#Override
public void setBufferSize(int size) {
original.setBufferSize(size);
}
#Override
public int getBufferSize() {
return original.getBufferSize();
}
#Override
public void flushBuffer() throws IOException {
tee.flush();
}
#Override
public void resetBuffer() {
original.resetBuffer();
}
#Override
public boolean isCommitted() {
return original.isCommitted();
}
#Override
public void reset() {
original.reset();
}
#Override
public void setLocale(Locale loc) {
original.setLocale(loc);
}
#Override
public Locale getLocale() {
return original.getLocale();
}
#Override
public void addCookie(Cookie cookie) {
original.addCookie(cookie);
}
#Override
public boolean containsHeader(String name) {
return original.containsHeader(name);
}
#Override
public String encodeURL(String url) {
return original.encodeURL(url);
}
#Override
public String encodeRedirectURL(String url) {
return original.encodeRedirectURL(url);
}
#SuppressWarnings("deprecation")
#Override
public String encodeUrl(String url) {
return original.encodeUrl(url);
}
#SuppressWarnings("deprecation")
#Override
public String encodeRedirectUrl(String url) {
return original.encodeRedirectUrl(url);
}
#Override
public void sendError(int sc, String msg) throws IOException {
original.sendError(sc, msg);
}
#Override
public void sendError(int sc) throws IOException {
original.sendError(sc);
}
#Override
public void sendRedirect(String location) throws IOException {
original.sendRedirect(location);
}
#Override
public void setDateHeader(String name, long date) {
original.setDateHeader(name, date);
}
#Override
public void addDateHeader(String name, long date) {
original.addDateHeader(name, date);
}
#Override
public void setHeader(String name, String value) {
original.setHeader(name, value);
}
#Override
public void addHeader(String name, String value) {
original.addHeader(name, value);
}
#Override
public void setIntHeader(String name, int value) {
original.setIntHeader(name, value);
}
#Override
public void addIntHeader(String name, int value) {
original.addIntHeader(name, value);
}
#Override
public void setStatus(int sc) {
original.setStatus(sc);
}
#SuppressWarnings("deprecation")
#Override
public void setStatus(int sc, String sm) {
original.setStatus(sc, sm);
}
#Override
public String getHeader(String arg0) {
return original.getHeader(arg0);
}
#Override
public Collection<String> getHeaderNames() {
return original.getHeaderNames();
}
#Override
public Collection<String> getHeaders(String arg0) {
return original.getHeaders(arg0);
}
#Override
public int getStatus() {
return original.getStatus();
}
}
}
For PUT and POST, it depends on your target resource. If your target resource do not add anything in the response's body after a PUT or POST request, it's normal to get an exception. In general, you know the resource that you send with PUT or POST so you can just check the response's status to know if your resource had been created or modified. You do not need to check the response body again.
You can use the following to accesss the response body in the interceptor. I did a quick unit test to confirm it works even on a POST with a 403 response.
However be careful, getBody returns an InputStream. Which means you can only read it once. You won't be able to read the same stream again outside the interceptor unless you provide a new response with a new body.
...
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
final ClientHttpResponse response = execution.execute(request, body);
final InputStream body = response.getBody();
return response;
}
...