Project Winter
MVC 의 기본적인 기능은 마무리 했고 Interceptor
를 구현할 차례이다.
사실 MVC 와 Ioc, DI 정도만 할걸.... 하는 후회가 조금 들지만 막상 구현을 끝내면 성취감도 좋고 계획한건 마무리 하는 성격이라 열심히 진행중이다.
Interceptor
를 하기 앞서 Spring
은 WebMvcConfigurer
를 구현한 클래스에서 addInterceptors()
메서드를 통해 HandlerInterceptor
를 등록하기 때문에 Configurer
기능을 먼저 개발했다.
WebMvcConfigurer
public interface WebMvcConfigurer {
default void addInterceptors(InterceptorRegistry registry) {}
}
default 메서드로 addInterceptors
를 선언해 오버라이딩을 강제하지 않았다.
WebMvcConfigurationSupport
public class WebMvcConfigurationSupport implements WebMvcConfigurer {
private final List<WebMvcConfigurer> configurers = new ArrayList<>();
private List<MappedInterceptor> interceptors;
public void addWebMvcConfigurers(List<WebMvcConfigurer> configurers) {
if (!configurers.isEmpty()) {
this.configurers.addAll(configurers);
}
}
public void loadConfigurer() {
initInterceptors();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
this.configurers.forEach(configurer -> configurer.addInterceptors(registry));
}
private void initInterceptors() {
InterceptorRegistry interceptorRegistry = new InterceptorRegistry();
addInterceptors(interceptorRegistry);
this.interceptors = interceptorRegistry.getInterceptors();
}
public MappedInterceptor[] getInterceptors() {
return this.interceptors.toArray(new MappedInterceptor[]{});
}
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
final RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
mapping.setInterceptors(getInterceptors());
return mapping;
}
public BeanNameUrlHandlerMapping beanNameUrlHandlerMapping() {
final BeanNameUrlHandlerMapping mapping = new BeanNameUrlHandlerMapping();
mapping.setInterceptors(getInterceptors());
return mapping;
}
}
bean
을 초기화할때 WebMvcConfigurer
를 상속한 클래스들을 받아 각각 클래스들에 재정의된 함수들을 호출하여 설정 정보를 초기화해주는 역할을 한다.
HandlerInterceptor
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {}
}
preHandler()
은 요청이 오면 handle 함수를 호출하기 전 호출된다. preHandle()
의 반환값으로 false 가 반환되면 더이상 인터셉터/핸들러 로직을 진행하지 않고 afterCompletion()
을 호출하고 요청을 마무리한다.
postHandle()
은 요청에 대한 핸들러 로직을 마무리 하고 ModelAndView
로직까지 성공하면 호출된다.
afterCompletion()
은 요청에 대한 결과와 상관없이 응답을 해준 후 호출된다.
InterceptorRegistration
public class InterceptorRegistration {
private final HandlerInterceptor interceptor;
private List<String> includePaths;
private List<String> excludePaths;
public InterceptorRegistration(HandlerInterceptor interceptor) {
if (interceptor == null) throw new IllegalArgumentException("Interceptor is required");
this.interceptor = interceptor;
}
public InterceptorRegistration addPathPatterns(String... paths) {
addPathPatterns(Arrays.asList(paths));
return this;
}
public InterceptorRegistration addPathPatterns(List<String> paths) {
this.includePaths = (this.includePaths != null) ? this.includePaths : new ArrayList<>(paths.size());
this.includePaths.addAll(paths);
return this;
}
public InterceptorRegistration excludePathPatterns(String... paths) {
excludePathPatterns(Arrays.asList(paths));
return this;
}
public InterceptorRegistration excludePathPatterns(List<String> paths) {
this.excludePaths = (this.excludePaths != null) ? this.excludePaths : new ArrayList<>(paths.size());
this.excludePaths.addAll(paths);
return this;
}
protected MappedInterceptor getInterceptor() {
String[] includePathsEmptyArray = {};
String[] excludePathsEmptyArray = {};
String[] includePathsArray = (this.includePaths == null) ? includePathsEmptyArray : this.includePaths.toArray(includePathsEmptyArray);
String[] excludePathsArray = (this.excludePaths == null) ? excludePathsEmptyArray : this.excludePaths.toArray(excludePathsEmptyArray);
return new MappedInterceptor(this.interceptor, includePathsArray, excludePathsArray);
}
}
개발자가 인터셉터를 만들고 Configurer
클래스에 인터셉터를 등록할 때 사용할 클래스이다.
인터셉터를 포함시킬 요청경로와 제외시킬 요청경로를 등록할 수 있다.
InterceptorRegistry
public class InterceptorRegistry {
private final List<InterceptorRegistration> registrations = new ArrayList<>();
public InterceptorRegistration addInterceptor(HandlerInterceptor interceptor) {
InterceptorRegistration registration = new InterceptorRegistration(interceptor);
this.registrations.add(registration);
return registration;
}
protected List<MappedInterceptor> getInterceptors() {
return this.registrations
.stream()
.map(InterceptorRegistration::getInterceptor)
.toList();
}
}
Configurer
에서 인터셉터를 받을때 사용하며 InterceptorRegistration
을 생성하여 인터셉터 관련 정보를 추가할 수 있도록 한다.
PathPattern
public class PathPattern implements Comparable<PathPattern> {
private final String pattern;
private final int order;
public PathPattern(String pattern, int order) {
this.pattern = pattern;
this.order = order;
}
public int getOrder() {
return order;
}
public boolean match(String pattern) {
return (this.pattern.equals("*") || this.pattern.equals(pattern));
}
@Override
public int compareTo(PathPattern o) {
return this.order - o.order;
}
public static String[] getPatterns(String path) {
if (path.startsWith("/")) path = path.substring(1);
return path.split("/");
}
}
요청경로 문자열을 /
기준으로 분할하고 각 resource 단위로 생성할 클래스이다.
MappedInterceptor
public class MappedInterceptor implements HandlerInterceptor {
private final HandlerInterceptor interceptor;
private final PathAdapter[] includePaths;
private final PathAdapter[] excludePaths;
public MappedInterceptor(HandlerInterceptor interceptor, String[] includePaths, String[] excludePaths) {
this.interceptor = interceptor;
this.includePaths = PathAdapter.initPathAdapters(includePaths);
this.excludePaths = PathAdapter.initPathAdapters(excludePaths);
}
public boolean matches(HttpServletRequest request) {
String uri = request.getRequestURI();
for (PathAdapter adapter : this.excludePaths) {
if (adapter.match(uri)) return false;
}
if (this.includePaths.length == 0) return true;
for (PathAdapter adapter : this.includePaths) {
if (adapter.match(uri)) return true;
}
return false;
}
public HandlerInterceptor getInterceptor() {
return this.interceptor;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return this.interceptor.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
this.interceptor.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
this.interceptor.afterCompletion(request, response, handler, ex);
}
private static class PathAdapter {
private final String path;
private final List<PathPattern> patterns;
private PathAdapter(String path) {
this.path = path;
this.patterns = new ArrayList<>();
final String[] patterns = PathPattern.getPatterns(path);
for (int i = 0; i < patterns.length; i++) {
this.patterns.add(new PathPattern(patterns[i], i));
}
Collections.sort(this.patterns);
}
boolean match(String requestPath) {
String[] requestPatterns = PathPattern.getPatterns(requestPath);
for (PathPattern pattern : this.patterns) {
int order = pattern.getOrder();
if (requestPatterns.length < order) break;
boolean isMatch = pattern.match(requestPatterns[order]);
if (!isMatch) return false;
}
return true;
}
public static PathAdapter[] initPathAdapters(String[] paths) {
return Arrays.stream(paths)
.map(PathAdapter::new)
.toArray(PathAdapter[]::new);
}
}
}
매핑된 URL 을 PathPattern 리스트로 가지고 요청에 대한 처리를 해야하는 인터셉터인지 확인 후 인터셉터를 반환하는 역할을 한다.
테스트
@WinterServerTest
public class InterceptorTest {
public static class TestInterceptor implements HandlerInterceptor {
private final Logger log = LoggerFactory.getLogger(TestInterceptor.class);
@Override
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception {
log.debug("preHandle called: {}", request.getRequestURI());
return true;
}
@Override
public void postHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final ModelAndView modelAndView) throws Exception {
log.debug("postHandle called: {}", request.getRequestURI());
}
@Override
public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final Exception ex) throws Exception {
log.debug("afterCompletion called: {}", request.getRequestURI());
}
}
@Controller
@RequestMapping("/interceptor/anno")
public static class InterceptorTestAnnotationController {
@RequestMapping("/include")
public String include() {
return "test";
}
@RequestMapping("/exclude")
public String exclude() {
return "test";
}
@RequestMapping("/exception")
public String exception() {
throw new IllegalStateException("throw exception");
}
}
public static class InterceptorTestBeanIncludeController implements com.project.winter.mvc.controller.Controller {
@Override
public ModelAndView handleRequest(final HttpServletRequest req, final HttpServletResponse res) {
return new ModelAndView("test");
}
}
public static class InterceptorTestBeanExcludeController implements com.project.winter.mvc.controller.Controller {
@Override
public ModelAndView handleRequest(final HttpServletRequest req, final HttpServletResponse res) {
return new ModelAndView("test");
}
}
public static class InterceptorTestBeanExceptionController implements com.project.winter.mvc.controller.Controller {
@Override
public ModelAndView handleRequest(final HttpServletRequest req, final HttpServletResponse res) {
throw new IllegalStateException("throw exception");
}
}
@Configuration
public static class InterceptorConfig implements WebMvcConfigurer {
@Bean(name = "/interceptor/bean/include")
public InterceptorTestBeanIncludeController interceptorTestBeanIncludeController() {
return new InterceptorTestBeanIncludeController();
}
@Bean(name = "/interceptor/bean/exclude")
public InterceptorTestBeanExcludeController interceptorTestBeanExcludeController() {
return new InterceptorTestBeanExcludeController();
}
@Bean(name = "/interceptor/bean/exception")
public InterceptorTestBeanExceptionController interceptorTestBeanExceptionController() {
return new InterceptorTestBeanExceptionController();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TestInterceptor())
.addPathPatterns("/interceptor/*")
.excludePathPatterns("/interceptor/*/exclude");
}
}
@Test
public void test_interceptor_annotation_controller_include() throws Exception {
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest httpRequest = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/interceptor/anno/include"))
.build();
HttpResponse<String> result = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
}
@Test
public void test_interceptor_annotation_controller_exclude() throws Exception {
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest httpRequest = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/interceptor/anno/exclude"))
.build();
HttpResponse<String> result = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
}
@Test
public void test_interceptor_annotation_controller_exception() throws Exception {
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest httpRequest = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/interceptor/anno/exception"))
.build();
HttpResponse<String> result = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
}
@Test
public void test_interceptor_bean_controller_include() throws Exception {
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest httpRequest = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/interceptor/bean/include"))
.build();
HttpResponse<String> result = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
}
@Test
public void test_interceptor_bean_controller_exclude() throws Exception {
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest httpRequest = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/interceptor/bean/exclude"))
.build();
HttpResponse<String> result = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
}
@Test
public void test_interceptor_bean_controller_exception() throws Exception {
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest httpRequest = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/interceptor/bean/exception"))
.build();
HttpResponse<String> result = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
}
}
테스트 코드가 조금 긴데 요약하면 애노테이션 기반, 빈 이름 기반 컨트롤러에 각각 include
, exclude
, exception
경우를 만들어 두고, include
는 preHandle()
, postHandle()
, afterCompletion()
함수를 모두 정상적으로 호출한다.
exclude
는 인터셉터를 호출하지 않는다.
exception
은 preHandle()
까지 호출하고 핸들러에서 예외를 발생시켜 postHandle()
은 건너뛰고 afterCompletion()
을 호출하게끔 했다.
이렇게 인터셉터까지 마무리 했고 남은건 ExceptionResolver
, DI
이렇게 남아있다. 원랜 7월 안에 끝내려 했지만 그러지 못했고 최대한 8월까진 끝내려고 한다.