設計模式中的俄羅斯套娃:裝飾者(Decorator)模式

叉叉哥發表於2021-10-20

俄羅斯套娃想必大家都不陌生,就是同一種玩具娃娃大的套小的,然後一層一層巢狀下去。

俄羅斯套娃

在設計模式中,有一種常用的套娃模式,叫做裝飾者(Decorator)模式,又稱為包裝(Wrapper)模式。

HttpServletRequest 套娃

在 Spring 框架開發的 Web 應用中,如果使用了 Spring Security 或 Spring Session,用 Debug 模式觀察一下某個請求對應的 HttpServletRequest 物件,會發現這就是一個俄羅斯套娃:

HttpServletRequest 物件

圖中可以看到我們拿到的 HttpServletRequest 物件,內部成員中包含了一個 HttpServletRequest 物件,而這個內部的 HttpServletRequest 物件內部又包含了一個 HttpServletRequest 物件,層層包含,層層套娃。這就是一個典型的裝飾者模式。

我們知道,HttpServletRequest 是 Servlet 規範中提供的一個 interface 介面。Servlet 規範本身沒有實現 HttpServletRequest 介面,HttpServletRequest 介面一般是由 Servlet 容器來實現,例如 Tomcat、Jetty。如果 Spring Security、Spring Session 等框架想要增強 HttpServletRequest 物件的功能,但是不改變原有物件的介面,最好的辦法就是使用裝飾者模式。例如:

  • Spring Security 增強了 HttpServletRequest.getRemoteUser() 方法,可返回當前通過 Spring Security 框架登入使用者的使用者名稱;
  • Spring Session 增強了 HttpServletRequest.getSession() 方法,增強後的 Session 取代了 Servlet 容器的預設實現,其讀寫可以使用一個集中式的儲存,例如 Redis,這樣可以方便叢集中的多個例項共享 Session。

HttpServletRequestWrapper / ServletRequestWrapper

javax.servlet.http 包下有個 HttpServletRequestWrapper[原始碼],繼承自 ServletRequestWrapper[原始碼]。可以看到這兩個類上的註釋:

This class implements the Wrapper or Decorator pattern. Methods default to calling through to the wrapped request object.

翻譯:這個類實現了裝飾者模式/包裝模式,方法模式會直接呼叫內部包裝的 request 物件。

ServletRequestWrapper 本身實現了 ServletRequest 介面,它的構造方法要求傳入另一個 ServletRequest 物件,並將這個物件賦值給內部 request 物件:

public class ServletRequestWrapper implements ServletRequest {

    private ServletRequest request;

    public ServletRequestWrapper(ServletRequest request) {
        if (request == null) {
            throw new IllegalArgumentException("Request cannot be null");   
        }
        this.request = request;
    }
    
    // ...
}

ServletRequestWrapperServletRequest 介面方法的實現,則是直接呼叫內部 request 物件對應的方法:

public String getContentType() {
    return this.request.getContentType();
}

public ServletInputStream getInputStream() throws IOException {
    return this.request.getInputStream();
}

public String getParameter(String name) {
    return this.request.getParameter(name);
}

// ...

以上就是一個最基本的裝飾器。我們可以直接拿來套娃:

HttpServletRequest request = ...; // 已有的 request 物件
HttpServletRequest requestWrapper = new HttpServletRequestWrapper(request); // 包裝後的物件

當然,上面程式碼沒有任何意義,因為 requestWrapper 沒有做任何擴充套件,使用 requestWrapper 物件和直接用 request 物件沒有任何區別。真正的裝飾者類會繼承 ServletRequestWrapper 並在此基礎上做增強。

下面,我們再看下 Spring Security 和 Spring Session 如何對 HttpServletRequest 物件進行裝飾。

Spring Security / Spring Session 中的裝飾者實現

在 Spring Security 文件 Servlet API integration 中,可以看到 Spring Security 框架對 HttpServletRequest 物件的 getRemoteUser()getUserPrincipal()isUserInRole(String) 等方法進行了增強,例如 getRemoteUser() 方法可以直接返回當前登入使用者的使用者名稱。接下來看一下 Spring Security 如何增強這些方法。

首先,Spring Security 提供了一個過濾器 SecurityContextHolderAwareRequestFilter,對相關請求進行過濾處理。在 SecurityContextHolderAwareRequestFilter 第 149 行 結合 HttpServlet3RequestFactory 第 163 行 可以看到,這個 Filter 中建立了一個新的 Servlet3SecurityContextHolderAwareRequestWrapper 物件,這個類繼承自 HttpServletRequestWrapper 類,並增強了相關方法。其父類 SecurityContextHolderAwareRequestWrapper[原始碼]中可以看到對 getRemoteUser() 方法的增強:

public class SecurityContextHolderAwareRequestWrapper extends HttpServletRequestWrapper {

    @Override
    public String getRemoteUser() {
        Authentication auth = getAuthentication();
        if ((auth == null) || (auth.getPrincipal() == null)) {
            return null;
        }
        if (auth.getPrincipal() instanceof UserDetails) {
            return ((UserDetails) auth.getPrincipal()).getUsername();
        }
        if (auth instanceof AbstractAuthenticationToken) {
            return auth.getName();
        }
        return auth.getPrincipal().toString();
    }
    
    // ...
}

簡單來講,就是 Spring Security 通過一個 Filter 過濾相關請求,拿到原始的 HttpServletRequest 物件,通過一個繼承自 HttpServletRequestWrapper 類的裝飾者,增強了 getRemoteUser() 等相關方法,再將增強後的物件傳給後續的業務處理,那麼後續我們在 Controller 層拿到的 HttpServletRequest 物件就可以直接使用 getRemoteUser() 等方法。

Spring Session 實現和 Spring Security 類似,這裡就不再重複介紹,有興趣可以看 SessionRepositoryFilter 原始碼

Collections 中的裝飾者

裝飾者模式不但可以增強被裝飾者的功能,還可以禁用某些功能。當然,禁用實際上也是一種“增強”。

例如,假設有一個 List,當我們需要將這個 List 傳給第三方的某個方法去讀,但是由於這個第三方方法不可信,為了防止這個方法對 List 篡改,可以通過裝飾器模式禁用 List 的修改方法,裝飾成一個只讀的 List。

java.util.Collections 中提供了一個靜態方法 unmodifiableList(List),用於將一個 List 封裝為只讀的 List:

List<String> list = ...;
List<String> unmodifiableList = Collections.unmodifiableList(list);

通過這個方法的原始碼可以看到,Collections.unmodifiableList(List) 方法實際上返回了一個 UnmodifiableListUnmodifiableList 是一個典型的裝飾者,其內部對 List 的讀相關方法直接呼叫被裝飾物件的對應方法,而對寫相關方法做了限制,丟擲 UnsupportedOperationException。下面是 UnmodifiableList 的部分原始碼:

static class UnmodifiableList<E> extends UnmodifiableCollection<E>
                                  implements List<E> {
    final List<? extends E> list;

    UnmodifiableList(List<? extends E> list) {
        super(list);
        this.list = list;
    }

    public E get(int index) {
        return list.get(index);
    }
    public E set(int index, E element) {
        throw new UnsupportedOperationException();
    }
    public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }
    public E remove(int index) {
        throw new UnsupportedOperationException();
    }
    public int indexOf(Object o) {
        return list.indexOf(o);
    }
    public int lastIndexOf(Object o) {
        return list.lastIndexOf(o);
    }
    public boolean addAll(int index, Collection<? extends E> c) {
        throw new UnsupportedOperationException();
    }
    
    // ...
}

java.util.Collections 中還提供了其他一系列裝飾者:

  • unmodifiableSet(Set)unmodifiableMap(Map) 等方法和 unmodifiableList(List) 類似,用於不同型別的集合的裝飾
  • synchronizedList(List)synchronizedSet(Set)synchronizedMap(Map) 等方法使用 synchronized 裝飾 List、Set、Map 中的相關方法,返回一個執行緒安全的集合
  • checkedList(List, Class)checkedSet(Set, Class)checkedMap(List, Class, Class) 等方法返回型別安全的集合,如果插入集合的元素型別不符合要求則會丟擲異常

InputStream 裝飾者

裝飾者不但可以增強被裝飾者原有的方法,還可以增加新的方法擴充套件功能。

java.io 包中,針對 InputStream 有一個基礎的抽象裝飾者 FilterInputStream,其原始碼如下:

public class FilterInputStream extends InputStream {

    protected volatile InputStream in;

    protected FilterInputStream(InputStream in) {
        this.in = in;
    }

    public int read() throws IOException {
        return in.read();
    }
    
    // ...
}

類似於上面講到的 HttpServletRequestWrapper 類,FilterInputStream 是一個基礎的裝飾者,它的子類才是具體的裝飾者的實現。DataInputStream 就是其中一個典型的裝飾者實現。

DataInputStream 用於從被裝飾的 InputStream 物件中讀取基本資料型別,它繼承自 FilterInputStream,並新增了新的方法,如 readByte()readInt()readFloat() 等,這些方法是 InputStream 介面中沒有的。

除了 DataInputStream 之外,FilterInputStream 常見的子類裝飾者還有:

  • BufferedInputStream 為被裝飾的 InputStream 提供緩衝功能以及支援 markreset 方法
  • CipherInputStream 使用加密演算法(例如 AES)對 InputStream 中的資料加密或解密
  • DeflaterInputStreamInflaterInputStream 使用 deflate 壓縮演算法對 InputStream 中的資料壓縮或解壓

裝飾者模式結構

裝飾者模式結構

圖片來源: https://refactoringguru.cn/de...

下面總結一下在前面的例子中,各個類和上圖中的對應關係:

  • 部件(Component)對應有 HttpServletRequestListInputStream
  • 基礎裝飾(Base Decorator)對應有 HttpServletRequestWrapperFilterInputStream
  • 具體裝飾類(Concrete Decorators)對應有 Servlet3SecurityContextHolderAwareRequestWrapperUnmodifiableListDataInputStream

關注我的公眾號

微信搜一搜 Java論道 關注我

相關文章