Spring中使用的设计模式,你都能说全吗?[下]

Posted javatiange

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring中使用的设计模式,你都能说全吗?[下]相关的知识,希望对你有一定的参考价值。

这是 Spring 面试题系列的第二篇 【Spring 中的设计模式】 下半部分,

本篇只回答一个问题:

Spring 中使用了哪些设计模式?分别都是如何实现的?

回顾一下上一篇的概述,从整体上讲,SpringFramework 中使用了 11 种设计模式:

  • 单例模式+原型模式
  • 工厂模式
  • 代理模式
  • 策略模式
  • 模板方法模式
  • 观察者模式
  • 适配器模式
  • 装饰者模式
  • 外观模式
  • 委派模式(不属于GoF23)

这一篇咱把下面的 6 个设计模式也详细解析一下。

喜欢本文的小伙伴不要忘记三连呀 ~ ~ ~ 三克油!!!

模板方法模式

模板方法模式,在 SpringFramework 中使用的那是相当的多啊!即便是初学 SpringFramework ,在学习到 jdbc 部分,也应该接触过一个模板方法模式的体现,就是 JdbcTemplate 的回调机制:(贴两个比较常见的经典方法)

public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
    return result(query(sql, new RowMapperResultSetExtractor<>(rowMapper)));
}

public List<Map<String, Object>> queryForList(String sql) throws DataAccessException {
    return query(sql, getColumnMapRowMapper());
} 

在调用时,只需要一句话就够了:

List<Map<String, Object>> datas = jdbcTemplate.queryForList("select * from user"); 

小伙伴们可能会一脸懵,这么个方法就体现模板方法模式了吗?我也看不出来啊?莫慌,下面咱先回顾下原生 jdbc 的操作步骤,慢慢自然就明白了。

原生jdbc使用步骤

原生的 jdbc 操作需要以下这么多步骤:

 // 注册驱动
    Class.forName("com.mysql.jdbc.Driver");
    // 获取连接
    Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
    // 编写SQL获取预处理对象
    PreparedStatement statement = connection.prepareStatement("select * from user");
    // 执行SQL获取结果集
    ResultSet resultSet = statement.executeQuery();
    // 封装结果数据
    List<Map<String, Object>> datas = new ArrayList<>();
    while (resultSet.next()) {
        Map<String, Object> data = new HashMap<>();
        data.put("id", resultSet.getString("id"));
        data.put("name", resultSet.getString("name"));
        datas.add(data);
    }
    // 关闭连接
    resultSet.close();
    statement.close();
    connection.close();
    // 返回/输出数据
    System.out.println(datas); 

但是,这里面的操作有很多是重复的,所以这一段代码可以抽取为几个方法:

// 获取连接
public Connection getConnection() throws Exception {
    Class.forName("com.mysql.jdbc.Driver");
    return DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
}

// 封装数据
public List<Map<String, Object>> encapsulate(ResultSet resultSet) throws SQLException {
    List<Map<String, Object>> datas = new ArrayList<>();
    while (resultSet.next()) {
        Map<String, Object> data = new HashMap<>();
        data.put("id", resultSet.getString("id"));
        data.put("name", resultSet.getString("name"));
        datas.add(data);
    }
    return datas;
}

// 关闭连接
public void close(Connection connection, PreparedStatement statement, ResultSet resultSet) throws SQLException {
    resultSet.close();
    statement.close();
    connection.close();
} 

如此下来,主干代码就只剩下这些了:

public void query() throws Exception {
    Connection connection = getConnection();
    // 编写SQL获取预处理对象
    PreparedStatement statement = connection.prepareStatement("select * from user");
    // 执行SQL获取结果集
    ResultSet resultSet = statement.executeQuery();
    // 封装结果数据
    List<Map<String, Object>> datas = encapsulate(resultSet);
    close(connection, statement, resultSet);
    // 返回/输出数据
    System.out.println(datas);
} 

那最后稍微调整一下,就可以成为一个通用的方法:

public List<Map<String, Object>> query(String sql, List<Object> params) throws Exception {
    Connection connection = getConnection();
    // 编写SQL获取预处理对象
    PreparedStatement statement = connection.prepareStatement(sql);
    // 填充占位符参数
    for (int i = 0; i < params.size(); i++) {
        statement.setObject(i + 1, params.get(i));
    }
    // 执行SQL获取结果集
    ResultSet resultSet = statement.executeQuery();
    // 封装结果数据
    List<Map<String, Object>> datas = encapsulate(resultSet);
    close(connection, statement, resultSet);
    // 返回数据
    return datas;
} 

目前这个方法已经可以通用了,不过只能返回 Map ,如果想返回模型类对象怎么办呢?这样咱就可以扩展一个自定义结果集封装的方法,让调用者自己决定如何封装。再次改动的代码可以像如下优化:

public <T> List<T> query(String sql, List<Object> params, Function<ResultSet, List<T>> encapsulate) throws Exception {
    Connection connection = getConnection();
    // 编写SQL获取预处理对象
    PreparedStatement statement = connection.prepareStatement(sql);
    // 填充占位符参数
    for (int i = 0; i < params.size(); i++) {
        statement.setObject(i + 1, params.get(i));
    }
    // 执行SQL获取结果集
    ResultSet resultSet = statement.executeQuery();
    // 封装结果数据(借助Function接口实现结果集封装)
    List<T> datas = encapsulate.apply(resultSet);
    close(connection, statement, resultSet);
    // 返回数据
    return datas;
} 

看最后一个参数,我让方法的调用者传一个 Function<ResultSet, List<T>> 类型的自定义处理逻辑,这样返回的数据究竟是什么类型我也就不用管了,完全由调用者自己决定。

如果你觉得自己学习效率低,缺乏正确的指导,可以加入资源丰富,学习氛围浓厚的技术圈一起学习交流吧!
[Java架构群]
群内有许多来自一线的技术大牛,也有在小厂或外包公司奋斗的码农,我们致力打造一个平等,高质量的JAVA交流圈子,不一定能短期就让每个人的技术突飞猛进,但从长远来说,眼光,格局,长远发展的方向才是最重要的。

到这里,模板方法模式就得以体现了,整个查询的动作我只需要让方法调用者传 SQL 、SQL 中可能出现的参数,以及自定义的结果集封装逻辑,其余的动作都不需要方法调用者关注。

可是,这跟 JdbcTemplate 又有什么关系呢???

JdbcTemplate中体现的模板方法

我上面不是摘出来一个 JdbcTemplate 的方法嘛:

public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
    return result(query(sql, new RowMapperResultSetExtractor<>(rowMapper)));
} 

咱点进去看 query 方法的实现:

public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
    // assert logger ......

    class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
        @Override
        @Nullable
        public T doInStatement(Statement stmt) throws SQLException {
            ResultSet rs = null;
            try {
                rs = stmt.executeQuery(sql);
                return rse.extractData(rs);
            }
            finally {
                JdbcUtils.closeResultSet(rs);
            }
        }
        @Override
        public String getSql() {
            return sql;
        }
    }

    return execute(new QueryStatementCallback());
} 

注意看方法参数列表的最后一个:ResultSetExtractor ,它是一个函数式接口,而且它只有一个方法,就是根据结果集返回泛型类型的数据

@FunctionalInterface
public interface ResultSetExtractor<T> {
	@Nullable
	T extractData(ResultSet rs) throws SQLException, DataAccessException;
} 

是不是这个设计跟我上面演示的思路基本一致了?这就是 JdbcTemplate 中体现的模板方法模式。

除了这些 xxxTemplate 中有体现,其实在 SpringFramework 中的最底层 IOC 容器模型,BeanFactoryApplicationContext ,以及 SpringWebMvc 中的 HandlerMappingHandlerAdapter 等等,都有很多模板方法模式的体现。下面咱也举两个例子来体会。

ApplicationContext中体现的模板方法

说到 ApplicationContext ,最经典的方法莫过于 AbstractApplicationContext 中的 refresh :(只截取很少的一部分)

public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
        // ......
        try {
            // ......
            // Initialize other special beans in specific context subclasses.
            onRefresh();
            // ......
        } // catch finally ......
    }
} 

在这个方法中有一个动作是 onRefresh 方法,点进去发现它是一个空方法:

/**
 * Template method which can be overridden to add context-specific refresh work.
 * Called on initialization of special beans, before instantiation of singletons.
 * <p>This implementation is empty.
 * @throws BeansException in case of errors
 * @see #refresh()
 */
protected void onRefresh() throws BeansException {
    // For subclasses: do nothing by default.
} 

注意看文档注释的前两个单词:Template method !!!文档注释已经告诉咱这是一个模板方法了!而且方法体中也说了,**它默认不做任何事情,留给子类扩展使用。**由此咱就可以了解 ApplicationContext 中最经典的模板方法模式设计之一了。

HandlerMapping中体现的模板方法

DispatcherServlet 接收到客户端的请求,要进行实际处理时,需要先根据 uri 寻找能匹配的 HandlerMapping ,这一步它会委托 HandlerMapping 帮忙找(这里面涉及到委派了,下面会讲到),而这个寻找 HandlerMapping 的动作在 AbstractHandlerMappinggetHandler 方法中:

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    Object handler = getHandlerInternal(request);
    if (handler == null) {
        handler = getDefaultHandler();
    }
    // ......
} 

这里面第一句就会调另一个 getHandlerInternal 方法,而这个方法在 AbstractHandlerMapping 中是自己定义的抽象方法:

protected abstract Object getHandlerInternal(HttpServletRequest request) throws Exception; 

这就是非常经典的模板方法模式的体现了:父类 / 抽象类把逻辑步骤都定义出来,具体的动作让子类实现

小结

SpringFramework 中体现的模板方法模式非常多,包括各种 Template ( JdbcTemplateHibernateTemplateRedisTemplate 等),以及核心源码内部设计的模板方法(如 AbstractApplicationContext 中的 refreshonRefreshAbstractHandlerMapping 中的 getHandlergetHandlerInternal 等),它设计的核心思想是父类 / 抽象类把逻辑步骤都定义出来,具体的动作让子类实现。

观察者模式

观察者模式,也被称为发布订阅模式,其实它还有一个叫法:监听器模式。那说到监听器,是不是就立马想起来 SpringFramework 中的 ApplicationListener 了?对了,就这么回事,其实都不用展开讲,小伙伴们也能答得出来了。不过,想回答的漂亮,还需要额外知道一点点东西。

观察者模式的核心

咱都知道,观察者模式的两大核心是:观察者、被观察主题。对应到 SpringFramework 中的概念分别是:事件、监听器

不过我个人比较喜欢把 SpringFramework 的事件驱动核心概念划分为 4 个:事件源、事件、广播器、监听器

  • 事件源:发布事件的对象
  • 事件:事件源发布的信息
  • 广播器:事件真正广播给监听器的对象【即 ApplicationContext
    • ApplicationContext 接口有实现 ApplicationEventPublisher 接口,具备事件广播器的发布事件的能力
  • 监听器:监听事件的对象

在 SpringFramework 中,事件源想要发布事件,需要注入事件广播器,通过事件广播器来广播事件

SpringFramework使用监听器

以下是一个最简单的监听器使用实例:

事件模型
public class ServiceEvent extends ApplicationEvent {
    
    public ServiceEvent(Object source) {
        super(source);
    }
} 
事件源
@Service
public class EventService implements ApplicationContextAware {
    
    private ApplicationContext ctx;
    
    public void publish() {
        ctx.publishEvent(new ServiceEvent(this));
    }
    
    @Override
    public void setApplicationContext(ApplicationContext ctx) throws BeansException {
        this.ctx = ctx;
    }
} 
监听器
@Component
public class MyListener implements ApplicationListener<ServiceEvent> {
    
    @Override
    public void onApplicationEvent(ServiceEvent event) {
        System.out.println("监听到ServiceEvent事件:" + event);
    }
} 

小结

SpringFramework 中实现的观察者模式就是事件驱动机制,可以通过自定义监听器,监听不同的事件,完成不同的业务处理。

适配器模式

了解 SpringWebMvc 中 DispatcherServlet 的工作原理,应该对 HandlerAdapter 不陌生,它就是辅助执行 Controller 中具体方法的适配器。如果只答这一句当然够用,不过回答的再细致点,那自然是更好的。

DispatcherServlet委派HandlerAdapter

DispatcherServletdoDispatch 方法中,有一句代码是根据 HandlerMapping 中的 Handler 获取 HandlerAdapter

 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); 

这个 Handler 就是目标 Controller 中的目标方法,在适配的过程中它会寻找现有的所有 HandlerAdapter 是否能支持执行这个 Handler ,根据 RequestMappingHandlerMapping 的命名,自然也能猜得出来,真正负责执行目标 Controller 中方法的是 RequestMappingHandlerAdapter

HandlerAdapter执行Handler

RequestMappingHandlerAdapter 中,执行目标 Controller 中方法的核心方法是 handle

public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
        throws Exception {
    return handleInternal(request, response, (HandlerMethod) handler);
} 

看到了 handleInternal ,是不是又想到了模板方法?没错,就是这个设计。

在底层,核心的执行 Handler 的逻辑其实就是反射

protected Object doInvoke(Object... args) throws Exception {
    ReflectionUtils.makeAccessible(getBridgedMethod());
    try {
        // method.invoke
        return getBridgedMethod().invoke(getBean(), args);
    } // catch ......
} 

由此可知,HandlerAdapter 的执行其实也没有很神秘,最终还是玩的反射而已。

小结

SpringFramework 中的适配器模式,体现之一是 HandlerAdapter 辅助执行目标 Controller 中的方法,底层是借助反射执行。

装饰者模式

装饰者跟代理、适配器都有些相似,不过装饰者更强调的是在原有的基础上附加额外的动作 / 方法 / 特性,而代理模式控制内部对象访问的意思。

SpringFramework 中体现的装饰者模式,在核心源码中并没有体现,不过在缓存模块 spring-cache 中倒是有一个体现:TransactionAwareCacheDecorator

Bean的包装

了解过 Bean 的创建流程,应该知道最底层的创建动作发生在 AbstractAutowireCapableBeanFactorydoCreateBean 中,而这里面第一波动作就把 BeanWrapper 都创建好了:

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
        throws BeanCreationException {
    // Instantiate the bean.
    BeanWrapper instanceWrapper = null;
    // ......
    if (instanceWrapper == null) {
        instanceWrapper = createBeanInstance(beanName, mbd, args);
    } 

注意看,它创建的动作是 createBeanInstance ,意为“创建 bean 实例”,那就可以理解为:bean 对象已经在这个动作下实例化好了。深入这个方法,最终可以在 createBeanInstance 方法的最后一句 return 中看到 bean 实例的创建,以及 BeanWrapper 的包装:

protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
    // ......
    return instantiateBean(beanName, mbd);
}

protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) {
    try {
        Object beanInstance;
        // 实例化对象......
        BeanWrapper bw = new BeanWrapperImpl(beanInstance);
        initBeanWrapper(bw);
        return bw;
    } // catch ......
} 

看到下面的代码中,它把 bean 的实例对象包装为一个 BeanWrapper ,形成装饰者。

SpringCache的核心设计

SpringCache 主做的是应用业务层级的缓存,它与 JSR107 有一定关系但又不同(Spring 自己实现了一套缓存,就是这个 SpringCache )。

用一张简单的图解释 SpringCache 的核心,大概可以这样理解:

一个 CacheManager 中包含多个 Cache ,一个 Cache 可以简单理解为一个 Map<String, T>

Cache 接口的设计,也能看出来就是 Map 的思路:(核心方法)

 @Nullable
	ValueWrapper get(Object key);

	void put(Object key, @Nullable Object value); 

Cache的装饰者体现

TransactionAwareCacheDecorator 作为装饰者,那肯定要实现 Cache 接口,并且组合一个 Cache 的对象:

public class TransactionAwareCacheDecorator implements Cache {

	private final Cache targetCache; 

这里面,真正体现装饰者的位置是在 put 方法中:

public void put(final Object key, @Nullable final Object value) {
    if (TransactionSynchronizationManager.isSynchronizationActive()) {
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
                TransactionAwareCacheDecorator.this.targetCache.put(key, value);
            }
        });
    }
    else {
        this.targetCache.put(key, value);
    }
} 

可以发现,它会在 put 之前,检查当前线程中是否存在已经开启的事务:如果存在事务,则会将 put 操作注册到事务结束后

小结

SpringFramework 中实现的装饰者模式,体现之一是在 SpringCache 缓存的存放逻辑中,如果执行缓存时的线程中存在事务,则缓存的保存会在事务结束后再执行。

外观模式

外观模式算是比较简单的模式,它强调的是统一入口,内部组合。一个经典的体现是在 IOC 解析 Bean 的定义信息时使用的 BeanDefinitionLoader

BeanDefinitionLoader 中,它组合了 4 种不同类型的解析器:

class BeanDefinitionLoader {
    // 注解式Bean定义解析器
	private final AnnotatedBeanDefinitionReader annotatedReader;
    // xml文件的Bean定义解析器
	private final XmlBeanDefinitionReader xmlReader;
    // Groovy的Bean定义解析器
	private BeanDefinitionReader groovyReader;
    // 类路径的Bean定义扫描器
	private final ClassPathBeanDefinitionScanner scanner; 

尽管组合的解析器很多,但最终暴露出来的方法是同样的名:load

 private int load(Class<?> source) { /* ... */ }
	private int load(GroovyBeanDefinitionSource source) { /* ... */ }
	private int load(Resource source) { /* ... */ }
	private int load(Package source) { /* ... */ }
	private int load(CharSequence source) { /* ... */ } 

由此可以体现出很经典的外观模式。

委派模式

委派模式本不属于 GoF23 中的设计模式,不过既然咱在前面也反复提到过几次,这里咱还是摘出来说两句。

DispatcherServlet 的核心工作流程中,委派模式体现在 HandlerMappingHandlerAdapterViewResolver 接受 DispatcherServlet 的委派,如下面的整体流程图:

到这里,SpringFramework 中使用的设计模式就差不多整理完了,想要回答的尽可能全面、准确、有深度,小伙伴们要好好理解这些模式的设计、思想,以及 SpringFramework 中的源码实现,这样在被面试官问到才会更加游刃有余。

最后

秃头哥给大家分享一篇一线开发大牛整理的java高并发核心编程神仙文档,里面主要包含的知识点有:多线程、线程池、内置锁、JMM、CAS、JUC、高并发设计模式、Java异步回调、CompletableFuture类等。

文档地址:一篇神文就把java多线程,锁,JMM,JUC和高并发设计模式讲明白了

码字不易,如果觉得本篇文章对你有用的话,请给我一键三连!关注作者,后续会有更多的干货分享,请持续关注!

以上是关于Spring中使用的设计模式,你都能说全吗?[下]的主要内容,如果未能解决你的问题,请参考以下文章

谁能说下电脑BIOS COMS里的常用的单词和解释?

关于Spring 的IoC和AOP的面试题,快看看你都能答上来哪些

这些面试中经常被问到的线程池问题,你都能回答的上来吗?

最全的Spring依赖注入方式,你都会了吗?

真单纯!谁说Github是程序员专用的?很多意想不到的事你都能做

小链的烦恼广场舞大妈大爷都能说的头头是道的区块链,却把我搞晕了。