Skip to content
Go back

设计模式面试回答——单例、工厂、策略、责任链与代理

设计模式面试回答

① 单例模式

一句话结论(30s)

单例的本质是保证一个类全局只有一个实例,核心设计决策是「懒加载 vs 线程安全 vs 防破坏三者怎么权衡」。因为 JVM 保证类的 <clinit> 只执行一次且线程安全,所以最省心的写法是静态内部类(懒加载+无锁)和枚举(还能防反射/反序列化);Spring 的 Bean 默认就是单例,靠的就是「容器注册表只存一份实例」这一套思想。

核心原理(2min)

五种写法按「懒加载/线程安全/防反射/防序列化」四个维度递进:

先想:单例的四个要求——懒加载、线程安全、防反射、防序列化,凭什么没有一个写法能全占?——因为「懒加载」要延迟创建、「线程安全」要加锁、「防反射/序列化」要 JVM 的特殊语义,三者天然拉扯。五种写法的演进,本质就是在这四个维度间做取舍、逐步补短板。

写法懒加载线程安全防反射防序列化一句话点评
饿汉式最简单,类加载即创建
synchronized 懒汉每次 getInstance 都争锁,慢
DCL(volatile+双重检查)经典,volatile 不能省
静态内部类 Holder最优雅,无锁无 volatile
枚举✗(类加载创建)最安全,Josh Bloch 推荐

关键机制:DCL 里 volatile 防的是指令重排——instance = new Singleton() 不是原子的,JIT 可能先把引用指向内存、再执行构造器,另一个线程在外层 if 看到 instance != null 就返回了「半成品对象」,导致 NPE。volatile 的 StoreStore 屏障禁止「引用赋值」在「构造器初始化」之前执行。模拟一下重排的坑: 线程 A 先把引用赋了出去、构造器还没跑完,线程 B 恰好在外层 if 看到 instance != null 就 return,拿到的对象字段全是默认值——这就是 DCL 不能省 volatile 的原因。

底层深入(5-10min)

(复述博客《单例模式的5种写法》的演进逻辑)

为什么 Holder 是瑞士军刀? getInstance() 首次被调时才触发内部类 Holder 的类加载,JVM 用 _init_lock 保证 <clinit> 不会多线程并发执行,所以它天然线程安全,还不用 synchronized/volatile。拆开想两层:Holder 凭什么「懒」?凭什么「线程安全」? 懒在「类加载是被动触发的」——只有首次调用 getInstance 才加载 Holder;安全在 JVM 的 _init_lock 保证 只执行一次且串行,把加锁的活儿外包给了 JVM。

为什么枚举最安全? 枚举实例由 JVM 保证全局唯一,且三招都免疫:

  1. 反射 Constructor.newInstance() 对枚举直接抛 IllegalArgumentException
  2. 反序列化时 ObjectInputStream 对枚举走 Enum.valueOf,不会 new 新实例;
  3. 不用自己写 readResolve()、不用写 private 构造器。

这三招不是「套路」,而是写在 JDK 源码里的硬校验。反射这条,Constructor.newInstance() 一进来就先看类是不是枚举:

// java.lang.reflect.Constructor#newInstance(JDK 8)
public T newInstance(Object ... initargs)
    throws InstantiationException, IllegalAccessException,
           IllegalArgumentException, InvocationTargetException
{
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, null, modifiers);
        }
    }
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
    ConstructorAccessor ca = constructorAccessor;
    if (ca == null) {
        ca = acquireConstructorAccessor();
    }
    T inst = (T) ca.newInstance(initargs);
    return inst;
}

反序列化 + clone 这两条也都在 java.lang.Enum 里被堵死——不是靠你自己写 readResolve(),而是父类直接把口子焊上:

// java.lang.Enum
protected final Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();   // 枚举不允许克隆
}

private void readObject(ObjectInputStream in) throws IOException,
    ClassNotFoundException {
    throw new InvalidObjectException("can't deserialize enum");   // 枚举不允许反序列化
}

这三招免疫靠的是「JVM 对枚举的特殊处理」,而不是你自己写的防御代码——这是它和 DCL/Holder 的本质区别。 反射校验在 newInstance 第一层、克隆校验在 clone、反序列化校验在 readObject,三个口子在 JDK 里都被预先封死,你连机会都没有。

落到源码:Spring 的 Bean 默认单例。 DefaultSingletonBeanRegistry 用一个 Map<String, Object> singletonObjects(三级缓存中的一级)存单例,getSingleton 时先查这个 map:

// org.springframework.beans.factory.support.DefaultSingletonBeanRegistry
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);   // 一级:成品单例
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);  // 三级:工厂
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16); // 二级:早期引用

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 先直接查一级缓存,命中就返回
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
            synchronized (this.singletonObjects) {
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    singletonObject = this.earlySingletonObjects.get(beanName);
                    if (singletonObject == null) {
                        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            singletonObject = singletonFactory.getObject();
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
        }
    }
    return singletonObject;
}

这就是「容器注册表 + 单例」的落地——singletonObjects 就是那张「只存一份实例」的注册表。但注意 Spring 的单例是「每个 IoC 容器里一个实例」,跟 GoF 单例「JVM 全局唯一」语义略有不同。想深一层:Spring 的单例和 GoF 单例是同一回事吗? 不是——Spring 是「每容器一个」,两个容器里同一 Bean 就是两个实例;GoF 是「JVM 全局唯一」,语义范围不同,面试时别混。


② 工厂模式(工厂方法 / 抽象工厂)

一句话结论(30s)

工厂模式的本质是把「创建对象的细节」从调用方剥离,核心决策是「按产品维度拆(工厂方法)还是按产品族拆(抽象工厂)」。因为 Spring 要创建大量需要复杂配置的对象,所以把创建逻辑封装成 FactoryBeangetBean("xxx") 拿产品而不是工厂。

核心原理(2min)

三种工厂逐级抽象:

先想:工厂到底「藏」了什么?——藏的是「创建对象的细节」。调用方只要产品,不关心怎么 new、怎么配置。想清楚这一点,三种工厂的区别就只是「按什么维度去藏」。

Spring 里哪里用了? 最典型的是 FactoryBean。它是「工厂本身也是 Bean」,但 getBean("xxx") 拿到的是工厂生产的产品:

@Component("sqlSessionFactory")
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory> {
    @Override
    public SqlSessionFactory getObject() {
        return new SqlSessionFactoryBuilder().build(configuration);
    }
    @Override
    public Class<?> getObjectType() { return SqlSessionFactory.class; }
}
// 拿产品
SqlSessionFactory f = ctx.getBean(SqlSessionFactory.class);
// 拿工厂本身(& 前缀)
SqlSessionFactoryBean bean = ctx.getBean("&sqlSessionFactory", SqlSessionFactoryBean.class);

& 前缀来自 BeanFactoryUtils.FACTORY_BEAN_PREFIX = "&"想想为什么拿工厂还要加个 & 前缀: 因为 getBean 默认返回 getObject() 的产品,& 前缀是唯一能拿到「工厂这个 Bean 本身」的钥匙。

底层深入(5-10min)

(复述博客《工厂模式-Spring-FactoryBean》)

为什么需要 FactoryBean? 普通 @Bean 方法用 new 创建复杂对象时配置代码冗长。FactoryBean 把「如何创建这个复杂对象」封装到独立类,容器 BeanDefinition 里注册的是 FactoryBean 本身,产品在真正被 getBean 时才通过 getObject() 懒创建想想产品是什么时候被 new 出来的: 注册时只存 BeanDefinition,真正 getBean 才走 getObject() 创建——把「创建」推迟到「要用」的那一刻,这也是懒加载的体现。

Spring 的 FactoryBean 接口本身只有三个方法,定义非常克制:

// org.springframework.beans.factory.FactoryBean
public interface FactoryBean<T> {
    String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";

    @Nullable
    T getObject() throws Exception;   // 返回产品:真正 getBean 时才调用

    @Nullable
    Class<?> getObjectType();         // 返回产品类型:autowire 时不用实例化就能判断

    default boolean isSingleton() {   // 默认单例
        return true;
    }
}

getObject() 是工厂方法,getObjectType() 让容器在 autowire 时不用真的 new 就知道类型,isSingleton() 默认 true——产品会被容器缓存、只创建一次。

源码落地例子:MyBatis-Spring 的 SqlSessionFactoryBean、Spring 的 ProxyFactoryBean(AOP 代理的创建也是工厂)。抽象工厂在 JDBC 里也有体现:DriverManager.getConnection 返回 ConnectionConnection.createStatement() 返回配套的 Statement——同一驱动下 Connection/Statement/ResultSet 是一整套产品族。


③ 模板方法模式

一句话结论(30s)

模板方法的本质是父类固定流程骨架(final 方法锁死步骤顺序),子类只填可变步骤,核心思想是好莱坞原则「别调我,我调你」。因为 JDBC 的「获取连接→执行→翻译异常→关闭」这段样板代码每个 DAO 都要写一遍,所以 Spring 把它抽成 JdbcTemplate 模板,你只写 SQL 回调。

核心原理(2min)

父类定义算法骨架,final 方法固定步骤顺序,可变步骤声明为 abstract 交给子类:

先想:「模板方法」模板的到底是什么?——模板的是「步骤顺序」。哪些步骤固定(连接、关闭、异常翻译),哪些步骤可变(SQL、数据处理),父类定顺序、子类填空。

abstract class DataExporter {
    public final void export() {          // final: 子类不能改流程
        List<Data> data = fetchData();    // 步骤1
        String formatted = format(data);  // 步骤2
        write(formatted);                 // 步骤3
    }
    abstract List<Data> fetchData();
    abstract String format(List<Data> data);
    abstract void write(String output);
}

想想为什么骨架方法要加 final: 防止子类整个重写 export、把步骤顺序改乱。流程控制权归父类,子类只能填可变步骤——这就是好莱坞原则「别调我,我调你」的结构前提。

Spring 里哪里用了? JdbcTemplate.execute(StatementCallback):模板负责「获取连接、创建 Statement、把 SQLException 翻译成 Spring 的 DataAccessException、关闭资源」,回调 action.doInStatement 只写纯 SQL,不写 try-catch-finally、不手动 close。

底层深入(5-10min)

(复述博客《模板方法模式-JdbcTemplate》)

算一笔账:100 个 DAO 方法 = 100 份重复的「获取连接-异常翻译-关闭」代码,模板方法把这 100 份合并成 1 份,回调只剩纯业务。想想省下来的到底是什么: 不是打字量,是「漏 close、翻译错异常、顺序写错」这类重复 bug 的 100 个复读机——合并成 1 份后,改一次就全改。

落到 JdbcTemplate.execute 源码,模板方法的「骨架 vs 可变」一目了然:

// org.springframework.jdbc.core.JdbcTemplate
@Override
@Nullable
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
    return execute(action, true);   // 骨架入口:复用下面的私有模板
}

@Nullable
private <T> T execute(StatementCallback<T> action, boolean closeResources) throws DataAccessException {
    Assert.notNull(action, "Callback object must not be null");

    Connection con = DataSourceUtils.getConnection(obtainDataSource());   // 步骤1:拿连接
    Statement stmt = null;
    try {
        stmt = con.createStatement();                                      // 步骤2:建 Statement
        applyStatementSettings(stmt);
        T result = action.doInStatement(stmt);                             // 步骤3:可变——回调只写 SQL
        handleWarnings(stmt);
        return result;
    }
    catch (SQLException ex) {
        // 提前释放连接,避免连接池死锁
        if (stmt != null) {
            handleWarnings(stmt, ex);
        }
        String sql = getSql(action);
        JdbcUtils.closeStatement(stmt);
        stmt = null;
        DataSourceUtils.releaseConnection(con, getDataSource());
        con = null;
        throw translateException("StatementCallback", sql, ex);            // 步骤4:异常翻译
    }
    finally {
        if (closeResources) {
            JdbcUtils.closeStatement(stmt);                                 // 步骤5:关资源
            DataSourceUtils.releaseConnection(con, getDataSource());
        }
    }
}

「拿连接、建 Statement、翻译异常、关资源」全部由模板方法接管,回调 doInStatement 只剩一句纯 SQL——这就是模板方法「父类锁骨架、子类填空」的工业级落地。

Spring 里第二个模板方法AbstractApplicationContext.refresh() 是容器启动的模板方法——14 个步骤固定顺序(obtainFreshBeanFactoryinvokeBeanFactoryPostProcessorsregisterBeanPostProcessorsfinishBeanFactoryInitialization 等),子类 AnnotationConfigApplicationContext 只重写个别步骤。

同一套路的三兄弟

框架模板方法回调
JdbcTemplateexecute(连接+异常+关闭)doInStatement(SQL)
HibernateTemplateexecute(Session 管理)doInHibernate(HQL)
RestTemplateexecute(HTTP 连接)RequestCallback

好莱坞原则:控制权颠倒——父类控制流程主动回调子类,子类不知道何时被调,只知道被调时做什么。这也解释了为什么模板方法的骨架方法要用 final 防子类破坏流程顺序。再品「别调我,我调你」: 子类不主动调用框架,而是框架在合适的时机回调子类填好的实现——控制权从子类手里夺回了父类/框架手里。


④ 策略模式

一句话结论(30s)

策略模式的本质是把「可替换的算法」抽成接口,运行时由调用方选择哪个实现,核心收益是消灭 if-else、符合开闭原则。因为 Spring 能把所有策略实现收集进一个 Map,所以最佳实践是 @Autowired Map<String, PayStrategy> 一行注入,新增策略只加一个 @Component 类。

核心原理(2min)

策略模式 = 策略接口 + N 个实现 + 一个上下文,上下文持有一个策略接口引用,调用方传 key 选算法:

先想:if-else 和策略,本质区别是什么?——if-else 把「选哪个」和「怎么执行」写死在同一个方法里;策略把「怎么执行」拆成独立类,把「选哪个」交给 Map 路由。

public interface PayStrategy {
    void pay(BigDecimal amount);
    String getType();   // 策略标识
}

@Service
public class PayContext {
    @Autowired
    private Map<String, PayStrategy> strategyMap;  // key=Bean 名

    public void pay(String type, BigDecimal amount) {
        PayStrategy s = strategyMap.get(type);
        if (s == null) throw new IllegalArgumentException("不支持");
        s.pay(amount);
    }
}

Spring 检测到注入目标是 Map<String, T>,就收集所有实现 T 的 Bean,以 Bean 名为 key 注入。新增支付方式 = 新建一个 @Component 类实现 PayStrategy,零修改 PayContext。策略的注册、发现、路由全由容器完成。想想 Spring 怎么知道要收集所有实现: 它检测到注入目标是 Map<String, T> 时,就扫描所有 T 的实现 Bean,以 Bean 名为 key 塞进 Map——「一行注入」背后是容器替你把注册、发现、路由全干了。

底层深入(5-10min)

(复述博客《策略模式-Map注入消除if-else》)

为什么比 if-else 好? 因为 if-else 每次新增支付方式都要改 pay() 方法本身,违反开闭原则;20 个分支后圈复杂度爆炸、单测难写。策略模式把「变化」隔离到一个个独立类里,每个策略可独立测试、独立上线。自己推演 20 种支付方式下的 if-else: 每加一种就要改一次 pay()、圈复杂度一路涨、单测要构造 20 个分支,改一处怕动全局——策略模式把每个「怎么执行」关进独立类,新增只加类、不改上下文。

JDK 里最朴素的策略模式是 Comparator——把「怎么比」抽成一个接口,运行时传不同的实现:

// java.util.Comparator —— JDK 自带的策略模式
@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);   // 唯一的抽象算法,调用方决定怎么比

    boolean equals(Object obj);
    // JDK 8 起还有 default 方法:reversed()、thenComparing() ...
}

// 同一个 sort,策略不同结果不同
users.sort((a, b) -> a.getAge() - b.getAge());            // 策略1:按年龄
users.sort(Comparator.comparing(User::getName));          // 策略2:按名字

Collections.sort / Stream.sorted 都是「固定排序骨架 + 可替换比较算法」——排序流程是模板,compare 才是策略。这和业务里 @Autowired Map<String, PayStrategy> 是同一套思想:接口统一、实现可替换、调用方选 key。

策略 vs 工厂 vs 状态(结构上策略和状态几乎一样,区别在使用方式):

策略工厂状态
目的算法可替换创建对象并隐藏实现状态改变行为
选择时机运行时由调用方决定创建时由工厂决定内部状态机自动切换
典型特征调用方传 key 选择方法直接返回产品内部状态驱动

策略由外部显式选择(调用方知悉),状态由内部状态机自动切换(调用方不知情)。这是面试里常被追问的「策略 vs 状态」考点。先自己分辨,再看结论:策略和状态结构一模一样,怎么区分? 看「谁在选择」——策略是调用方显式传 key,状态是内部状态机根据自身状态自动切换,调用方根本不知道换了行为。


⑤ 责任链模式

一句话结论(30s)

责任链的本质是把「一串处理步骤」拆成独立 Handler 串联,请求沿链传递、谁处理谁终止(或处理完继续传)。核心决策是「纯责任链 vs 不纯责任链」——因为把消息发送的 7 种校验逻辑重构成责任链后,圈复杂度能从 23 降到每个 Handler ≤ 3。

核心原理(2min)

先想:责任链解决的核心痛点是什么?——一个方法里塞 7 种校验逻辑,圈复杂度飙到 23,改一处怕动全身。拆成链后,每个 Handler 只关心自己那一件事。

一个典型的责任链重构:消息发送从 2 种扩展到 7 种类型,每种消息校验逻辑不同,原来全堆在一个方法里,圈复杂度 23。重构为责任链后每个校验规则独立成一个 Handler:

abstract class MessageHandler {
    abstract boolean handle(MessageContext ctx);
    void compensate(MessageContext ctx) { }  // 失败补偿(默认空)
}

boolean executeChain(List<MessageHandler> handlers, MessageContext ctx) {
    int executed = 0;
    for (; executed < handlers.size(); executed++) {
        if (!handlers.get(executed).handle(ctx)) break;
    }
    // 逆序补偿:做得越多撤得越彻底
    for (int i = executed - 1; i >= 0; i--) {
        handlers.get(i).compensate(ctx);
    }
    return executed == handlers.size();
}

效果:圈复杂度 23 → 每个 Handler ≤ 3,单元测试覆盖率 34% → 92%。

底层深入(5-10min)

(复述博客《责任链模式-Servlet-FilterChain》+ 对比 Netty)

Tomcat FilterChain:迭代 + pos 索引(不是递归),避免深链路栈溢出。真实源码在 org.apache.catalina.core.ApplicationFilterChain

// org.apache.catalina.core.ApplicationFilterChain(Tomcat 9)
private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
private int pos = 0;   // 当前游标
private int n = 0;     // Filter 总数

private void internalDoFilter(ServletRequest request, ServletResponse response)
        throws IOException, ServletException {
    // 还有下一个 Filter,就继续往后走
    if (pos < n) {
        ApplicationFilterConfig filterConfig = filters[pos++];
        try {
            Filter filter = filterConfig.getFilter();
            if (request.isAsyncSupported() &&
                    "false".equalsIgnoreCase(filterConfig.getFilterDef().getAsyncSupported())) {
                request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
            }
            ...
            filter.doFilter(request, response, this);  // Filter 内部回调 chain.doFilter() → 方法重入
        } catch (IOException | ServletException | RuntimeException e) {
            throw e;
        } catch (Throwable e) {
            e = ExceptionUtils.unwrapInvocationTargetException(e);
            ExceptionUtils.handleThrowable(e);
            throw new ServletException(sm.getString("filterChain.filter"), e);
        }
        return;
    }
    // 链走完(pos == n),落到 Servlet
    ...
    servlet.service(request, response);
}

关键就是 pos 游标:pos < nfilters[pos++] 取下一个、把 this(链本身)传给 filter.doFilter,Filter 处理完再调 chain.doFilter 就重入同一个方法、pos 已经 +1,自然推进到下一个。想想为什么用「迭代 + pos」而不是递归: 递归一层套一层,Filter 数量一多就栈溢出;用 pos 索引推进,每步都返回再重入,栈深度始终是 O(1)。

Spring HandlerInterceptor:逆序 afterCompletion。A、B、C 都执行了 preHandle,C 返回 false → triggerAfterCompletion 从 C 开始逆序调 C→B→A 的 afterCompletion。理由是对称性:先做的先回滚会留下「B 的预操作还在但 A 已回滚」的不对称,逆序保证「做得越多,撤得越彻底」。自己推一下为什么必须逆序: 正序回滚会导致「A 先撤、B 还在」,中间状态不对称;逆序让「后做的先撤」,始终回到上一个一致状态。

Netty Pipeline:双向链表责任链(复述博客《Netty的Reactor模式》)——入站 Head→Tail,出站 Tail→Head,Handler 间用 fireChannelRead(msg) 传递事件,每个 Channel 独立一条 Pipeline。

实现链结构失败处理
Tomcat Filter迭代 + pos 索引直接返回,不调后续
Spring Interceptor迭代 + interceptorIndex逆序 afterCompletion
Netty Pipeline双向链表异常沿链传递
业务消息链List + 索引逆序 compensate

⑥ 代理模式(JDK / CGLIB)

一句话结论(30s)

代理的本质是用代理对象控制对目标对象的访问,调用方无感知。核心决策是「JDK 接口代理 vs CGLIB 子类代理」——因为 JDK 代理要求接口且靠反射调用(慢),CGLIB 用子类 + FastClass 索引非反射调用(快 2-3 倍),所以 Spring Boot 2.x 强制默认 CGLIB。

核心原理(2min)

先想:JDK 动态代理为什么必须要有接口?——因为生成的代理类 extends Proxy 并 implements 你的接口,Java 单继承,只能靠接口去「冒充」目标类型。

Spring AOP 什么时候用哪个? 目标有接口且注入接口类型 → JDK 代理;无接口、或字段注入实现类 → CGLIB。Spring Boot 2.x 起 proxy-target-class 默认 true,强制 CGLIB,原因是 @Autowired UserServiceImpl 这种字段注入需要代理对象是 UserServiceImpl 实例(JDK 代理只实现了接口,不是实现类)。试着自己推理为什么字段注入实现类必须 CGLIB: JDK 代理生成的类只 implements 接口、不是 UserServiceImpl 的子类,注入 UserServiceImpl 类型字段时类型对不上;CGLIB 生成的是 UserServiceImpl 的子类,类型兼容。

底层深入(5-10min)

(复述博客《Spring-AOP-JDK动态代理-vs-CGLIB》)

JDK 代理的核心在 JdkDynamicAopProxy.invoke——它自己实现 InvocationHandlerProxy.newProxyInstance 时把 this 传进去,之后每次方法调用都进这个 invoke

// org.springframework.aop.framework.JdkDynamicAopProxy
public Object getProxy(@Nullable ClassLoader classLoader) {
    return Proxy.newProxyInstance(determineClassLoader(classLoader), this.proxiedInterfaces, this);
}

@Override
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    ...
    if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
        return equals(args[0]);          // equals/hashCode 特殊处理
    }
    else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
        return hashCode();
    }
    ...
    // 取该方法的拦截器链
    List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
    if (chain.isEmpty()) {
        // 没有 advice,直接反射调目标方法,不建 MethodInvocation
        Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
        retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
    }
    else {
        // 有 advice:构造 MethodInvocation,沿链 proceed
        MethodInvocation invocation =
                new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
        retVal = invocation.proceed();
    }
    return retVal;
}

CGLIB 代理的核心在 CglibAopProxy.DynamicAdvisedInterceptor.intercept——它是 CGLIB 的 MethodInterceptor 回调,比 JDK 版多了个 MethodProxy 参数:

// org.springframework.aop.framework.CglibAopProxy.DynamicAdvisedInterceptor
@Override
@Nullable
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
    ...
    target = targetSource.getTarget();
    Class<?> targetClass = (target != null ? target.getClass() : null);
    List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
    Object retVal;
    if (chain.isEmpty() && CglibMethodInvocation.isMethodProxyCompatible(method)) {
        // 无 advice 且方法可走 MethodProxy:直接用 methodProxy 调目标,非反射
        Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
        retVal = invokeMethod(target, method, argsToUse, methodProxy);
    }
    else {
        // 有 advice:构造 CglibMethodInvocation 沿链 proceed
        retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
    }
    retVal = processReturnType(proxy, target, method, retVal);
    return retVal;
}

两者骨架几乎一样(取拦截链 → 空则直调、非空则 proceed),区别就在「直调」这一步:JDK 用 invokeJoinpointUsingReflection 反射,CGLIB 用 invokeMethod(..., methodProxy) 走 FastClass 索引。

MethodProxy 为什么比反射快? CGLIB 为代理类和目标类各生成一个 FastClass(索引数组),每个方法有固定 index,invokeSuper 按 index 直接调方法,没有 Method.invoke() 反射开销。想想 FastClass 快在哪: 每个方法预先分好固定 index,调用时按 index 直接命中,省掉了反射里「按名字找方法」的开销。

为什么必须 invokeSuper 而不是 method.invoke? 因为 method.invoke(proxy, args) 里 proxy 是代理子类,会调子类重写后的方法,子类方法内部又调 interceptor.intercept()无限递归invokeSuper 直接调父类原始实现,只织入一次。自己走一遍递归链: method.invoke(proxy) → 命中子类重写的方法 → 子类方法又调 interceptor.intercept() → 又进一次代理 → 无限递归。invokeSuper 直接调父类原始实现,断掉这条环。

对比表

JDK 动态代理CGLIB
生成方式Proxy.newProxyInstanceASM 生成子类字节码
前提需要接口不需要接口
限制只能代理接口方法final 类/方法不可代理
调用反射 Method.invokeFastClass 索引(非反射)
性能慢 2-3x

⑦ 装饰器模式

一句话结论(30s)

装饰器的本质是用「组合」替代「继承」来横向叠加功能,核心价值是「N+M 个类覆盖 N×M 种功能组合」。因为 Java IO 的洋葱模型 new BufferedInputStream(new DataInputStream(new FileInputStream(...))) 是它最经典的工业级实现。

核心原理(2min)

Java IO 三层嵌套,顺序可任意组合、换顺序即换功能组合:

先想:装饰器为什么用「组合」而不是「继承」?——继承把功能锁死在类型上,N×M 种组合就得 N×M 个子类;组合让每个功能独立成类,想叠几层叠几层。

InputStream is = new BufferedInputStream(
                    new DataInputStream(
                        new FileInputStream("data.bin")));
// FileInputStream: 读字节
// DataInputStream: 解析 int/double/String
// BufferedInputStream: 加缓冲减少系统调用

如果不用装饰器,每种组合都要一个类:FileInputStream、BufferedFileInputStream、DataFileInputStream、BufferedDataFileInputStream…… N 个基础 × M 种增强 = N×M 组合 = 类爆炸。装饰器把每个功能独立成一个类,用组合(持有被装饰对象引用 + 实现相同接口)替代继承。自己算一遍:3 个基础流 × 3 种增强,不用装饰器要写几个类? 每个组合一个类,指数级增长——装饰器用「组合」把 N×M 压成 N+M。

底层深入(5-10min)

(复述博客《装饰器模式-JavaIO》)

Java IO 装饰器的「洋葱」能叠起来,靠的是基类 FilterInputStream 这个约定——持有被装饰流、实现同一接口、默认逐层委托:

// java.io.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();   // 默认委托给被装饰对象
    }

    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }
    // ... 其余方法同样逐层委托
}

BufferedInputStreamDataInputStream 都继承它:构造时把上一层的流塞进 in,各自只重写自己关心的方法(缓冲、解析类型),其余继续向下委托。于是 new BufferedInputStream(new DataInputStream(new FileInputStream(...))) 里每个装饰器「既是 InputStream 又是别人的包装」,功能才能任意叠加。

装饰器 vs 代理:结构上几乎一样(都持有目标引用 + 实现相同接口),区别在意图

装饰器代理
目的增强功能(叠加能力)控制访问(权限/懒加载/远程)
调用方知情(显式 new Decorator(target))不知情(@Autowired 拿到代理)
叠加无限多层通常一层
实例化调用方创建目标再传给装饰器代理内部持有目标

Java IO 是装饰器(调用方显式包装),Spring AOP 是代理(调用方拿到的就是代理,不知道有代理)。这是「同构不同意图」的经典考点。先自己说一遍区别,再对照表: 装饰器「增强」且调用方知情(显式 new 包装),代理「控制」且调用方无感知(@Autowired 拿到就是代理);结构同构、意图不同,是这道题的核心。


追问清单(10 个追问点)

1. 单例的 5 种写法?枚举单例为什么最安全?Spring 的 Bean 默认是单例吗?

一句话结论:五种写法是饿汉、synchronized 懒汉、DCL、静态内部类、枚举,按「懒加载/线程安全/防反射/防序列化」递进;枚举最安全因为它靠 JVM 语义天然免疫反射和反序列化。 展开:DCL 的 volatile 防指令重排(半成品对象 NPE);枚举 Constructor.newInstance() 抛异常、反序列化走 Enum.valueOf 不 new;Spring 的 Bean 默认单例,靠 DefaultSingletonBeanRegistrysingletonObjects Map 存实例,但语义是「每容器一个」,不是 GoF 的「JVM 全局唯一」。

2. 工厂方法和抽象工厂的区别?Spring 里哪里用了工厂模式?

一句话结论:工厂方法按产品拆(一种产品一个工厂,解决单个对象创建),抽象工厂按产品族拆(一个工厂生产配套的一组对象)。 展开:简单工厂用 if-else 违背开闭,工厂方法新增产品只加工厂类;Spring 里 FactoryBean 是最典型工厂——工厂本身是 Bean,getBean 拿产品、& 前缀拿工厂本身,MyBatis 的 SqlSessionFactoryBean、AOP 的 ProxyFactoryBean 都是例子。

3. 模板方法模式的核心思想?Spring 里哪里用了?

一句话结论:核心是父类用 final 锁死流程骨架、子类只填可变步骤,遵循好莱坞原则「别调我,我调你」。 展开:Spring 里 JdbcTemplate.execute 是模板(连接+异常翻译+关闭),回调只写 SQL;AbstractApplicationContext.refresh() 是容器启动的 14 步模板方法,子类只重写个别步骤。

4. 策略模式和 if-else 相比好在哪?项目里除了责任链还适合用策略吗?

一句话结论:好在把「变化」隔离成独立类,符合开闭原则,新增策略零改动上下文;适合——比如消息类型的分发路由。 展开:if-else 每次加分支都要改方法、圈复杂度爆炸、单测难写;策略配合 @Autowired Map<String, Strategy> 一行注入,Spring 自动收集实现。消息模块除了校验责任链,发送策略(短信/邮件/推送)也适合用策略模式。

5. 责任链具体怎么拆的?类图怎么画的?

一句话结论:把 7 种消息的校验逻辑各拆成一个 Handler,抽象类定义 handle + compensate,一个 Executor 按 List 顺序执行并逆序补偿。 展开:类结构 = MessageHandler(抽象,handle/compensate)← N 个具体 Handler;MessageChainExecutor 持有 List<MessageHandler>executeChain 正向执行、记录 executed 索引、失败时逆序 compensate。数据用 MessageContext 上下文贯穿。圈复杂度 23 → 每个 ≤ 3,覆盖率 34% → 92%。

6. Netty 的 Pipeline 是怎么用责任链的?和业务消息链有什么异同?

一句话结论:Netty Pipeline 是双向链表责任链,入站 Head→Tail、出站 Tail→Head,Handler 间用 fireChannelRead 传递;业务消息链是单向 List + 索引 + 逆序补偿。 展开:相同点是「不纯责任链」,Handler 解耦、可插拔;不同点是 Netty 双向(读和写两个方向传播)、每个 Channel 独立 Pipeline、异常沿链传递,业务消息链单向且带显式的失败逆序补偿,更偏业务校验场景。

7. JDK 动态代理和 CGLIB 的区别?Spring AOP 什么时候用哪个?

一句话结论:JDK 依赖接口 + 反射调用(慢),CGLIB 用子类 + FastClass 索引非反射调用(快 2-3 倍),但 final 类/方法不可代理。 展开:有接口且按接口注入用 JDK,无接口或字段注入实现类用 CGLIB;Spring Boot 2.x 强制默认 CGLIB,因为 @Autowired UserServiceImpl 字段注入需要代理对象是实现类实例。CGLIB 的 invokeSuper 直接调父类方法,避免 method.invoke 触发的无限递归。

8. 装饰器模式和代理模式有什么区别?Java IO 里哪里用了装饰器?

一句话结论:结构同构、意图不同——装饰器「增强功能」且调用方知情,代理「控制访问」且调用方无感知。 展开:Java IO 的 BufferedInputStream(DataInputStream(FileInputStream)) 是装饰器洋葱模型,N+M 类覆盖 N×M 组合、避免类爆炸;Spring AOP 是代理,@Autowired 拿到的就是代理对象,调用方不知道被代理。

9. 读过哪个框架的源码?说一个印象最深的用了设计模式的地方

一句话结论:印象最深的是 Spring 的 AbstractApplicationContext.refresh() 模板方法 + DefaultSingletonBeanRegistry 三级缓存。 展开refresh() 用模板方法把 14 个启动步骤顺序锁死,子类只重写个别步骤;三级缓存里 singletonObjects(成品)、earlySingletonObjects(早期引用)、singletonFactories(工厂)配合解决循环依赖,同时体现了单例(注册表存一份)、工厂(三级缓存是 ObjectFactory)、模板方法三种模式的组合。

10. 实际写代码时,怎么判断”这里该用设计模式了”?有没有过度设计的经历?

一句话结论:判断标准是「同一类变化出现三次以上、或圈复杂度/重复代码明显超标」才引入模式,而不是上来就套;有过一次过度设计,后来回退成简单 if-else。 展开:以消息模块为例,2 种类型时 if-else 完全够用,扩到 7 种、圈复杂度到 23 才重构责任链。反过来,有次给一个只有两个分支的简单判断套了策略模式 + 工厂 + Map 注入,结果类多了十几个、同事抱怨难读,最后回退成 if-else——教训是「模式是给变化服务的,没有变化就是过度设计」。


Share this post on:

Previous Post
SQL窗口函数——ROW_NUMBER vs RANK vs DENSE_RANK
Next Post
并发编程面试回答——锁、线程池、ThreadLocal 与 JUC