头图

设计模式中的俄罗斯套娃:装饰者(Decorator)模式

叉叉哥
English

俄罗斯套娃想必大家都不陌生,就是同一种玩具娃娃大的套小的,然后一层一层嵌套下去。

俄罗斯套娃

在设计模式中,有一种常用的套娃模式,叫做装饰者(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论道 关注我

阅读 471

Java论道
定期分享Java技术文章,囊括Spring全家桶、Linux、架构设计、分布式、微服务、消息中间件、面试题等。微...
3.7k 声望
52 粉丝
0 条评论
你知道吗?

3.7k 声望
52 粉丝
文章目录
宣传栏