background
The production system of The client of Party A requires early warning of safety risks and quick traceability of safety events, so a set of log management specifications should be made.
The system that we access is required to output corresponding logs in strict accordance with the provided format for important scenarios such as user login, registration, and password change.
In the follow-up, log information on our system was collected through fileBeat interconnection.
In simple terms, the application system uniformly prints corresponding logs when processing interface requests.
Problem description
Mature and common unified log printing scheme is to use AOP technology, custom annotation, use Around notification @around on the aspect, intercept requests, get the Controller class method input and output parameters.
However, the interface used in the business scenario was implemented in the following way:
@RequestMapping(value = "/auth", method = { RequestMethod.POST, RequestMethod.GET, RequestMethod.OPTIONS })
public void auth(HttpServletRequest req, HttpServletResponse resp) {
authService.auth(req, resp);
}
Copy the code
Drop the pass parameters directly into HttpServletRequest.
Returns parameters, again using HttpServletResponse output.
public void printResult(HttpServletRequest req, HttpServletResponse resp,
String action, int code, String msg, Object result) {
PrintWriter p = null;
Ret ret = new Ret();
Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss")
.serializeNulls()
.create();
try {
p = resp.getWriter();
ret.setRspCode(code);
ret.setRspDesc(msg);
ret.setData(result);
p.write(gson.toJson(ret));
return;
} catch (Exception e) {
logger.error(e.getMessage());
} finally{ p.flush(); p.close(); }}Copy the code
Unlike usual practice, the specific input and output parameters, with object encapsulation, directly on the method can be.
If we want to intercept the request for passing parameters in advance, we can use methods such as request.getParameter() to get the parameters. However, in a specific interface business process, when using methods such as Request.getParameter (), the parameters passed in cannot be obtained.
Because streams can only be read once.
So the question is: How can Request and Response be read repeatedly?
The solution
Using methods like Request.getParameter (), you end up calling the getInputStream method.
Needs to be rewritten HttpServletRequestWrapper wrapper classes, in the calling getInputStream method, the flow data and written to the cache.
To obtain the parameters, read the cache data directly.
This allows the contents of the Request to be read multiple times.
The implementation code
Encapsulates the request
Custom class ContentCachingRequestWrapper
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
/ * * * * rewrite HttpServletRequestWrapper * *@Author: linzengrui
* @Date: 2021/11/22 15:33 the * /
public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public ContentCachingRequestWrapper(HttpServletRequest request) {
super(request);
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8))){
String line = "";
while((line = reader.readLine()) ! =null) { sb.append(line); }}catch (IOException e) {
e.printStackTrace();
}
body = sb.toString().getBytes(StandardCharsets.UTF_8);
}
@Override
public BufferedReader getReader(a) throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream(a) throws IOException {
final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public boolean isFinished(a) {
return false;
}
@Override
public boolean isReady(a) {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {}@Override
public int read(a) throws IOException {
returninputStream.read(); }}; }public byte[] getBody() {
returnbody; }}Copy the code
Wrap the response
Custom class ContentCachingResponseWrapper
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;
/ * * * * rewrite HttpServletResponseWrapper * *@Author: linzengrui
* @Date: 2021/11/22 19:45 * /
public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream byteArrayOutputStream;
private ServletOutputStream servletOutputStream;
private PrintWriter printWriter;
public ContentCachingResponseWrapper(HttpServletResponse response) {
super(response);
byteArrayOutputStream = new ByteArrayOutputStream();
servletOutputStream = new ServletOutputStream() {
@Override
public boolean isReady(a) {
return false;
}
@Override
public void setWriteListener(WriteListener writeListener) {}@Override
public void write(int b) throws IOException { byteArrayOutputStream.write(b); }}; printWriter =new PrintWriter(byteArrayOutputStream);
}
@Override
public PrintWriter getWriter(a) {
return printWriter;
}
@Override
public ServletOutputStream getOutputStream(a) throws IOException {
return servletOutputStream;
}
public byte[] toByteArray() {
returnbyteArrayOutputStream.toByteArray(); }}Copy the code
Filter Intercepts requests
The interceptor LogFilter uses the wrapper class encapsulated above to get the pass parameter.
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {}@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
/ / to use rewrite HttpServletRequestWrapper custom wrapper classes
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest)request);
/ / to use rewrite HttpServletResponseWrapper custom wrapper classes
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);
// The fetch stream method can only be executed once
try(ServletOutputStream outputStream = response.getOutputStream()){
// Get the pass parameter
String requestParamJson = new String(requestWrapper.getBody());
log.info("requestParamJson --> {}", requestParamJson);
// Specific methods to execute the process
chain.doFilter(requestWrapper, responseWrapper);
// After the fetch stream operation is triggered, data can be fetched from the cache multiple times
String respDataJson = new String(responseWrapper.toByteArray());
log.info("respDataJson <-- {}", respDataJson);
// TODO writes logs
// The content needs to be rewritten, otherwise the stream will have no output
outputStream.write(respDataJson.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
}catch(Exception e){ e.printStackTrace(); }}@Override
public void destroy(a) {}}Copy the code
The SpringBoot boot class adds an annotation @ServletComponentScan
Note: The bootstrap class is annotated @ServletComponentScan to identify the Filter injected above.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@ServletComponentScan
@SpringBootApplication
public class SpringBootApplication {
public static void main(String[] args) { SpringApplication.run(SpringBootApplication.class, args); }}Copy the code
For the specific business interface, the original logic remains unchanged and the input parameters can still be obtained.