หน้าเว็บ

วันอังคารที่ 23 กันยายน พ.ศ. 2557

wrap http servlet response ด้วย HttpServletResponseWrapper

ปัญหา

        อันนี้เป็นปัญหาที่ผมเจอระหว่างการพัฒนาโปรเจ็คที่ใช้ spring mvc ครับ ซึ่ง view ผมเป็น jsp
ความต้องการของผมคือ ผมต้องการ parse jsp ไปเป็น html เอง (manual parse) เพราะผมต้องการ return ผลลัพธ์กลับไปเป็น json ซึ่งห่อหุ้ม html (jsp) อีกที  ก็เลยได้ลองหาข้อมูลใน google อยู่สักพัก  ปรากฏว่ามันไม่น่าจะทำได้

ทุกปัญหาย่อมมีทางออก!
         มันก็มีทางออกอยู่ว่า  ถ้าอยาก parse template เอง  ก็ต้องเปลี่ยนไปเขียน code ที่เป็นพวก template engine แทน jsp เช่น themeleaf, freemarker หรือ velocity อันนี้ได้ชัวร์ 100%  แต่ผมไม่อยากเปลี่ยนนี่สิ  เพราะผมต้องการ feature บางอย่างของ jsp ที่มีมากกว่า template engine พวกนี้
        จนในที่สุด  ผมก็พบทางออก  ลืมไปเลย  ว่ามันมีวิธีนี้อยู่ manual parse ไม่ได้ไม่เป็นไร  งั้นห่อหุ่ม (wrap) ผลลัพธ์แทนแล้วกัน  ซึ่งเป็นที่มาของบทความนี้ครับ

วิธีการ


        ผมแก้ปัญหาการ manual parse ไปเป็นการห่อหุ้ม (wrap) ผลลัพธ์ที่ถูก parse แล้วแทน  สรุปก็ยังใช้ jsp เหมือนเดิม  แล้วให้ servlet engine parse jsp ให้จนได้ผลลัพธ์ที่เป็น html
        เราค่อยเอา html นั้นมา wrap ด้วย json ตามที่เราต้องการครับ

คำถาม
        แล้วเราจะ wrap มันยังไง?

คำตอบ
        wrap ด้วย HttpServletResponseWrapper ผ่าน filter นั่นเอง

มาเขียนกันเลยดีกว่าครับ  ซึ่งมีแค่ 2 ขั้นตอนเท่านั้น

1. สร้าง class ที่ extends HttpServletResponseWrapper  เพื่อ modify response ที่ถูกส่งกลับมา ให้เป็น String ด้วยการเปลี่ยน PrintWriter ของ HttpServletResponse เดิม  ไปเป็น CharArrayWritter (จริงๆ แล้วก็ไม่ได้เปลี่ยนหรอก  แค้่ให้ PrintWriter มันเขียนลง CharArrayWritter แทน)

CharResponseWrapper.java
package com.blogspot.na5cent.servlet;

import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

/**
 * @author redcrow
 */
public class CharResponseWrapper extends HttpServletResponseWrapper {

    protected CharArrayWriter charWriter;

    protected PrintWriter writer;

    protected boolean getOutputStreamCalled;

    protected boolean getWriterCalled;

    public CharResponseWrapper(HttpServletResponse response) {
        super(response);
        
        charWriter = new CharArrayWriter();
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        if (getWriterCalled) {
            throw new IllegalStateException("getWriter already called");
        }

        getOutputStreamCalled = true;
        return super.getOutputStream();
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        if (writer != null) {
            return writer;
        }
        
        if (getOutputStreamCalled) {
            throw new IllegalStateException("getOutputStream already called");
        }
        
        getWriterCalled = true;
        writer = new PrintWriter(charWriter);
        return writer;
    }

    @Override
    public String toString() {
        String s = null;

        if (writer != null) {
            s = charWriter.toString();
        }
        
        return s;
    }
}
2. เขียน filter เพื่อเปลี่ยนและ wrap response ด้วย CharResponseWrapper จากข้อ 1 แล้ว manual json ก่อนส่งกลับไปให้ client (browser)
Jsp2JsonResponseWrapperFilter.java
package com.blogspot.na5cent.filter;

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.blogspot.na5cent.servlet.CharResponseWrapper;
import com.google.gson.Gson;
import java.util.HashMap;
import java.util.Map;
import java.util.Date;

/**
 * @author redcrow
 */
@WebFilter(urlPatterns = "/*")
public class Jsp2JsonResponseWrapperFilter implements Filter {

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

    }

    private boolean isJsp2JsonRequest(HttpServletRequest request) {
        return request.getRequestURI().contains("/json/");
    }

    @Override
    public void doFilter(
            ServletRequest request,
            ServletResponse response,
            FilterChain chain
    ) throws IOException, ServletException {

        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;

        if (!isJsp2JsonRequest(req)) {
            chain.doFilter(request, response);
            return;
        }

        ServletResponse newResponse = new CharResponseWrapper(resp); //*****
        chain.doFilter(request, newResponse);
        String html = newResponse.toString();

        if (html != null) {
            Map<String, Object> map = new HashMap<>();
            map.put("payload", html);
            map.put("timestamp", new Date().getTime());

            response.getWriter().write(new Gson().toJson(map));
            response.setContentType("application/json");
        }
    }

    @Override
    public void destroy() {

    }

}
ถ้า file .jsp เราเป็น
<%@page contentType="text/html" pageEncoding="UTF-8"%>

<h1>hello world</h1>
ผลลัพธ์จะได้เป็น json ดังนี้
{"timestamp":1411477104056,"payload":"\u003ch1\u003ehello world\u003c/h1\u003e"}
แค่นี้ ผมก็แก้ปัญหาที่ผมต้องการได้แล้วครับ

เหตุผล

        แน่นอนว่า ต้องมีคนสงสัยว่าทำไมผมต้องทำแบบนี้

        ตอบ :  เพราะผมต้องการ add additional data ลงไปใน response โดยไม่ให้ส่งผลกระทบต่อ html/jsp เดิมครับ  รูปแบบ json เป็นอะไรที่อ่านแล้วเข้าใจง่ายที่สุด และใช้ร่วมกับ javascript ได้ดีที่สุด  ผมมองถึงอนาคตที่ต้องมี additional data ต่างๆ อีกมากมาย  รวมทั้งต้องการทำให้ response ทุกอย่างที่เป็น ajax มีรูปแบบเป็น json แบบเดี่ยวกันทั้งหมด  เพื่อความง่ายในการ manage ครับ

2 ความคิดเห็น:

  1. อ่านบทความตอนแรกว่าจะโพสถามว่าทำแบบนี้ทำไม แต่พอเลื่อนอ่านมาจนสุดเลยได้คำตอบ เหมือนรู้เลยว่าจะถามว่าอะไรครับ ฮ่าๆ

    ตอบลบ
  2. ครับ ฮ่าๆๆๆ ผมเชื่อว่าต้องมีคนสงสัยครับ

    ตอบลบ