Spring Web HTTP 캐 시 지원 시리즈 4: WebRequest \ # checkNotModified 지원 검증 기 메커니즘

HTTP 캐 시 메커니즘 이 강하 고 약 한 검증 기 메커니즘 을 지원 하기 위해 Spring Web 컨트롤 러 방법 으로 사용 할 수 있 는 WebRequest#checkNotModified 방법 을 제공 했다.
전형 적 인 용법 은 예 를 들 면 다음 과 같다.
    @RequestMapping(value = "/document", method = RequestMethod.GET)
    public String getDocument(ServletWebRequest webRequest, Model model, 
                    @RequestParam(value = "documentId", required = true) long documentId) {
    
        // 1.             
        MyDocument document = documentService.findById(documentId);

        // HTTP     
        // 2.                    ,        webRequest  
        //      HTTP       If-Modified-Since      
        //          
        final long lastUpdate = document.getLastUpdateTimestamp();
        final boolean notModified = webRequest.checkNotModified(lastUpdate);
        if (notModified) {//            ,                  
            //    null           ,         HTTP    304 
            return null;
        }

        // 3.                  +          ,      
        //                        
        
        model.addAttribute("title", document.getTitle());
        model.addAttribute("content", document.getContent());
		// ... 
      
      
        return "document_view";
    }

이 예 는 이러한 장면 을 묘사 했다.
  • 시스템 은 문서 대상 을 동적 으로 유지 하고 이 문서 대상 은 수 정 될 수 있 습 니 다.
  • 시스템 사용자 가 특정한 문서 대상 을 방문 하면 해당 하 는 문 서 는 특정한 템 플 릿 보 기 를 통 해 사용자 에 게 보 여 줍 니 다.
  • 문서 대상 의 변경 이 빈번 하지 않 습 니 다. 사용자 가 특정한 문서 의 페이지 를 가 져 왔 다 면 다시 변경 되 기 전에 서버 는 이 요청 자 에 게 이 문 서 를 다시 렌 더 링 할 필요 가 없습니다.

  • 위의 예 에서 webRequest.checkNotModified(lastUpdate) 문서 가 바 뀌 었 는 지 확인 하 는 관건 적 인 논 리 를 실 행 했 습 니 다. lastUpdate 은 서버 쪽 문서 대상 의 최근 변경 시간 이지 문서 대상 ETag 을 바탕 으로 하 는 것 이 아니 라 Spring Web 캐 시 체제 의 약 한 검증 기 를 지원 하 는 예 입 니 다.다음은 HTTP 의 소스 코드 를 분석 해 보 겠 습 니 다.
    소스 코드
    소스 버 전: Spring Security Config 5.1.4. RELEASE
    package org.springframework.web.context.request;
    
    import java.security.Principal;
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Arrays;
    import java.util.Enumeration;
    import java.util.Iterator;
    import java.util.List;
    import java.util.Locale;
    import java.util.Map;
    import java.util.TimeZone;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    
    import org.springframework.http.HttpMethod;
    import org.springframework.http.HttpStatus;
    import org.springframework.lang.Nullable;
    import org.springframework.util.CollectionUtils;
    import org.springframework.util.ObjectUtils;
    import org.springframework.util.StringUtils;
    import org.springframework.web.util.WebUtils;
    
    /**
     * WebRequest adapter for an javax.servlet.http.HttpServletRequest.
     *
     * @author Juergen Hoeller
     * @author Brian Clozel
     * @author Markus Malkusch
     * @since 2.0
     */
    public class ServletWebRequest extends ServletRequestAttributes implements NativeWebRequest {
    
    	private static final String ETAG = "ETag";
    
    	private static final String IF_MODIFIED_SINCE = "If-Modified-Since";
    
    	private static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
    
    	private static final String IF_NONE_MATCH = "If-None-Match";
    
    	private static final String LAST_MODIFIED = "Last-Modified";
    
    	private static final List<String> SAFE_METHODS = Arrays.asList("GET", "HEAD");
    
    	/**
    	 * Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match".
    	 * @see Section 2.3 of RFC 7232
    	 */
    	private static final Pattern ETAG_HEADER_VALUE_PATTERN = 
    		Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?");
    
    	/**
    	 * Date formats as specified in the HTTP RFC.
    	 * @see * Section 7.1.1.1 of RFC 7231
    	 */
    	private static final String[] DATE_FORMATS = new String[] {
    			"EEE, dd MMM yyyy HH:mm:ss zzz",
    			"EEE, dd-MMM-yy HH:mm:ss zzz",
    			"EEE MMM dd HH:mm:ss yyyy"
    	};
    
    	private static final TimeZone GMT = TimeZone.getTimeZone("GMT");
    
    	private boolean notModified = false;
    
    
    	/**
    	 * Create a new ServletWebRequest instance for the given request.
    	 * @param request current HTTP request
    	 */
    	public ServletWebRequest(HttpServletRequest request) {
    		super(request);
    	}
    
    	/**
    	 * Create a new ServletWebRequest instance for the given request/response pair.
    	 * @param request current HTTP request
    	 * @param response current HTTP response (for automatic last-modified handling)
    	 */
    	public ServletWebRequest(HttpServletRequest request, @Nullable HttpServletResponse response) {
    		super(request, response);
    	}
    
    
    	@Override
    	public Object getNativeRequest() {
    		return getRequest();
    	}
    
    	@Override
    	public Object getNativeResponse() {
    		return getResponse();
    	}
    
    	@Override
    	public <T> T getNativeRequest(@Nullable Class<T> requiredType) {
    		return WebUtils.getNativeRequest(getRequest(), requiredType);
    	}
    
    	@Override
    	public <T> T getNativeResponse(@Nullable Class<T> requiredType) {
    		HttpServletResponse response = getResponse();
    		return (response != null ? WebUtils.getNativeResponse(response, requiredType) : null);
    	}
    
    	/**
    	 * Return the HTTP method of the request.
    	 * @since 4.0.2
    	 */
    	@Nullable
    	public HttpMethod getHttpMethod() {
    		return HttpMethod.resolve(getRequest().getMethod());
    	}
    
    	@Override
    	@Nullable
    	public String getHeader(String headerName) {
    		return getRequest().getHeader(headerName);
    	}
    
    	@Override
    	@Nullable
    	public String[] getHeaderValues(String headerName) {
    		String[] headerValues = StringUtils.toStringArray(getRequest().getHeaders(headerName));
    		return (!ObjectUtils.isEmpty(headerValues) ? headerValues : null);
    	}
    
    	@Override
    	public Iterator<String> getHeaderNames() {
    		return CollectionUtils.toIterator(getRequest().getHeaderNames());
    	}
    
    	@Override
    	@Nullable
    	public String getParameter(String paramName) {
    		return getRequest().getParameter(paramName);
    	}
    
    	@Override
    	@Nullable
    	public String[] getParameterValues(String paramName) {
    		return getRequest().getParameterValues(paramName);
    	}
    
    	@Override
    	public Iterator<String> getParameterNames() {
    		return CollectionUtils.toIterator(getRequest().getParameterNames());
    	}
    
    	@Override
    	public Map<String, String[]> getParameterMap() {
    		return getRequest().getParameterMap();
    	}
    
    	@Override
    	public Locale getLocale() {
    		return getRequest().getLocale();
    	}
    
    	@Override
    	public String getContextPath() {
    		return getRequest().getContextPath();
    	}
    
    	@Override
    	@Nullable
    	public String getRemoteUser() {
    		return getRequest().getRemoteUser();
    	}
    
    	@Override
    	@Nullable
    	public Principal getUserPrincipal() {
    		return getRequest().getUserPrincipal();
    	}
    
    	@Override
    	public boolean isUserInRole(String role) {
    		return getRequest().isUserInRole(role);
    	}
    
    	@Override
    	public boolean isSecure() {
    		return getRequest().isSecure();
    	}
    
    
        //     Last-Modified/If-Modified-Since    HTTP        
    	@Override
    	public boolean checkNotModified(long lastModifiedTimestamp) {
            //    ETag   null,           
    		return checkNotModified(null, lastModifiedTimestamp);
    	}
    
        //     ETag/If-None-Match    HTTP        
    	@Override
    	public boolean checkNotModified(String etag) {
            //    lastModifiedTimestamp   -1,            
    		return checkNotModified(etag, -1);
    	}
    
    	@Override
    	public boolean checkNotModified(@Nullable String etag, long lastModifiedTimestamp) {
    		HttpServletResponse response = getResponse();
    		if (this.notModified || (response != null && HttpStatus.OK.value() != response.getStatus())) {
    			return this.notModified;
    		}
    
    		// Evaluate conditions in order of precedence.
    		// See https://tools.ietf.org/html/rfc7232#section-6
    
    		if (validateIfUnmodifiedSince(lastModifiedTimestamp)) {
            //           If-Unmodified-Since     lastModifiedTimestamp(>0)  ,
            //          this.notModified      true,              412
    			if (this.notModified && response != null) {
    				response.setStatus(HttpStatus.PRECONDITION_FAILED.value());
    			}
    			return this.notModified;
    		}
    
           //           Etag/If-None-Match            ,
            //         Last-Modified/If-Modified-Since            
    		boolean validated = validateIfNoneMatch(etag);
    		if (!validated) {
                //         ,       Last-Modified/If-Modified-Since            
    			validateIfModifiedSince(lastModifiedTimestamp);
    		}
    
    		// Update response ,       
    		if (response != null) {
    			boolean isHttpGetOrHead = SAFE_METHODS.contains(getRequest().getMethod());
    			if (this.notModified) {
    				//    this.notModified   true,      HTTP       304    412
    				response.setStatus(isHttpGetOrHead ?
    						HttpStatus.NOT_MODIFIED.value() : HttpStatus.PRECONDITION_FAILED.value());
    			}            
    			if (isHttpGetOrHead) {
    				// this.notModified   false,   HTTP       GET    HEAD,
    				//          Last-Modified,    ETag
    				if (lastModifiedTimestamp > 0 
    					&& parseDateValue(response.getHeader(LAST_MODIFIED)) == -1) {
    					response.setDateHeader(LAST_MODIFIED, lastModifiedTimestamp);
    				}
    				if (StringUtils.hasLength(etag) && response.getHeader(ETAG) == null) {
    					response.setHeader(ETAG, padEtagIfNecessary(etag));
    				}
    			}
    		}
    		//         ,this.notModified   true   ,         ,
    		// this.notModified   false   ,                Last-Modified,    ETag
    		return this.notModified;
    	}
    
    	//    false             If-Unmodified-Since     lastModifiedTimestamp<0,   
    	//    true           If-Unmodified-Since     lastModifiedTimestamp(>0)  ,  :
    	//         ,  this.notModified      true
    	//         ,  notModified      false
    	private boolean validateIfUnmodifiedSince(long lastModifiedTimestamp) {
    		if (lastModifiedTimestamp < 0) {
                //             -1,    false
    			return false;
    		}
    		//            If-Unmodified-Since : 
    		//      ,                                    。
    		//               ,      412 Precondition Failed       
    		long ifUnmodifiedSince = parseDateHeader(IF_UNMODIFIED_SINCE);
    		if (ifUnmodifiedSince == -1) {
    			//        If-Unmodified-Since ,    false
    			return false;
    		}
    		// We will perform this validation...
    		//    If-Unmodified-Since(     ,            )         ,
    		//    this.notModified     true,     this.notModified     false
    		this.notModified = (ifUnmodifiedSince < (lastModifiedTimestamp / 1000 * 1000));
            
            //    true         If-Unmodified-Since,      If-Unmodified-Since         
    		return true;
    	}
    
        //   HTTP            ,   etag              ,
        //                ,  etag       ,          If-None-Match,
        //                            etag,           etag
        //                            
        //       true    Etag/If-None-Match          ,
        //        Last-Modified/If-Modified-Since       
    	private boolean validateIfNoneMatch(@Nullable String etag) {
    		if (!StringUtils.hasLength(etag)) {
    			return false;
    		}
    
    		Enumeration<String> ifNoneMatch;
    		try {
    			ifNoneMatch = getRequest().getHeaders(IF_NONE_MATCH);
    		}
    		catch (IllegalArgumentException ex) {
    			return false;
    		}
    		if (!ifNoneMatch.hasMoreElements()) {
    			return false;
    		}
    
    		// We will perform this validation...
    		etag = padEtagIfNecessary(etag);
    		if (etag.startsWith("W/")) {
    			etag = etag.substring(2);
    		}
    		while (ifNoneMatch.hasMoreElements()) {
    			String clientETags = ifNoneMatch.nextElement();
    			Matcher etagMatcher = ETAG_HEADER_VALUE_PATTERN.matcher(clientETags);
    			// Compare weak/strong ETags as per https://tools.ietf.org/html/rfc7232#section-2.3
    			while (etagMatcher.find()) {
    				if (StringUtils.hasLength(etagMatcher.group()) && etag.equals(etagMatcher.group(3))) {
    					this.notModified = true;
    					break;
    				}
    			}
    		}
    
    		return true;
    	}
    
    	private String padEtagIfNecessary(String etag) {
    		if (!StringUtils.hasLength(etag)) {
    			return etag;
    		}
    		if ((etag.startsWith("\"") || etag.startsWith("W/\"")) && etag.endsWith("\"")) {
    			return etag;
    		}
    		return "\"" + etag + "\"";
    	}
    
    	private boolean validateIfModifiedSince(long lastModifiedTimestamp) {
    		if (lastModifiedTimestamp < 0) {
    			//    lastModifiedTimestamp   -1,       false,        If-Modified-Since
    			return false;
    		}
            //            If-Modified-Since  
    		long ifModifiedSince = parseDateHeader(IF_MODIFIED_SINCE);
    		if (ifModifiedSince == -1) {
    			//           If-Modified-Since,      false,        If-Modified-Since
    			return false;
    		}
    		// We will perform this validation...
    		//    If-Modified-Since(     ,                  )         ,
    		//    this.notModified     false,     this.notModified     true        
    		this.notModified = ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000);
            
    		//    true         If-Modified-Since,      If-Modified-Since 
    		return true;
    	}
    
    	public boolean isNotModified() {
    		return this.notModified;
    	}
    
        //     ,                     
    	private long parseDateHeader(String headerName) {
    		long dateValue = -1;
    		try {
    			dateValue = getRequest().getDateHeader(headerName);
    		}
    		catch (IllegalArgumentException ex) {
    			String headerValue = getHeader(headerName);
    			// Possibly an IE 10 style value: "Wed, 09 Apr 2014 09:57:42 GMT; length=13774"
    			if (headerValue != null) {
    				int separatorIndex = headerValue.indexOf(';');
    				if (separatorIndex != -1) {
    					String datePart = headerValue.substring(0, separatorIndex);
    					dateValue = parseDateValue(datePart);
    				}
    			}
    		}
    		return dateValue;
    	}
    
    	private long parseDateValue(@Nullable String headerValue) {
    		if (headerValue == null) {
    			// No header value sent at all
    			return -1;
    		}
    		if (headerValue.length() >= 3) {
    			// Short "0" or "-1" like values are never valid HTTP date headers...
    			// Let's only bother with SimpleDateFormat parsing for long enough values.
    			for (String dateFormat : DATE_FORMATS) {
    				SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.US);
    				simpleDateFormat.setTimeZone(GMT);
    				try {
    					return simpleDateFormat.parse(headerValue).getTime();
    				}
    				catch (ParseException ex) {
    					// ignore
    				}
    			}
    		}
    		return -1;
    	}
    
    	@Override
    	public String getDescription(boolean includeClientInfo) {
    		HttpServletRequest request = getRequest();
    		StringBuilder sb = new StringBuilder();
    		sb.append("uri=").append(request.getRequestURI());
    		if (includeClientInfo) {
    			String client = request.getRemoteAddr();
    			if (StringUtils.hasLength(client)) {
    				sb.append(";client=").append(client);
    			}
    			HttpSession session = request.getSession(false);
    			if (session != null) {
    				sb.append(";session=").append(session.getId());
    			}
    			String user = request.getRemoteUser();
    			if (StringUtils.hasLength(user)) {
    				sb.append(";user=").append(user);
    			}
    		}
    		return sb.toString();
    	}
    
    
    	@Override
    	public String toString() {
    		return "ServletWebRequest: " + getDescription(true);
    	}
    
    }
    

    관련 글
    Spring Web HTTP 캐 시 지원 시리즈 1: HTTP 캐 시 메커니즘 소개 Spring Web HTTP 캐 시 지원 시리즈 2: Spring Web HTTP 캐 시 지원 시리즈 3: Cache - Control 헤드 지원 Spring Web HTTP 캐 시 지원 시리즈 4: WebRequest \ # checkNotModified 지원 검증 기 메커니즘

    좋은 웹페이지 즐겨찾기