Project

[Project Winter/#7] Configurer, Interceptor 기능 개발

djawnstj 2023. 7. 28. 16:53

Project Winter

 

MVC 의 기본적인 기능은 마무리 했고 Interceptor 를 구현할 차례이다.

 

사실 MVC 와 Ioc, DI 정도만 할걸.... 하는 후회가 조금 들지만 막상 구현을 끝내면 성취감도 좋고 계획한건 마무리 하는 성격이라 열심히 진행중이다.

 

Interceptor 를 하기 앞서 SpringWebMvcConfigurer 를 구현한 클래스에서 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 경우를 만들어 두고, includepreHandle(), postHandle(), afterCompletion() 함수를 모두 정상적으로 호출한다.


exclude 는 인터셉터를 호출하지 않는다.


exceptionpreHandle() 까지 호출하고 핸들러에서 예외를 발생시켜 postHandle() 은 건너뛰고 afterCompletion() 을 호출하게끔 했다.

 

이렇게 인터셉터까지 마무리 했고 남은건 ExceptionResolver, DI 이렇게 남아있다. 원랜 7월 안에 끝내려 했지만 그러지 못했고 최대한 8월까진 끝내려고 한다.

반응형