인터셉터를 구현하다보니 URL 요청에서 template로 매핑된 부분의 값을 구할 필요가 있었다.
아래 코드에서 thisValue에 해당하는 부분이다.
@GetMapping("/i/wanna/{thisValue}/really")
public SomeResponse getSomeHanlder(@PathVariable String thisValue) {
// ...
어떻게 구할까 고민을 하다가 @PathVariable 를 처리하는 PathVariableMethodArgumentResolver 에서 힌트를 얻을 수 있었다.
package org.springframework.web.servlet.mvc.method.annotation;
public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver
implements UriComponentsContributor {
@Override
@SuppressWarnings("unchecked")
@Nullable
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
Map<String, String> uriTemplateVars = (Map<String, String>) request.getAttribute(
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
return (uriTemplateVars != null ? uriTemplateVars.get(name) : null);
}
코드는 복잡한데 getAttribute로 특정 key의 값을 구해오는 과정이다.
이 Map에는 key가 템플릿부분의 이름이, value가 매핑된 값이 저장이 되는 구조이다.
package org.springframework.web.servlet;
public interface HandlerMapping {
String URI_TEMPLATE_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".uriTemplateVariables";
HandlerMapping.class.getName() 부분은 패키지명을 포함하는 클래스의 이름이므로,
org.springframework.web.servlet.HandlerMapping
라는 값을 가지게 된다.
따라서 URI_TEMPLATE_VARIABLES_ATTRIBUTE 는 "org.springframework.web.servlet.HandlerMapping.uriTemplateVariables" 라는 문자열을 가지게 된다.
PathVariableMethodArgumentResolver 에서는 NativeWebRequest을 이용해서 getAttribute를 통해 구했지만 사실 인스턴스를 만든 HttpServletRequest 에서 직접 구해와도 된다. 왜냐하면 이것은 ServletWebRequest 이고 invokeHandlerMethod 과정에서 만들어지는 인스턴스이기 때문이다.
package org.springframework.web.context.request;
public class ServletRequestAttributes extends AbstractRequestAttributes {
private final HttpServletRequest request;
@Override
public Object getAttribute(String name, int scope) {
if (scope == SCOPE_REQUEST) {
if (!isRequestActive()) {
throw new IllegalStateException(
"Cannot ask for request attribute - request is not active anymore!");
}
return this.request.getAttribute(name); // here
}
else {
HttpSession session = getSession(false);
if (session != null) {
try {
Object value = session.getAttribute(name);
if (value != null) {
this.sessionAttributesToUpdate.put(name, value);
}
return value;
}
catch (IllegalStateException ex) {
// Session invalidated - shouldn't usually happen.
}
}
return null;
}
}
구현체 중 카탈리나
package org.apache.catalina.connector;
public class RequestFacade implements HttpServletRequest {
protected Request request = null;
@Override
public Object getAttribute(String name) {
if (request == null) {
throw new IllegalStateException(
sm.getString("requestFacade.nullRequest"));
}
return request.getAttribute(name);
}
// ...
public class Request implements HttpServletRequest {
@Override
public Object getAttribute(String name) {
// Special attributes
SpecialAttributeAdapter adapter = specialAttributes.get(name);
if (adapter != null) {
return adapter.get(this, name);
}
Object attr = attributes.get(name);
if (attr != null) {
return attr;
}
attr = coyoteRequest.getAttribute(name);
if (attr != null) {
return attr;
}
if (TLSUtil.isTLSRequestAttribute(name)) {
coyoteRequest.action(ActionCode.REQ_SSL_ATTRIBUTE, coyoteRequest);
attr = coyoteRequest.getAttribute(Globals.CERTIFICATES_ATTR);
if (attr != null) {
attributes.put(Globals.CERTIFICATES_ATTR, attr);
}
attr = coyoteRequest.getAttribute(Globals.CIPHER_SUITE_ATTR);
if (attr != null) {
attributes.put(Globals.CIPHER_SUITE_ATTR, attr);
}
attr = coyoteRequest.getAttribute(Globals.KEY_SIZE_ATTR);
if (attr != null) {
attributes.put(Globals.KEY_SIZE_ATTR, attr);
}
attr = coyoteRequest.getAttribute(Globals.SSL_SESSION_ID_ATTR);
if (attr != null) {
attributes.put(Globals.SSL_SESSION_ID_ATTR, attr);
}
attr = coyoteRequest.getAttribute(Globals.SSL_SESSION_MGR_ATTR);
if (attr != null) {
attributes.put(Globals.SSL_SESSION_MGR_ATTR, attr);
}
attr = coyoteRequest.getAttribute(SSLSupport.PROTOCOL_VERSION_KEY);
if (attr != null) {
attributes.put(SSLSupport.PROTOCOL_VERSION_KEY, attr);
}
attr = attributes.get(name);
sslAttributesParsed = true;
}
return attr;
}
그렇다면 누가 이 값을 넣어줄까?
DispatcherServelt에 핵심 동작은 doDispatch 라는 메서드에서 이루어 진다.
package org.springframework.web.servlet;
public class DispatcherServlet extends FrameworkServlet {
@Nullable
private List<HandlerMapping> handlerMappings; // 구현은 ArrayList로..
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest); // 여기!
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
// ..
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
처리할 핸들러를 가져올 때(getHandler) 내부에서 값을 넣어주게 된다.
좀 더 구체적으로 설명하면 AbstractHandlerMethodMapping 에서 lookupHandlerMethod 를 수행할 때 handleMatch 부분에서 값이 세팅이 된다.
package org.springframework.web.servlet.handler;
public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {
@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<>();
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
// No choice but to go through all mappings...
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
if (!matches.isEmpty()) {
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
matches.sort(comparator);
Match bestMatch = matches.get(0);
if (matches.size() > 1) {
if (logger.isTraceEnabled()) {
logger.trace(matches.size() + " matching mappings: " + matches);
}
if (CorsUtils.isPreFlightRequest(request)) {
return PREFLIGHT_AMBIGUOUS_MATCH;
}
Match secondBestMatch = matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
String uri = request.getRequestURI();
throw new IllegalStateException(
"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
}
}
request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
handleMatch(bestMatch.mapping, lookupPath, request); // 여기!
return bestMatch.handlerMethod;
}
else {
return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}
package org.springframework.web.servlet.mvc.method;
public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMethodMapping<RequestMappingInfo> {
/**
* Expose URI template variables, matrix variables, and producible media types in the request.
* @see HandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE
* @see HandlerMapping#MATRIX_VARIABLES_ATTRIBUTE
* @see HandlerMapping#PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE
*/
@Override
protected void handleMatch(RequestMappingInfo info, String lookupPath, HttpServletRequest request) {
super.handleMatch(info, lookupPath, request);
String bestPattern;
Map<String, String> uriVariables;
Set<String> patterns = info.getPatternsCondition().getPatterns();
if (patterns.isEmpty()) {
bestPattern = lookupPath;
uriVariables = Collections.emptyMap();
}
else {
bestPattern = patterns.iterator().next();
uriVariables = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath);
}
request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern);
if (isMatrixVariableContentAvailable()) {
Map<String, MultiValueMap<String, String>> matrixVars = extractMatrixVariables(request, uriVariables);
request.setAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, matrixVars);
}
// 여기!
Map<String, String> decodedUriVariables = getUrlPathHelper().decodePathVariables(request, uriVariables);
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, decodedUriVariables);
if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) {
Set<MediaType> mediaTypes = info.getProducesCondition().getProducibleMediaTypes();
request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes);
}
}
PathMatcher
참고로 내부적으로 Ant 스타일의 path 패턴을 위해 PathMatcher 인터페이스의 구현체로 AntPathMatcher를 사용한다.
package org.springframework.util;
public class AntPathMatcher implements PathMatcher {
@Override
public Map<String, String> extractUriTemplateVariables(String pattern, String path) {
Map<String, String> variables = new LinkedHashMap<>();
boolean result = doMatch(pattern, path, true, variables);
if (!result) {
throw new IllegalStateException("Pattern \"" + pattern + "\" is not a match for \"" + path + "\"");
}
return variables;
}
제일 위에 든 예로 설명하면
pattern에는 "/i/wanna/{thisValue}/really" 와 같은 패턴 문자열이 들어가고
path 에는 실제 들어온 요청 "/i/wanna/foo/really" 처럼 문자열이 들어온다.
결국 Map<String, String>에는 "thisValue" 가 키로, "foo"가 값으로 들어가게 된다.
'Programing > Framework' 카테고리의 다른 글
스프링 캐시 인터셉터 (0) | 2021.04.01 |
---|---|
[spring] JSR-303 과 @Valid 과 @Validated (0) | 2020.11.11 |
[spring] ServletWebRequest 생성 (0) | 2020.11.11 |
[Spring] Controller & RequestMapping : RequestMappingInfo (0) | 2020.11.10 |
[JOOQ] MySQL JDBC batch 벤치마킹 (0) | 2020.10.13 |