設計模式——從HttpServletRequestWrapper瞭解裝飾者模式

糖拌蕃茄發表於2020-12-31

從一個業務開始

最近專案上緊急需要,為了應付一個不知道啥的安全檢測,我們要給系統追加防XSS注入的功能,這裡有經驗的JavaWeb開發就會想到,用過濾器或者基於專案框架的攔截器來做,但是順著這個思路下去,我們會發現一個很尷尬的問題,如果我們在過濾器裡把Request物件的引數拿處理校驗(即表明使用了Reuqest的引數),那麼後續的過濾器或者Controller都將得到一個“失效”的Request物件,後面的邏輯全線垮掉……

於是,愛好學習的你就去了某度,某度經過檢索,給了你一個頁面……,又經過重重查詢和閱讀,你終於有了解決方案:

我們可以利用HttpServletRequestWrapper包裝HttpServletRequest,在Wrapper中實現引數的修改,然後用HttpServletRequestWrapper替換HttpServletRequest,這樣就實現了引數的修改設定。

這時候,喜歡刨根問底的你,就想看看這個HttpServletRequestWrapper究竟是什麼,於是好奇之旅開始。

HttpServletRequestWrapper原始碼

說白了HttpServletRequestWrapper是HttpServletRequest裝飾類,什麼是裝飾類,我們後面再說,這一節裡,先來看看它的落地實現,也就是原始碼。

整體關係

這裡我就先說結論吧:

  • HttpServletRequestWrapper繼承了ServletRequestWrapper,實現了HttpServletRequest介面;
  • ServletRequestWrapper和HttpServletRequest都實現了ServletRequest介面;
  • ServletRequestWrapper是對ServletRequest的包裝;
  • HttpServletRequestWrapper是對HttpServletRequest的包裝;

實際的繼承關係如下圖:

 

 

ServletRequestWrapper的原始碼:

package javax.servlet;

import java.io.BufferedReader;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map;

/**
 * ServletRequestWrapper提供了一個簡便實現類,在對ServletRequest有改動需求的時,可以重寫此類,之後Request的操作都會經過重寫的類.
 */
public class ServletRequestWrapper implements ServletRequest {
    
    /**
     * ServletRequestWrapper持有ServletRequest的例項.
     */
    private ServletRequest request;

    /**
     * 構建ServletRequestWrapper,傳入ServletRequest的值.
     */
    public ServletRequestWrapper(ServletRequest request) {
        if (request == null) {
            throw new IllegalArgumentException("Request cannot be null");
        }
        this.request = request;
    }

    /**
     * 獲取ServletRequest例項.
     */
    public ServletRequest getRequest() {
        return this.request;
    }

    /**
     * 設定ServletRequest例項.
     * @param request ServletRequest.
     */
    public void setRequest(ServletRequest request) {
        if (request == null) {
            throw new IllegalArgumentException("Request cannot be null");
        }
        this.request = request;
    }

    /**
     * 獲取Servlet的屬性.
     */
    public Object getAttribute(String name) {
        return this.request.getAttribute(name);
    }

    /**
     * 獲取所有屬性列舉格式.
     */
    public Enumeration getAttributeNames() {
        return this.request.getAttributeNames();
    }

    /**
     * 返回請求中輸入內容的字元編碼型別,如果沒有定義字元編碼型別就返回空值.
     * @return 字元編碼.
     */
    public String getCharacterEncoding() {
        return this.request.getCharacterEncoding();
    }

    /**
     * 設定輸入內容的字元編碼型別.
     * @param env 字元編碼型別.
     * @throws java.io.UnsupportedEncodingException .
     */
    public void setCharacterEncoding(String enc) throws java.io.UnsupportedEncodingException {
        this.request.setCharacterEncoding(enc);
    }

    /**
     * 請求內容的長度,如果長度未知就返回-1.
     * @return 請求內容長度.
     */
    public int getContentLength() {
        return this.request.getContentLength();
    }

    /**
     * 返回請求資料體的MIME型別CONTENT-TYPE,如果型別未知返回空值.
     * @return
     */
    public String getContentType() {
        return this.request.getContentType();
    }

    /**
     * 返回一個輸入流,使用該輸入流以二進位制方式讀取請求正文的內容.
     * javax.servlet.ServletInputStream是一個抽象類,繼承自InputStream.
     * @return .
     * @throws IOException .
     */
    public ServletInputStream getInputStream() throws IOException {
        return this.request.getInputStream();
    }

    /**
     * 根據指定引數名獲取引數值.
     * @param name 引數名.
     * @return 引數值.
     */
    public String getParameter(String name) {
        return this.request.getParameter(name);
    }

    /**
     * 獲取引數的Map形式,包括所有引數.
     * @return 引數Map.
     */
    public Map getParameterMap() {
        return this.request.getParameterMap();
    }

    /**
     * 獲取所有引數名的列舉.
     * @return 引數名列舉.
     */
    public Enumeration getParameterNames() {
        return this.request.getParameterNames();
    }

    /**
     * 根據指定屬性名獲取引數值陣列.
     * @param name 引數名.
     * @return 引數值陣列.
     */
    public String[] getParameterValues(String name) {
        return this.request.getParameterValues(name);
    }

    /**
     * 返回請求使用的協議資訊.格式為:協議/主版本號.次版本號.例如:http/1.0.
     * @return
     */
    public String getProtocol() {
        return this.request.getProtocol();
    }

    /**
     * 返回請求所使用的URL的模式.若http、https等.
     * @return 模式.
     */
    public String getScheme() {
        return this.request.getScheme();
    }

    /**
     * 返回請求傳送到的伺服器的主機名.
     * @return 主機名.
     */
    public String getServerName() {
        return this.request.getServerName();
    }

    /**
     * 返回請求傳送到的伺服器的埠號.
     * @return 埠號.
     */
    public int getServerPort() {
        return this.request.getServerPort();
    }

    /**
     * 返回BufferedReader物件,以位元組資料方式讀取請求正文.
     * @return .
     * @throws IOException .
     */
    public BufferedReader getReader() throws IOException {
        return this.request.getReader();
    }

    /**
     * 返回傳送請求的客戶端或最後一個代理伺服器的IP地址.
     * @return IP地址.
     */
    public String getRemoteAddr() {
        return this.request.getRemoteAddr();
    }

    /**
     * 返回傳送請求的客戶端或最後一個代理伺服器的主機名.
     * @return 主機名.
     */
    public String getRemoteHost() {
        return this.request.getRemoteHost();
    }

    /**
     * 根據傳遞的屬性名和屬性值設定Request屬性.
     * @param name 屬性名.
     * @param o 屬性值.
     */
    public void setAttribute(String name, Object o) {
        this.request.setAttribute(name, o);
    }

    /**
     * 從Request中刪除指定的屬性名對應的值.一般使用此方法.
     * @param name 屬性名.
     */
    public void removeAttribute(String name) {
        this.request.removeAttribute(name);
    }

    /**
     * 根據客戶端傳遞的Accept-Language對應的區域設定.
     * 若客戶端未指定Accept-Language,則返回伺服器預設語言環境.
     * @return
     */
    public Locale getLocale() {
        return this.request.getLocale();
    }

    /**
     * 返回Locale物件的列舉,從首選區域開始按降序返回基於Accept-Language頭的客戶端可接受的區域.
     * 如果客戶機請求不提供Accept-Language頭,此方法返回包含一個Locale的列舉,即是伺服器預設語言環境對應的Locale.
     * @return
     */
    public Enumeration getLocales() {
        return this.request.getLocales();
    }

    /**
     * 指示是否使用安全通道(如HTTPS)發出此請求.
     */
    public boolean isSecure() {
        return this.request.isSecure();
    }

    /**
     * 返回RequestDispatcher物件,作為path所定位的資源的封裝.
     * RequestDispatcher用於伺服器請求轉發.
     * @param path 相對路徑或絕對路徑.
     * @return RequestDispatcher.
     */
    public RequestDispatcher getRequestDispatcher(String path) {
        return this.request.getRequestDispatcher(path);
    }

    /**
     * @deprecated
     * Servlet API 2.1開始已不推薦使用此API.
     * 取得檔案在伺服器上的絕對路徑.
     * @param path 相對路徑.
     * @return 絕對路徑.
     */
    public String getRealPath(String path) {
        return this.request.getRealPath(path);
    }

    /**
     * 返回傳送請求的客戶端或者最後一個代理伺服器的IP源埠, 這個方法是在Servlet 2.4規範中新增的方法.
     * @return 埠號.
     */
    public int getRemotePort() {
        return this.request.getRemotePort();
    }

    /**
     * 返回接收到請求的IP介面的主機名,這個方法是在Servlet 2.4規範中新增的方法.
     * @return 主機名.
     */
    public String getLocalName() {
        return this.request.getLocalName();
    }

    /**
     * 返回接收到請求的網路介面的IP地址,這個方法是在Servlet 2.4規範中新增的方法.
     * @return IP地址.
     */
    public String getLocalAddr() {
        return this.request.getLocalAddr();
    }

    /**
     * 返回接收到請求的網路介面的IP埠號,這個方法是在Servlet 2.4規範中新增的方法.
     * @return 埠號.
     */
    public int getLocalPort() {
        return this.request.getLocalPort();
    }

}

HttpServletRequestWrapper的原始碼:

package javax.servlet.http;

import javax.servlet.ServletRequestWrapper;
import java.util.Enumeration;

/**
 * HttpServletRequestWrapper提供了一個簡便實現不了,在對HttpServletRequest有改動需求的時,可以重寫此類,之後Request的操作都會經過重寫的類.
 */
public class HttpServletRequestWrapper extends ServletRequestWrapper implements HttpServletRequest {

    /**
     * Constructs.
     */
    public HttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
    }
    
    /**
     * 獲取HttpServletRequest.
     * @return HttpServletRequest.
     */
    private HttpServletRequest _getHttpServletRequest() {
        return (HttpServletRequest) super.getRequest();
    }

    /**
     * 返回認證型別.
     * 所有Servlet容器都支援basic、form、client_cert,digest不一定支援.
     * 若不支援,則返回null.
     */
    public String getAuthType() {
        return this._getHttpServletRequest().getAuthType();
    }

    /**
     * 獲取請求中帶有的Cookie資訊.
     */
    public Cookie[] getCookies() {
        return this._getHttpServletRequest().getCookies();
    }

    /**
     * 以長整數形式返回一個特定的請求頭,該長整數代表一個Date物件. 
     * 該方法可以用在包含時間資訊的header中,如:If-Modified-Since.
     * @param name 頭名稱.
     * @return 頭值.
     */
    public long getDateHeader(String name) {
        return this._getHttpServletRequest().getDateHeader(name);
    }

    /**
     * 根據指定的頭名稱獲取頭的值.
     * 若存在多個,則返回第一個.
     * @param name 頭名稱.
     * @return 頭值.
     */
    public String getHeader(String name) {
        return this._getHttpServletRequest().getHeader(name);
    }

    /**
     * 根據指定的頭名稱獲取頭值的列舉.
     * 若沒有找到,則返回空的列舉.
     * @param name 頭名稱.
     * @return 頭值.
     */
    public Enumeration getHeaders(String name) {
        return this._getHttpServletRequest().getHeaders(name);
    }

    /**
     * 獲取所有的頭的列舉.
     * @return 頭的列舉.
     */
    public Enumeration getHeaderNames() {
        return this._getHttpServletRequest().getHeaderNames();
    }

    /**
     * 根據指定頭名稱獲取int型別的值.若未找到則返回-1,如不是int型別,則會丟擲NumberFormatException異常.
     * @param name 頭名稱.
     * @return 頭值.
     */
    public int getIntHeader(String name) {
        return this._getHttpServletRequest().getIntHeader(name);
    }

    /**
     * 獲取HTTP方法,如:GET、POST、PUT等.
     * @return 方法名.
     */
    public String getMethod() {
        return this._getHttpServletRequest().getMethod();
    }

    /**
     * 官網解釋:
     *  返回與客戶端發出此請求時傳送的URL相關聯的任何額外路徑資訊.
     *  額外的路徑資訊跟隨servlet路徑,但位於查詢字串之前,並以"/"字元開頭.
     * 例如:url-pattern配置為/demo/*,請求URL為http://localhost/Pro/demo/htm/index.html,則pathInfo為/htm/index.html.
     * @return
     */
    public String getPathInfo() {
        return this._getHttpServletRequest().getPathInfo();
    }

    /**
     * 返回servlet名稱之後、
     *      查詢字串之前的任何額外路徑資訊,並將其轉換為實際路徑. 
     *      與轉換的CGI變數PATH U的值相同
     * @return
     */
    public String getPathTranslated() {
        return this._getHttpServletRequest().getPathTranslated();
    }

    /**
     * 返回專案根路徑.
     * 例如:url-pattern配置為/demo/*,請求URL為http://localhost/Pro/demo/htm/index.html,則contextPath為/demo.
     * @return 專案根路徑.
     */
    public String getContextPath() {
        return this._getHttpServletRequest().getContextPath();
    }

    /**
     * 獲得請求中的查詢字串,例如a=1&b=2這樣的格式.
     * @return 查詢字串.
     */
    public String getQueryString() {
        return this._getHttpServletRequest().getQueryString();
    }

    /**
     * 如果使用者已經過驗證,則返回發出此請求的使用者的登入資訊,如果使用者未經過驗證,則返回 null.
     * 使用者名稱是否隨每個後續請求傳送取決於瀏覽器和驗證型別,返回的值與 CGI變數REMOTE_USER的值相同.
     * @return 使用者資訊.
     */
    public String getRemoteUser() {
        return this._getHttpServletRequest().getRemoteUser();
    }

    /**
     * 返回一個 boolean值,指示指定的邏輯"角色"中是否包含經過驗證的使用者.
     * 角色和角色成員關係可使用部署描述符定義.
     * 如果使用者沒有經過驗證,則該方法返回 false.
     * @param role 角色.
     * @return 已驗證使用者是否屬於某種角色.
     */
    public boolean isUserInRole(String role) {
        return this._getHttpServletRequest().isUserInRole(role);
    }

    /**
     * 返回包含當前已經過驗證的使用者的名稱的 java.security.Principal物件.
     * 如果使用者沒有經過驗證,則該方法返回 null.
     * @return java.security.Principal或null.
     */
    public java.security.Principal getUserPrincipal() {
        return this._getHttpServletRequest().getUserPrincipal();
    }

    /**
     * 獲取請求對應的sessionId.
     * @return sessionId.會話ID.
     */
    public String getRequestedSessionId() {
        return this._getHttpServletRequest().getRequestedSessionId();
    }

    /**
     * 請求URL的相對地址,包括伺服器路徑,不包括查詢引數.
     * @return 請求URL的相對地址.
     */
    public String getRequestURI() {
        return this._getHttpServletRequest().getRequestURI();
    }

    /**
     * 請求的URL地址,包含協議、主機名、埠和伺服器路徑,但是不包括查詢引數.
     * @return 請求URL地址.
     */
    public StringBuffer getRequestURL() {
        return this._getHttpServletRequest().getRequestURL();
    }

    /**
     * 官方解釋:
     *  返回此請求的URL中呼叫servlet的部分.
     *  此路徑以"/"字元開頭,包含servlet名稱或到servlet的路徑,但不包含任何額外的路徑資訊或查詢字串.
     *  與CGI變數SCRIPT_NAME的值相同.
     * 其實真是的意思就是,在配置webx.xml或程式設計式配置時,配置了url-pattern,請求的URL與url-pattern的有效部分重合部分就是servletPath,
     * 也可以理解為url-pattern的有效部分就是servletPath.
     * 例如:url-pattern配置為/demo/*,請求URL為http://localhost/Pro/demo/htm/index.html,則servletPath為/demo.
     * @return
     */
    public String getServletPath() {
        return this._getHttpServletRequest().getServletPath();
    }

    /**
     * 獲得請求對應的當前sesson.
     * 在沒有session的情況下:
     *  若create=true,則新建立一個session.
     *  若create=false,則不建立session,返回null.
     * @param create session失效的情況下,是否建立session.
     * @return HttpSession例項.
     */
    public HttpSession getSession(boolean create) {
        return this._getHttpServletRequest().getSession(create);
    }

    /**
     * 獲得請求對應的當前sesson.
     * 若沒有session,則建立一個session.
     * @return HttpSession例項.
     */
    public HttpSession getSession() {
        return this._getHttpServletRequest().getSession();
    }

    /**
     * 返回session是否有效.
     * @return session是否有效.
     */
    public boolean isRequestedSessionIdValid() {
        return this._getHttpServletRequest().isRequestedSessionIdValid();
    }

    /**
     * sessionId是否從Cookie中獲得.
     * @return 若是從Cookie中獲得,返回true,否則返回false.
     */
    public boolean isRequestedSessionIdFromCookie() {
        return this._getHttpServletRequest().isRequestedSessionIdFromCookie();
    }

    /**
     * sessionId是否從URL中獲得.
     * @return 若是從URL中獲得,返回true,否則返回false.
     */
    public boolean isRequestedSessionIdFromURL() {
        return this._getHttpServletRequest().isRequestedSessionIdFromURL();
    }

    /**
     * @deprecated
     * sessionId是否從URL中獲得.
     * @return 若是從URL中獲得,返回true,否則返回false.
     */
    public boolean isRequestedSessionIdFromUrl() {
        return this._getHttpServletRequest().isRequestedSessionIdFromUrl();
    }

}

 

其實檢視原始碼後會發現,ServletRequestWrapper和HttpServletRequestWrapper提供的API完全呼叫的ServletRequest和HttpServletRequest的API進行實現。在我們進行應用時,使用HttpServletRequestWrapper即可,它同時擁有ServletRequestWrapper和HttpServletRequestWrapper的功能。

裝飾者模式

我們來分析一下,HttpServletRequestWrapper的實現流程,首先,所有的源頭,都是ServletRequest這個介面,這個介面其實就是定義來請求功能的;然後HttpServletRequest實現了ServletRequest,它的物件就是我們在Filter常用的Request,ServletRequestWrapper也實現了ServletRequest這個介面,同時它內部又引用了ServletRequest作為成員變數,它和HttpServletRequest就有同樣了API,屬於同一層級,這時候,HttpServletRequestWrapper繼承了ServletRequestWrapper同時實現了HttpServletRequest,也就是集兩家之長,又能代替兩家,所以可以滿足我們一開始的場景需求(替換掉HttpServletRequest物件)。

以上這段分析描述,也就是裝飾模式的實現思路,下面開始介紹一下何為裝飾模式。

裝飾模式的定義與特點

裝飾(Decorator)模式的定義:指在不改變現有物件結構的情況下,動態地給該物件增加一些職責(即增加其額外功能)的模式,它屬於物件結構型模式。
裝飾(Decorator)模式的主要優點有:

    • 裝飾器是繼承的有力補充,比繼承靈活,在不改變原有物件的情況下,動態的給一個物件擴充套件功能,即插即用
    • 通過使用不用裝飾類及這些裝飾類的排列組合,可以實現不同效果
    • 裝飾器模式完全遵守開閉原則

其主要缺點是:裝飾模式會增加許多子類,過度使用會增加程式得複雜性

裝飾模式的結構與實現

通常情況下,擴充套件一個類的功能會使用繼承方式來實現。但繼承具有靜態特徵,耦合度高,並且隨著擴充套件功能的增多,子類會很膨脹。如果使用組合關係來建立一個包裝物件(即裝飾物件)來包裹真實物件,並在保持真實物件的類結構不變的前提下,為其提供額外的功能,這就是裝飾模式的目標。下面來分析其基本結構和實現方法。

模式的結構

裝飾模式主要包含以下角色。

    1. 抽象構件(Component)角色:定義一個抽象介面以規範準備接收附加責任的物件。
    2. 具體構件(ConcreteComponent)角色:實現抽象構件,通過裝飾角色為其新增一些職責。
    3. 抽象裝飾(Decorator)角色:繼承抽象構件,幷包含具體構件的例項,可以通過其子類擴充套件具體構件的功能。
    4. 具體裝飾(ConcreteDecorator)角色:實現抽象裝飾的相關方法,並給具體構件物件新增附加的責任。

模板的UML實現關係為:

細品之後,是不是發現這個UML和HttpServletRequestWrapper那個很相似 !!!!!其實ServletRequest充當了這裡抽象構件的角色,ServletRequestWrapper衝到了抽象裝飾的角色,而HttpServletRequest就是具體構件的位置。

下面看一個簡單的裝飾者實現例子:

package decorator;
public class DecoratorPattern {
    public static void main(String[] args) {
        Component p = new ConcreteComponent();
        p.operation();
        System.out.println("---------------------------------");
        Component d = new ConcreteDecorator(p);
        d.operation();
    }
}
//抽象構件角色
interface Component {
    public void operation();
}
//具體構件角色
class ConcreteComponent implements Component {
    public ConcreteComponent() {
        System.out.println("建立具體構件角色");
    }
    public void operation() {
        System.out.println("呼叫具體構件角色的方法operation()");
    }
}
//抽象裝飾角色
class Decorator implements Component {
    private Component component;
    public Decorator(Component component) {
        this.component = component;
    }
    public void operation() {
        component.operation();
    }
}
//具體裝飾角色
class ConcreteDecorator extends Decorator {
    public ConcreteDecorator(Component component) {
        super(component);
    }
    public void operation() {
        super.operation();
        addedFunction();
    }
    public void addedFunction() {
        System.out.println("為具體構件角色增加額外的功能addedFunction()");
    }
}

裝飾模式的應用場景

前面講解了關於裝飾模式的結構與特點,下面介紹其適用的應用場景,裝飾模式通常在以下幾種情況使用。

    • 當需要給一個現有類新增附加職責,而又不能採用生成子類的方法進行擴充時。例如,該類被隱藏或者該類是終極類或者採用繼承方式會產生大量的子類。
    • 當需要通過對現有的一組基本功能進行排列組合而產生非常多的功能時,採用繼承關係很難實現,而採用裝飾模式卻很好實現。
    • 當物件的功能要求可以動態地新增,也可以再動態地撤銷時。


裝飾模式在Java中的最著名的應用莫過於 Java I/O 標準庫的設計了。例如,InputStream 的子類 FilterInputStream,OutputStream 的子類 FilterOutputStream,Reader 的子類 BufferedReader 以及 FilterReader,還有 Writer 的子類 BufferedWriter、FilterWriter 以及 PrintWriter 等,它們都是抽象裝飾類。

下面程式碼是為 FileReader 增加緩衝區而採用的裝飾類 BufferedReader 的例子:

BufferedReader in = new BufferedReader(new FileReader("filename.txt"));
String s = in.readLine();

裝飾模式的擴充套件

(1) 如果只有一個具體構件而沒有抽象構件時,可以讓抽象裝飾繼承具體構件,其結構圖如圖

(2) 如果只有一個具體裝飾時,可以將抽象裝飾和具體裝飾合併,其結構圖如圖 


該型別文章目錄:設計模式彙總


相關文章