2015년 11월 30일 월요일

HttpLogginFilter 관련 - HttpRequest logging 처리



위 그림과 같이 filter의 경우 request와 reponse에 대해서 처리를 해주지만 interceptor처럼 prehandle/posthandle/afterCompletion 와 같이 실행되는 시점을 나눠서 처리할 수가 없다. 

그래서 나는 doFilter() 메쏘드가 request와 response 시점에 두번 실행되는 것으로 생각했다.
하지만 debug모드를 이용하여 테스트 한 결과 chain.doFilter(,); 메쏘드의 코드 라인 기준으로 먼저 선언된 코드는 request에 실행되고 chain.doFilter(,); 메쏘드 이후 작성된 코드는 response처리 시점에 실행되는 것으로 확인 되었다.

filter 자체가 쓰레드 기반으로 실행이 되고 dofilter기점으로 해당 쓰레드는 sleep상태를 유지하다가 servelt이 실행되고 결과를 리턴하여 dispatcherServlet이 filter를 깨워주는 형태로 WAS가 처리해주는 것으로 예상된다.

 - 주의점  exception 발생시 Interceptor과 filter 를 통하지 않으므로 참고

해당 필터를 통해서 HttpRequest에서  getInputStream을 하게 되면 해당 Request에서 inputStream의 데이타 없어지므로 주의 해야 한다.

HttpServletRequest의 InputStream은 한 번 읽으면 다시 읽을 수 없습니다. 톰캣 개발자님들께서 친히 막아두셨기 때문이죠! 만약 Interceptor나 Filter에서 InputStream을 읽게되면, 이후 Spring이 Converter를 이용해 Json 데이터를 바인딩 처리할 때 아래와 같은 에러를 만날 수 있습니다. 불쌍한 Spring이 이미 읽어버린 InputStream을 다시 읽으려고 시도하다가 슬픈 에러를 뱉어내는거죠.

java.lang.IllegalStateException: getReader() has already been called for this request
org.springframework.http.converter.HttpMessageNotReadableException: Could not read JSON: Stream closed; nested exception is java


기본적으로 HttpServletRequestWrapper을 상속 받아서 처리를 하나 Multipart처리를 위하여 AbstractMultipartHttpServletRequest 를 상속 받고 StandardMultipartHttpServletRequest 클래스의 Multipart 처리 부분의 코드를 그대로 copy해서 구현한다.


HttpRequestWrapper.java
/**
 * HttpRequestWrapper
 * Multipart 처리를 위한 AbstractMultipartHttpServletRequest 상속으로 처리 변경
 * @author mike
 * @date 2017. 8. 8.
 * @version
 */
public class HttpRequestWrapper extends AbstractMultipartHttpServletRequest {

    private byte[] bodyData;
 
    public HttpRequestWrapper(HttpServletRequest request) throws IOException {
        this(request, false);
        InputStream is = super.getInputStream();
        bodyData = IOUtils.toByteArray(is);
    }
    public HttpRequestWrapper(HttpServletRequest request, boolean lazyParsing) throws MultipartException {
        super(request);
        if (!lazyParsing) {
            parseRequest(request);
        }
    }
   //InputStream에 대해서 지속적으로 사용할 수 있도록 수정
    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream bis = new ByteArrayInputStream(bodyData);
        return new ServletInputImpl(bis);
    }
//Multipart 처리 부분  
 private static final String CONTENT_DISPOSITION = "content-disposition";

    private static final String FILENAME_KEY = "filename=";

    private Set<String> multipartParameterNames;

    private void parseRequest(HttpServletRequest request) {
        try {
            Collection<Part> parts = request.getParts();
            this.multipartParameterNames = new LinkedHashSet<String>(parts.size());
            MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<String, MultipartFile>(parts.size());
            for (Part part : parts) {
                String filename = extractFilename(part.getHeader(CONTENT_DISPOSITION));
                if (filename != null) {
                    files.add(part.getName(), new StandardMultipartFile(part, filename));
                }
                else {
                    this.multipartParameterNames.add(part.getName());
                }
            }
            setMultipartFiles(files);
        }
        catch (Exception ex) {
            throw new MultipartException("Could not parse multipart servlet request", ex);
        }
    }

    private String extractFilename(String contentDisposition) {
        if (contentDisposition == null) {
            return null;
        }
        // TODO: can only handle the typical case at the moment
        int startIndex = contentDisposition.indexOf(FILENAME_KEY);
        if (startIndex == -1) {
            return null;
        }
        String filename = contentDisposition.substring(startIndex + FILENAME_KEY.length());
        if (filename.startsWith("\"")) {
            int endIndex = filename.indexOf("\"", 1);
            if (endIndex != -1) {
                return filename.substring(1, endIndex);
            }
        }
        else {
            int endIndex = filename.indexOf(";");
            if (endIndex != -1) {
                return filename.substring(0, endIndex);
            }
        }
        return filename;
    }


    @Override
    protected void initializeMultipart() {
        parseRequest(getRequest());
    }

    @Override
    public Enumeration<String> getParameterNames() {
        if (this.multipartParameterNames == null) {
            initializeMultipart();
        }
        if (this.multipartParameterNames.isEmpty()) {
            return super.getParameterNames();
        }

        // Servlet 3.0 getParameterNames() not guaranteed to include multipart form items
        // (e.g. on WebLogic 12) -> need to merge them here to be on the safe side
        Set<String> paramNames = new LinkedHashSet<String>();
        Enumeration<String> paramEnum = super.getParameterNames();
        while (paramEnum.hasMoreElements()) {
            paramNames.add(paramEnum.nextElement());
        }
        paramNames.addAll(this.multipartParameterNames);
        return Collections.enumeration(paramNames);
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        if (this.multipartParameterNames == null) {
            initializeMultipart();
        }
        if (this.multipartParameterNames.isEmpty()) {
            return super.getParameterMap();
        }

        // Servlet 3.0 getParameterMap() not guaranteed to include multipart form items
        // (e.g. on WebLogic 12) -> need to merge them here to be on the safe side
        Map<String, String[]> paramMap = new LinkedHashMap<String, String[]>();
        paramMap.putAll(super.getParameterMap());
        for (String paramName : this.multipartParameterNames) {
            if (!paramMap.containsKey(paramName)) {
                paramMap.put(paramName, getParameterValues(paramName));
            }
        }
        return paramMap;
    }

    @Override
    public String getMultipartContentType(String paramOrFileName) {
        try {
            Part part = getPart(paramOrFileName);
            return (part != null ? part.getContentType() : null);
        }
        catch (Exception ex) {
            throw new MultipartException("Could not access multipart servlet request", ex);
        }
    }

    @Override
    public HttpHeaders getMultipartHeaders(String paramOrFileName) {
        try {
            Part part = getPart(paramOrFileName);
            if (part != null) {
                HttpHeaders headers = new HttpHeaders();
                for (String headerName : part.getHeaderNames()) {
                    headers.put(headerName, new ArrayList<String>(part.getHeaders(headerName)));
                }
                return headers;
            }
            else {
                return null;
            }
        }
        catch (Exception ex) {
            throw new MultipartException("Could not access multipart servlet request", ex);
        }
    }


    /**
     * Spring MultipartFile adapter, wrapping a Servlet 3.0 Part object.
     */
    private static class StandardMultipartFile implements MultipartFile {

        private final Part part;

        private final String filename;

        public StandardMultipartFile(Part part, String filename) {
            this.part = part;
            this.filename = filename;
        }

        @Override
        public String getName() {
            return this.part.getName();
        }

        @Override
        public String getOriginalFilename() {
            return this.filename;
        }

        @Override
        public String getContentType() {
            return this.part.getContentType();
        }

        @Override
        public boolean isEmpty() {
            return (this.part.getSize() == 0);
        }

        @Override
        public long getSize() {
            return this.part.getSize();
        }

        @Override
        public byte[] getBytes() throws IOException {
            return FileCopyUtils.copyToByteArray(this.part.getInputStream());
        }

        @Override
        public InputStream getInputStream() throws IOException {
            return this.part.getInputStream();
        }

        @Override
        public void transferTo(File dest) throws IOException, IllegalStateException {
            this.part.write(dest.getPath());
        }
    }
}

class ServletInputImpl extends ServletInputStream {

    private InputStream is;

    public ServletInputImpl(InputStream bis) {
        is = bis;
    }

    @Override
    public int read() throws IOException {
        return is.read();
    }

    @Override
    public int read(byte[] b) throws IOException {
        return is.read(b);
    }

    @Override
    public boolean isFinished() {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean isReady() {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public void setReadListener(ReadListener arg0) {
        // TODO Auto-generated method stub
     
    }



http 해더/ http uri/ http body를 출력하는 필터

HttpRequestWapper를 구현하여 해당 데이타를 유지 시켜주는 작업이 필요하다.
public class HttpLoggingFilter implements Filter{
    protected final Logger log = LoggerFactory.getLogger(this.getClass());

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        HttpRequestWrapper requestWrapper = new HttpRequestWrapper(request);
        log.debug("======================================          REQUEST         ======================================{}" );
        Enumeration<String> requestHeaderNames = requestWrapper.getHeaderNames();
        while (requestHeaderNames.hasMoreElements()) {
            String headerName = requestHeaderNames.nextElement();
            Enumeration<String> headers = requestWrapper.getHeaders(headerName);
            while (headers.hasMoreElements()) {
                String headerValue = headers.nextElement();
                log.debug(" Request HEADER - {} : {} ", headerName, headerValue );
            }
         
        }
        if(requestWrapper.getHeader("Server")==null||requestWrapper.getHeader("Server").isEmpty()){ //response데이타는 제외
            log.debug(" Request METHOD:{} URI:{}  QUERYSTRING:{}", requestWrapper.getMethod(), requestWrapper.getRequestURI(), requestWrapper.getQueryString());
            if(requestWrapper.getMethod().equalsIgnoreCase("POST")||requestWrapper.getMethod().equalsIgnoreCase("PUT")){
                if((requestWrapper.getHeader("content-type")!=null&&requestWrapper.getHeader("content-type").indexOf("multipart")<0)){ // 이미지 업로드는 제외
                    log.debug(" Request Body:  {}", StringHelper.getString(requestWrapper.getInputStream()));
                }
             
            }
        }
     
        chain.doFilter(requestWrapper, response);
    }

    public void destroy() {}
 
 
}




댓글 없음:

댓글 쓰기