[spring](https://spring.io/) 是 Java 非常重要的框架，且蕴含了一系列设计模式，非常值得研究，本期就通过 [Spring 学习](https://www.cnblogs.com/wmyskxz/p/8820371.html) 这篇文章了解一下 spring。

## spring 为何长寿

spring 作为一个后端框架，拥有 17 年历史，这在前端看来是不可思议的。前端几乎没有一个框架可以流行超过 5 年，就最近来看，react、angular、vue 三大框架可能会活的久一点，他们都是前端相对成熟阶段的产物，我们或多或少可以看出一些设计模式。然而这些前端框架与 spring 比起来还是差距很大，我们来看看 spring 到底强大在哪。

### 设计模式

设计模式是一种思想，不依附于任何编程语言与开发框架。比如你学会了工厂设计模式，可以在后端用，也可以转到前端用，可以在 Go 语言用，也可以在 Typescript 用，可以在 React 框架用，也可以在 Vue 里用，所以设计模式是一种具有迁移能力的知识，学会后可以受益整个职业生涯，而语言、框架则不具备迁移性，前端许多同学都把精力花在学习框架特性上，遇到前端技术迭代时期就尴尬了，这就是为什么大公司面试要问框架原理，就是看看你能否抓住一些不变的东西，所以洋洋洒洒的说上下文相关的细节也不是面试官想要的，真正想听到的是你抽象后对框架原理共性的总结。

spring 框架就用到了许多设计模式，包括：

工厂模式：用工厂生产对象实例来代替原始的 new。所谓工厂就是屏蔽实例话的细节，调用处无需关心实例化对象需要的环境参数，提升可维护性。spring 的 BeanFactory 创建 bean 对象就是工厂模式的体现。
代理模式：允许通过代理对象访问目标对象。Spring 实现 AOP 就是通过动态代理模式。
单例模式：单实例。spring 的 bean 默认都是单例。
包装器模式：将几个不同方法通用部分抽象出来，调用时通过包装器内部引导到不同的实现。比如 spring 连接多种数据库就使用了包装器模式简化。
观察者模式：这个前端同学很熟悉，就是事件机制，spring 中可以通过 ApplicationEvent 实践观察者模式。
适配器模式：通过适配器将接口转换为另一个格式的接口。spring AOP 的增强和通知就使用了适配器模式。
模板方法模式：父类先定义一些函数，这些函数之间存在调用关联，将某些设定为抽象函数等待子类继承时去重写。spring 的 `jdbcTemplate`、`hibernateTemplate` 等数据库操作类使用了模版方法模式。

### 全家桶

spring 作为一个全面的 java 框架，提供了系列全家桶满足各种场景需求：spring mvc、spring security、spring data、spring boot、spring cloud。

- spring boot：简化了 spring 应用配置，约定大于配置的思维。
- spring data：是一个数据操作与访问工具集，比如支持 jdbc、redis 等数据源操作。
- spring cloud：是一个微服务解决方案，基于 spring boot，集成了服务发现、配置管理、消息总线、负载均衡、断路器、数据监控等各种服务治理能力。
- spring security：支持一些安全模型比如单点登录、令牌中继、令牌交换等。
- spring mvc：MVC 思想的 web 框架。

## IOC

IOC（Inverse of Control）控制反转。IOC 是 Spring 最核心部分，因为所有对象调用都离不开 IOC 模式。

假设我们有三个类：Country、Province、City，最大的类别是国家，其次是省、城市，国家类需要调用省类，省类需要调用城市类：

```java
public class Country {
  private Province province;
  public Country(){
    this.province = new Province()
  }
}

public class Province {
  private City city;
  public Province(){
    this.city = new City()
  }
}

public class City {
  public City(){
    // ...
  }
}
```

假设来了一个需求，City 实例化时需增加人口（people）参数，我们就要改动所有类代码：

```java
public class Country {
  private Province province;
  public Country(int people){
    this.province = new Province(people)
  }
}

public class Province {
  private City city;
  public Province(int people){
    this.city = new City(people)
  }
}

public class City {
  public City(int people){
    // ...
  }
}
```

那么在真实业务场景中，一个底层类可能被数以千计的类使用，这么改显然难以维护。IOC 就是为了解决这个问题，它使得我们可以只改动 City 的代码，而不用改动其他类的代码：

```java
public class Country {
  private Province province;
  public Country(Province province){
    this.province = province
  }
}

public class Province {
  private City city;
  public Province(City city){
    this.city = city
  }
}

public class City {
  public City(int people){
    // ...
  }
}
```

可以看到，增加 `people` 属性只需要改动 city 类。然而这样做也是有成本的，就是类实例化步骤会稍微繁琐一些：

```java
City city = new City(1000);
Province province = new Province(city);
Country country = new Country(province);
```

这就是控制反转，由 Country 依赖 Province 变成了类依赖框架（上面的实例化代码）注入。

然而手动维护这种初始化依赖是繁琐的，spring 提供了 bean 容器自动做这件事，我们只需要利用装饰器 Autowired 就可以自动注入依赖：

```java
@Component
public class Country {  
    @Autowired  
    private Province province;
}
@Component
public class Province {  
    @Autowired
    public City city;
}
@Component
public class City {  
}
```

实际上这种自动分析并实例化的手段，不仅比手写方便，还能解决循环依赖的问题。在实际场景中，两个类相互调用是很常见的，假设现在有 A、B 类相互依赖：

```java
@Component
public class A {  
    @Autowired  
    private B b;
}
@Component
public class B {  
    @Autowired
    public A a;
}
```

那么假设我们想获取 A 实例，会经历这样一个过程：

```text
获取 A 实例 -> 实例化不完整 A -> 检测到注入 B -> 实例化不完整 B -> 检测到注入 A -> 注入不完整 A -> 得到完整 B -> 得到完整 A -> 返回 A 实例
```

其实 spring 仅支持单例模式下非构造器的循环依赖，这是因为其内部有一套机制，让 bean 在初始化阶段先提前持有对方引用地址，这样就可以同时实例化两个对象了。

除了方便之外，IOC 配合 spring 容器概念还可以使获取实例时不用关心一个类实例化需要哪些参数，只需要直接申明获取即可，这样在类的数量特别多，尤其是大量代码不是你写的情况下，不需要阅读类源码也可以轻松获取实例，实在是大大提升了可维护性。

说到这就提到了 Bean 容器，在 spring 概念中，Bean 容器是对 class 的加强，如果说 Class 定义了类的基本含义，那 Bean 就是对类进行使用拓展，告诉我们应该如何实例化与使用这个类。

举个例子，比如利用注解描述的这段 Bean 类：

```java
@Configuration
public class CityConfig {
  @Scope("prototype")
  @Lazy
  @Bean(initMethod = "init", destroyMethod = "destroy")
  public City city() {
    return new City()
  }
}
```

可以看到，额外描述了是否延迟加载，是否单例，初始化与析构函数分别是什么等等。

下面给出一个从 Bean 获取实例的例子，采用比较古老的 xml 配置方式：

```java
public interface City {
  Int getPeople();
}
```

```java
public class CityImpl implements City {
  public Int getPeople() {
    return 1000;
  }
}
```

接下来用 xml 描述这个 bean：

```xml
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://www.springframework.org/schema/beans"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" default-autowire="byName">

  <bean id="city" class="xxx.CityImpl"/>
</beans>
```

`bean` 支持的属性还有很多，由于本文并不做入门教学，就不一一列举了，总之 `id` 是一个可选的唯一标志，接下来我们可以通过 `id` 访问到 city 的实例。

```java
public class App {
  public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("classpath:application.xml");

    // 从 context 中读取 Bean，而不 new City()
    City city = context.getBean(City.class);

    System.out.println(city.getPeople());
  }
}
```

可以看到，程序任何地方使用 city 实例，只需要调用 `getBean` 函数，就像一个工厂把实例化过程给承包了，我们不需要关心 City 构造函数要传递什么参数，不需要关心它依赖哪些其他的类，只要这一句话就可以拿到实例，是不是在维护项目时省心了很多。

## AOP

AOP（Aspect Oriented Program）面向切面编程。

AOP 是为了解决主要业务逻辑与次要业务逻辑之间耦合问题的。主要业务逻辑比如登陆、数据获取、查询等，次要业务逻辑比如性能监控、异常处理等等，次要业务逻辑往往有：不重要、和业务关联度低、贯穿多处业务逻辑的特性，如果没有好的设计模式，只能在业务代码里将主要逻辑与次要逻辑混合起来，但 AOP 可以做到主要、次要业务逻辑隔离。

使用 AOP 就是在定义在哪些地方（类、方法）切入，在什么地方切入（方法前、后、前后）以及做什么。

比如说，我们想在某个方法前后分别执行两个函数计算执行时间，下面是主要业务逻辑：

```java
@Component("work")
public class Work {
  public void do() {
    System.out.println("执行业务逻辑");
  }
}
```

再定义切面方法：

```java
@Component
@Aspect
class Broker {
  @Before("execution(* xxx.Work.do())")
  public void before(){
    // 记录开始时间
  }

  @After("execution(* xxx.Work.do())")
  public void after(){
    // 计算时间
  }
}
```

再通过 xml 定义扫描下这两个 Bean，就可以在运行 `work.do()` 之前执行 `before()`，之后执行 `after()`。

还可以完全覆盖原函数，利用 `joinPoint.proceed()` 可以执行原函数：

```java
@Component
@Aspect
class Broker {
  @Around("execution(* xxx.Work.do())")
  public void around(ProceedingJoinPoint joinPoint) {
    // 记录开始时间

    try {
      joinPoint.proceed();
    } catch (Throwable throwable) {
      throwable.printStackTrace();
    }

    // 计算时间
  }
}
```

关于表达式 `"execution(* xxx.Work.do())"` 是用正则的方式匹配，`*` 表示任意返回类型的方法，后面就不用解释了。

可以看到，我们可以在不修改原方法的基础上，在其执行前后增加自定义业务逻辑，或者监控其报错，非常适合做次要业务逻辑，且由于不与主要业务逻辑代码耦合，保证了代码的简洁，且次要业务逻辑不容易遗漏。

## 总结

IOC 特别适合描述业务模型，后端天然需要这一套，然而随着前端越做越重，如果某个业务场景下需要将部分业务逻辑放到前端，也是非常推荐使用 IOC 设计模式来做，这是后端沉淀了近 20 年的经验，没有必要再另辟蹊径。

AOP 对前端有帮助但没有那么大，因为前端业务逻辑较为分散，如果要进行切面编程，往往用 `window` 事件监听来做会更彻底，可能这都是前端没有流行 AOP 的原因。当然前端约定大于配置的趋势下，比如打点或监控都集成到框架内部，往往也做到了业务代码无感，剩下的业务代码也就没有 AOP 的需求。

最后，spring 的低侵入式设计，使得业务代码不用关心框架，让业务代码能够快速在不同框架间切换，这不仅方便了业务开发者，更使得 spring 走向成功，这是前端还需要追赶的。

> 讨论地址是：[精读《Spring 概念》· Issue #265 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/265)

**如果你想参与讨论，请 [点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**

> 关注 **前端精读微信公众号**

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

> 版权声明：自由转载-非商用-非衍生-保持署名（[创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)）



