Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] refactor: RequestWrappingFilter 개선 (#798) #803

Merged
merged 9 commits into from
Mar 26, 2024
Original file line number Diff line number Diff line change
@@ -85,8 +85,10 @@ private String getRequestPayload(HttpServletRequest request) {
try {
ContentCachingRequestWrapper cachedRequest = (ContentCachingRequestWrapper) request;
return objectMapper.readTree(cachedRequest.getContentAsByteArray()).toPrettyString();
} catch (IOException | ClassCastException e) {
} catch (IOException e) {
log.warn("ObjectMapper에서 직렬화 중에 문제가 발생했습니다.", e);
} catch (ClassCastException e) {
log.warn("HttpServletRequest 객체를 ContentCachingRequestWrapper 타입으로 형변환 하는 중 문제가 발생했습니다.", e);
}
return "[ObjectMapper에서 직렬화 중에 문제가 발생했습니다.]";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.festago.common.filter.wrapping;

import com.festago.common.aop.LogRequestBody;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

/**
* ApplicationReadyEvent를 통해 Lazy하게 UriPatternMatcher의 패턴을 추가하는 클래스 <br/>
*/
@Component
@RequiredArgsConstructor
public class UriPatternInitializer {

private final RequestMappingHandlerMapping requestMappingHandlerMapping;
private final UriPatternMatcher uriPatternMatcher;

@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
for (var entry : requestMappingHandlerMapping.getHandlerMethods().entrySet()) {
RequestMappingInfo requestMappingInfo = entry.getKey();
HandlerMethod handlerMethod = entry.getValue();
if (handlerMethod.hasMethodAnnotation(LogRequestBody.class)) {
Set<RequestMethod> methods = requestMappingInfo.getMethodsCondition().getMethods();
Set<String> directPaths = requestMappingInfo.getDirectPaths();
uriPatternMatcher.addPattern(methods, directPaths);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.festago.common.filter.wrapping;

import java.util.Collections;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.RequestMethod;

@Component
public class UriPatternMatcher {

private final AntPathMatcher antPathMatcher = new AntPathMatcher();
private final Map<RequestMethod, Set<String>> methodToPatterns = new EnumMap<>(RequestMethod.class);

/**
* HttpMethod 목록에 대해 Pattern에 추가될 URI Path 목록을 추가하는 메서드<br/> 어플리케이션이 실행될 때, 하나의 스레드에서 접근하는 것을 가정으로 설계했기에 Thread Safe
* 하지 않음.<br/> 따라서 다른 Bean에서 해당 클래스를 의존하여, 이 메서드를 호출하는 것에 주의할 것
*
* @param methods 패턴에 추가할 HttpMethod 목록
* @param paths 패턴에 추가할 URI 목록
*/
public void addPattern(Set<RequestMethod> methods, Set<String> paths) {
for (RequestMethod method : methods) {
Set<String> patterns = methodToPatterns.computeIfAbsent(method, ignore -> new HashSet<>());
patterns.addAll(paths);
}
}

/**
* HttpMethod와 Path이 등록된 패턴에 일치하는지 검사하는 메서드
*
* @param method 패턴에 일치하는지 검사할 HttpMethod
* @param path 패턴에 일치하는지 검사할 경로. 예시: "/api/v1/festival"
* @return method에 대한 path가 등록된 패턴에 일치하면 true, 아니면 false
*/
public boolean match(RequestMethod method, String path) {
Set<String> patterns = methodToPatterns.getOrDefault(method, Collections.emptySet());
for (String pattern : patterns) {
if (antPathMatcher.match(pattern, path)) {
return true;
}
}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.festago.common.filter.wrapping;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;

/**
* LogRequestBodyAspect 클래스가 해당 클래스에 의존하므로, 해당 클래스 수정, 삭제 시 LogRequestBodyAspect 클래스도 수정하거나 삭제할 것!
*/
@Profile("!test")
@Component
@RequiredArgsConstructor
public class UriPatternRequestWrappingFilter extends OncePerRequestFilter {

private final UriPatternMatcher uriPatternMatcher;

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain
) throws ServletException, IOException {
if (uriPatternMatcher.match(RequestMethod.resolve(request.getMethod()), request.getRequestURI())) {
ContentCachingRequestWrapper wrappingRequest = new ContentCachingRequestWrapper(request);
chain.doFilter(wrappingRequest, response);
} else {
chain.doFilter(request, response);
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.festago.common.filter.wrapping;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.RequestMethod;

@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@SuppressWarnings("NonAsciiCharacters")
class UriPatternMatcherTest {

UriPatternMatcher uriPatternMatcher;

@BeforeEach
void setUp() {
uriPatternMatcher = new UriPatternMatcher();
}

@Test
void 경로가_패턴에_매칭되면_참이다() {
// given
uriPatternMatcher.addPattern(Set.of(RequestMethod.POST), Set.of("/api/v1/schools"));

// when & then
assertThat(uriPatternMatcher.match(RequestMethod.POST, "/api/v1/schools")).isTrue();
}

@Test
void 경로가_패턴에_매칭되지_않으면_거짓이다() {
// given
uriPatternMatcher.addPattern(Set.of(RequestMethod.POST), Set.of("/api/v1/schools"));

// when & then
assertThat(uriPatternMatcher.match(RequestMethod.POST, "/api/v1/festivals")).isFalse();
}

@Test
void 경로가_매칭되어도_HttpMethod가_매칭되지_않으면_거짓이다() {
// given
uriPatternMatcher.addPattern(Set.of(RequestMethod.GET), Set.of("/api/v1/schools"));

// when & then
assertThat(uriPatternMatcher.match(RequestMethod.POST, "/api/v1/schools")).isFalse();
}

@Test
void PathVariable_경로가_패턴에_매칭되어야_한다() {
// given
uriPatternMatcher.addPattern(Set.of(RequestMethod.GET), Set.of("/api/v1/schools/{schoolId}"));

// when & then
assertThat(uriPatternMatcher.match(RequestMethod.GET, "/api/v1/schools/1")).isTrue();
}

@Test
void QueryParameter가_있어도_패턴에_매칭되어야_한다() {
// given
uriPatternMatcher.addPattern(Set.of(RequestMethod.GET), Set.of("/api/v1/schools/{schoolId}"));

// when & then
assertThat(uriPatternMatcher.match(RequestMethod.GET, "/api/v1/schools/1?foo=bar")).isTrue();
}
}
Loading