반응형
Java, Tomcat, Spring (Spring Boot) 환경에서 Client가 전송한 Request Body를 로깅하고 싶은 경우가 있다. HttpServletRequest 객체의 Stream은 한번 읽히면 Tomcat 내부적으로 다시 읽지 못하도록 보안적으로 막혀있다.
로깅을 위해 Request 객체의 Stream을 받아 Paramter들을 가지고 특정 로직을 구현하고, Controller 단으로 Requset를 넘기면, Data Biding 모듈이 데이터를 바인딩하지 못하고 has already been called for this request 라는 메시지와 함께 Exception이 난다.
이를 해결하기 위해서 대안은 HttpServletRequestWrapper 클래스를 구현하는 것이다. Filter Layer에서 HttpRequest를 HttpServletWrapper 클래스에 넘겨주고 Wrapper 클래스 내부에서는 HttpServletRequest의 InputStream을 복사하여 새 Stream을 만들어 사용하고, HttpServletRequestWrapper클래스를 구현한 객체를 Filter에선 FilterChain객체의 doFilter의 인자로 넘겨준다.
[RequestParameterLoggingWrapper]
@Slf4j
public class RequestParameterLoggingWrapper extends HttpServletRequestWrapper {
private final Charset encoding;
private byte[] rawData;
private Map<String, String[]> params = new HashMap<>();
public RequestParameterLoggingWrapper(HttpServletRequest request) {
super(request);
this.params.putAll(request.getParameterMap());
String charEncoding = request.getCharacterEncoding();
this.encoding = StringUtils.isBlank(charEncoding) ? StandardCharsets.UTF_8 : Charset.forName(charEncoding);
try {
InputStream is = request.getInputStream();
this.rawData = IOUtils.toByteArray(is);
String collect = this.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
if (StringUtils.isEmpty(collect)) {
return;
}
if (request.getContentType() != null && request.getContentType().contains(
ContentType.MULTIPART_FORM_DATA.getMimeType())) {
return;
}
JSONParser jsonParser = new JSONParser();
Object parse = jsonParser.parse(collect);
if (parse instanceof JSONArray) {
JSONArray jsonArray = (JSONArray) jsonParser.parse(collect);
setParameter("requestBody", jsonArray.toJSONString());
} else {
JSONObject jsonObject = (JSONObject) jsonParser.parse(collect);
Iterator iterator = jsonObject.keySet().iterator();
while (iterator.hasNext()) {
String key = (String) iterator.next();
setParameter(key, jsonObject.get(key).toString().replace("\"", "\\\""));
}
}
} catch (Exception e) {
log.error("ReadableRequestWrapper init error", e);
}
}
@Override
public String getParameter(String name) {
String[] paramArray = getParameterValues(name);
if (paramArray != null && paramArray.length > 0) {
return paramArray[0];
} else {
return null;
}
}
@Override
public Map<String, String[]> getParameterMap() {
return Collections.unmodifiableMap(params);
}
@Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(params.keySet());
}
@Override
public String[] getParameterValues(String name) {
String[] result = null;
String[] dummyParamValue = params.get(name);
if (dummyParamValue != null) {
result = new String[dummyParamValue.length];
System.arraycopy(dummyParamValue, 0, result, 0, dummyParamValue.length);
}
return result;
}
private void setParameter(String name, String value) {
String[] param = {value};
setParameter(name, param);
}
private void setParameter(String name, String[] values) {
params.put(name, values);
}
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.rawData);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
public int read() {
return byteArrayInputStream.read();
}
};
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(this.getInputStream(), this.encoding));
}
}
[RequestParameterLoggingWrapperFilter]
@Slf4j
@Service
@Order(2)
public class RequestParameterLoggingWrapperFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.info("ReadableRequestWrapperFilter => Do FilterInternal");
RequestParameterLoggingWrapper wrapper = new RequestParameterLoggingWrapper(request);
filterChain.doFilter(wrapper, response);
}
}
[AOP for RequestLoggingFilter Active]
@Slf4j
@Aspect
@Component
public class LoggerAspect {
private final HttpServletRequest request;
public LoggerAspect(HttpServletRequest request) {
this.request = request;
}
private JSONObject getParams() {
JSONObject jsonObject = new JSONObject();
Enumeration<String> params = request.getParameterNames();
while (params.hasMoreElements()) {
String param = params.nextElement();
String replaceParam = param.replaceAll("\\.", "-");
jsonObject.put(replaceParam, request.getParameter(param));
}
return jsonObject;
}
@Pointcut("execution(* com.*.service.controller.*Controller.*(..))")
public void loggerPointCut() {
}
@Around("loggerPointCut()")
public Object methodLogger(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object result = proceedingJoinPoint.proceed();
String controllerName = proceedingJoinPoint.getSignature().getDeclaringType().getSimpleName();
String methodName = proceedingJoinPoint.getSignature().getName();
Map<String, Object> params = new HashMap<>();
try {
params.put("requestUrl", request.getRequestURI());
params.put("httpMethod", request.getMethod());
params.put("controller", controllerName);
params.put("method", methodName);
params.put("params", getParams());
params.put("time", LocalDateTime.now());
} catch (Exception e) {
log.error("LoggerAspect error", e);
}
log.info("requestLog : {}", params);
return result;
}
}
반응형
'Programming > Spring' 카테고리의 다른 글
[Spring Boot] Filter (1) - Request Body Modify (0) | 2020.07.15 |
---|---|
[SpringBoot] Dockerizing a Spring Boot Application (0) | 2019.11.19 |
[Spring Boot] Spring Security with REST API Login AS JSON (8) | 2019.10.23 |
[SpringBoot] Redis Channel Subscribe with MessagePack (0) | 2019.09.18 |
[SpringBoot] Redis Publish Channel Subscribe (0) | 2019.09.18 |